Dsmaterials Vasu

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 49

Session : s1/s2

Topic :Data Structures

Stack and Queue


Describe stack operation.
Answer Stack is a data structure that follows Last in First out strategy. Stack Operations:y y y y y

Push Pushes (inserts) the element in the stack. The location is specified by the pointer. Pop Pulls (removes) the element out of the stack. The location is specified by the pointer Swap: - the two top most elements of the stack can be swapped Peek: - Returns the top element on the stack but does not remove it from the stack Rotate:- the topmost (n) items can be moved on the stack in a rotating fashion

A stack has a fixed location in the memory. When a data element is pushed in the stack, the pointer points to the current element.

Describe queue operation.


Answer Queue is a data structure that follows First in First out strategy. Queue Operations:
y y y y y

Push Inserts the element in the queue at the end. Pop removes the element out of the queue from the front Size Returns the size of the queue Front Returns the first element of the queue. Empty to find if the queue is empty.

Discuss how to implement queue using stack.


Answer A queue can be implemented by using 2 stacks:y y y y

1. An element is inserted in the queue by pushing it into stack 1 2. An element is extracted from the queue by popping it from the stack 2 3. If the stack 2 is empty then all elements currently in stack 1 are transferred to stack 2 but in the reverse order 4. If the stack 2 is not empty just pop the value from stack 2.

Data structure stacks and queues - April 16, 2009 at 17:50 PM by Vidya Sagar

Explain stacks and queues in detail.


A stack is a data structure based on the principle Last In First Out. Stack is container to hold nodes and has two operations - push and pop. Push operation is to add nodes into the stack and pop operation is to delete nodes from the stack and returns the top most node. A queue is a data structure based on the principle First in First Out. The nodes are kept in an order. A node is inserted from the rear of the queue and a node is deleted from the front. The first element inserted in the first element first to delete. Data Structure queues- posted on August 06, 2008 at 13:10 PM by Amit Satpute

Question - What are priority queues?


Answer A priority queue is essentially a list of items in which each item has associated with it a priority Items are inserted into a priority queue in any, arbitrary order. However, items are withdrawn from a priority queue in order of their priorities starting with the highest priority item first. Priority queues are often used in the implementation of algorithms

Question - What is a circular singly linked list?


Answer In a circular singly linked list, the last node of the list is made to point to the first node. This eases the traveling through the list.. Interview Question 3 What is the simple difference between stack and queue type data structures? Answer 3 Stack operates in a Last In First Out (LIFO) fashion while Queue operates in a First In First Out fashion (FIFO). Interview Question 4 Can you tell real life examples for stack and queue ?

Answer 4 Plates arranged one over the other in hotel is an example for stack. Plate that was placed last on top will be the first one to be removed from the stack. Queue in billing section of stores is an example for queue.

Data Structures Interview Questions Set -1 What is data structure? A data structure is a way of organizing data that considers not only the items stored, but also their relationship to each other. Advance knowledge about the relationship between data items allows designing of efficient algorithms for the manipulation of data. List out the areas in which data structures are applied extensively? Compiler Design, Operating System, Database Management System, Statistical analysis package, Numerical Analysis, Graphics, Artificial Intelligence, Simulation If you are using C language to implement the heterogeneous linked list, what pointer type will you use? The heterogeneous linked list contains different data types in its nodes and we need a link, pointer to connect them. It is not possible to use ordinary pointers for this. So we go for void pointer. Void pointer is capable of storing pointer to any type as it is a generic pointer type. What is the data structures used to perform recursion? Stack. Because of its LIFO (Last In First Out) property it remembers its caller, so knows whom to return when the function has to return. Recursion makes use of system stack for storing the return addresses of the function calls. Every recursive function has its equivalent iterative (nonrecursive) function. Even when such equivalent iterative procedures are written, explicit stack is to be used. What are the methods available in storing sequential files ? Straight merging, Natural merging, Polyphase sort, Distribution of Initial runs. List out few of the Application of tree data-structure? The manipulation of Arithmetic expression, Symbol Table construction, Syntax analysis. In RDBMS, what is the efficient data structure used in the internal storage representation? B+ tree. Because in B+ tree, all the data is stored only in leaf nodes, that makes searching easier. This corresponds to the records that shall be stored in leaf nodes. What is a spanning Tree? A spanning tree is a tree associated with a network. All the nodes of the graph appear on the tree

once. A minimum spanning tree is a spanning tree organized so that the total edge weight between nodes is minimized. Does the minimum spanning tree of a graph give the shortest distance between any 2 specified nodes? Minimal spanning tree assures that the total weight of the tree is kept at its minimum. But it doesn't mean that the distance between any two nodes involved in the minimum-spanning tree is minimum. Whether Linked List is linear or Non-linear data structure? According to Access strategies Linked list is a linear one. According to Storage Linked List is a Non-linear one. What is the quickest sorting method to use? The answer depends on what you mean by quickest. For most sorting problems, it just doesn't matter how quick the sort is because it is done infrequently or other operations take significantly more time anyway. Even in cases in which sorting speed is of the essence, there is no one answer. It depends on not only the size and nature of the data, but also the likely order. No algorithm is best in all cases. There are three sorting methods in this author's toolbox that are all very fast and that are useful in different situations. Those methods are quick sort, merge sort, and radix sort. The Quick Sort The quick sort algorithm is of the divide and conquer type. That means it works by reducing a sorting problem into several easier sorting problems and solving each of them. A dividing value is chosen from the input data, and the data is partitioned into three sets: elements that belong before the dividing value, the value itself, and elements that come after the dividing value. The partitioning is performed by exchanging elements that are in the first set but belong in the third with elements that are in the third set but belong in the first Elements that are equal to the dividing element can be put in any of the three sets the algorithm will still work properly. The Merge Sort The merge sort is a divide and conquer sort as well. It works by considering the data to be sorted as a sequence of already-sorted lists (in the worst case, each list is one element long). Adjacent sorted lists are merged into larger sorted lists until there is a single sorted list containing all the elements. The merge sort is good at sorting lists and other data structures that are not in arrays, and it can be used to sort things that don't fit into memory. It also can be implemented as a stable sort. The Radix Sort The radix sort takes a list of integers and puts each element on a smaller list, depending on the value of its least significant byte. Then the small lists are concatenated, and the process is repeated for each more significant byte until the list is sorted. The radix sort is simpler to implement on fixed-length data such as ints.

How can I search for data in a linked list? Unfortunately, the only way to search a linked list is with a linear search, because the only way a linked list's members can be accessed is sequentially. Sometimes it is quicker to take the data from a linked list and store it in a different data structure so that searches can be more efficient. What is the heap? The heap is where malloc(), calloc(), and realloc() get memory. Getting memory from the heap is much slower than getting it from the stack. On the other hand, the heap is much more flexible than the stack. Memory can be allocated at any time and deallocated in any order. Such memory isn't deallocated automatically; you have to call free(). Recursive data structures are almost always implemented with memory from the heap. Strings often come from there too, especially strings that could be very long at runtime. If you can keep data in a local variable (and allocate it from the stack), your code will run faster than if you put the data on the heap. Sometimes you can use a better algorithm if you use the heap faster, or more robust, or more flexible. Its a tradeoff. If memory is allocated from the heap, its available until the program ends. That's great if you remember to deallocate it when you're done. If you forget, it's a problem. A memory leak is some allocated memory that's no longer needed but isn't deallocated. If you have a memory leak inside a loop, you can use up all the memory on the heap and not be able to get any more. (When that happens, the allocation functions return a null pointer.) In some environments, if a program doesn't deallocate everything it allocated, memory stays unavailable even after the program ends. What is the easiest sorting method to use? The answer is the standard library function qsort(). It's the easiest sort by far for several reasons: It is already written. It is already debugged. It has been optimized as much as possible (usually). Void qsort(void *buf, size_t num, size_t size, int (*comp)(const void *ele1, const void *ele2)); Stack:A stack is a linear list of elements for which all insertions and deletions(usually accesses) are made at only one end of the list. They are also called as LIFO lists(Last Input First Output). The operations supported are : 1)IsEmpty(S): returns whether the stack is empty or not. 2)IsFull(S): return whether the stack is full or not. 3)Push(Element X,S): pushes element X on to the top of the stack. 4)Pop(S) : pops an element from the top of the stack on to the output(printing on to the output console isn't necessary though in which case we can define another function Top(S) which gives

the top element of the stack). All the above mentioned operations are of O(1) complexity.

Queue: Queue is a linear list for which all insertions are made at one end and deletions(accesses as well) are made at the other end of the list and the lists are also called as FIFO lists(First Input First Output ). The operations supported are 1)IsEmpty(Q):returns whether the queue is empty or not. 2)IsFull(Q): return whether the queue is full or not. 3)Enqueue(Element X,Q): inserts an element X on the rear side of the queue. 4)Dequeue(Q): removes the element pointed to by the front end of the queue. Similar to a stack ,the operations of the queue are also of O(1) complexity. Dequeue (double ended queue):A Dequeue is a linear list for which insertions and deletions(accesses as well) occur at the ends. Analogous to the operations defined for stack and Queue,we can also define some operations for Dequeue. A simple observation reveals the fact that we can simulate both stack and queue from Dequeue by input and output restrictions. Having dwelt at such a length on these linear lists,we shall also see some interesting questions based on the simple properties of these linear lists. Questions 1)How do you implement 2 stacks using only one array.Your stack routines should not indicate an overflow unless every slot in the array is used? Solution:given an Array,start the first stack S1 from left end and other stack S2 from the right end.while S1 gets grows towards right ,S2 grows towards left.

2)Propose a data structure which supports the stack Push and Pop operations and a third operation FindMin,which returns the smallest element in the data strucuture all in O(1) worst case time. Solution:Use 2 stacks S1 in to which the elements are pushed and S2 in to which only the current minimum is pushed. When one needs to insert an element E ,we first push E on to S1 and then access the top element T of S2 which is the minimum before E has been inserted.If only E is less than T , we push E on to S2 . When one needs to pop an element ,pop the top element of S1 and if this element is also equal to the one on top of S2, then pop it off S2 as well. Hence the current minimum will always be on top of S2 .Hence along with other normal stack operations, access of minimum element is also possible in O(1). 3)Show how to implement 3 stacks in a single array efficiently?(debated question) Solution: It is still up for debate ,as we haven't yet figured out the exact solution.We will soon put the best solution for this problem. 4)Consider a empty stack of integers.Let the numbers 1,2,3,4,5,6 be pushed on to this stack only in the order they appeared from left to right.Let S indicates a push and X indicate a pop operation.Can they be permuted in to the order 325641(output) and order 154623?(if a permutation is possible give the order string of operations. (Hint: SSSSSSXXXXXX outputs 654321) Solution: SSSXXSSXSXXX outputs 325641. 154623 cannot be output as 2 is pushed much before 3 so can appear only after 3 is output. 5)Given a string containing N S's and N X's where S indicates a push operation and X indicates a pop operation, and with the stack initially empty,Formulate a rule to check whether a given string S of operations is admissible or not (Hint: A string S of operations should always abide by the properties of the stack which in this case only means you never pop an element from an empty stack)

Solution:Given a string of length 2N, we wish to check whether the given string of operations is permissible or not with respect to its functioning on a stack. The only restricted operation is pop whose prior requirement is that the stack should not be empty.So while traversing the string from left to right,prior to any pop the stack shouldn't be

empty which means the no of S's is always greater than or equal to that of X's. Hence the condition is at any stage on processing of the string, no of S's > no of X's 6)Find a simple formula for An, the number of permutations the can be printed on an input of n distinct characters to a stack (similar to question 4) Solution:The numbers are input in the order 1,2,3,...,N. So the problem amounts to the number of strings each of N pushes(denoted by S) and N pops(denoted by X). The only criteria to select a string is at any stage of its processing character by character we should have no of S's > no of X's . This problem is no different from parenthesis problem where N needs to give the no of possible permutations of N parenthesis , which if given by Nth catalan number Cn=(2n)!/((n+1)! n!). For more insite into catalan number refer this link. 7)Show that it is possible to obtain the permutation P1P2.........Pn from 1,2,.........n using a stack if and only if there are no indices i < j < k such that Pj < Pk < Pi. Solution:The solution can be arrived simply by veirfying that among the 6 possible orders of Pi,Pj,Pk the rest 5 are possible and the ordering in question i.e is Pi,Pj,Pk is not possible. We leave it to the reader the verification of possibility of the 5 orderings and deal only with proving that the order Pi,Pj,Pk is not possible. Suppose say that the order Pi,Pj,Pk is possible. As Pi is the largest and printed first (i < j < k) followed by Pj and Pk,just before the popping of Pi the ordering of these 3 on the stack shall be Pi,Pk followed by Pj(from top).But as j<k ,Pj is printed prior to Pk contradicting the ordering on the stack. Hence this ordering is not possible.

Linked List
1 Count() Write a Count() function that counts the number of times a given int occurs in a list. The code for this has the classic list traversal structure as demonstrated in Length(). void CountTest() { List myList = BuildOneTwoThree(); // build {1, 2, 3} int count = Count(myList, 2); // returns 1 since there's 1 '2' in the list } /* Given a list and an int, return the number of times that int occurs in the list. */ int Count(struct node* head, int searchFor) { // Your code 11 2 GetNth() Write a GetNth() function that takes a linked list and an integer index and returns the data value stored in the node at that index position. GetNth() uses the C numbering convention that the first node is index 0, the second is index 1, ... and so on. So for the list {42, 13, 666} GetNth() with index 1 should return 13. The index should be in the range [0..length1]. If it is not, GetNth() should assert() fail (or you could implement some other error case strategy). void GetNthTest() { struct node* myList = BuildOneTwoThree(); // build {1, 2, 3} int lastNode = GetNth(myList, 2); // returns the value 3

} Essentially, GetNth() is similar to an array[i] operation the client can ask for elements by index number. However, GetNth() no a list is much slower than [ ] on an array. The advantage of the linked list is its much more flexible memory management we can Push() at any time to add more elements and the memory is allocated as needed. // Given a list and an index, return the data // in the nth node of the list. The nodes are numbered from 0. // Assert fails if the index is invalid (outside 0..lengh-1). int GetNth(struct node* head, int index) { // Your code 3 DeleteList() Write a function DeleteList() that takes a list, deallocates all of its memory and sets its head pointer to NULL (the empty list). void DeleteListTest() { struct node* myList = BuildOneTwoThree(); // build {1, 2, 3} DeleteList(&myList); // deletes the three nodes and sets myList to NULL } Post DeleteList() Memory Drawing The following drawing shows the state of memory after DeleteList() executes in the above sample. Overwritten pointers are shown in gray and deallocated heap memory has an 'X' through it. Essentially DeleteList() just needs to call free() once for each node and set the head pointer to NULL. 12 Stack Heap 123

myList DeleteListTest() myList is overwritten with the value NULL. The three heap blocks are deallocated by calls to free(). Their memory will appear to be intact for a while, but the memory should not be accessed. DeleteList() The DeleteList() implementation will need to use a reference parameter just like Push() so that it can change the caller's memory (myList in the above sample). The implementation also needs to be careful not to access the .next field in each node after the node has been deallocated. void DeleteList(struct node** headRef) { // Your code 4 Pop() Write a Pop() function that is the inverse of Push(). Pop() takes a non-empty list, deletes the head node, and returns the head node's data. If all you ever used were Push() and Pop(), then our linked list would really look like a stack. However, we provide more general functions like GetNth() which what make our linked list more than just a stack. Pop() should assert() fail if there is not a node to pop. Here's some sample code which calls Pop().... void PopTest() {

struct node* head = BuildOneTwoThree(); // build {1, 2, 3} int a = Pop(&head); // deletes "1" node and returns 1 int b = Pop(&head); // deletes "2" node and returns 2 int c = Pop(&head); // deletes "3" node and returns 3 int len = Length(head); // the list is now empty, so len == 0 } Pop() Unlink Pop() is a bit tricky. Pop() needs to unlink the front node from the list and deallocate it with a call to free(). Pop() needs to use a reference parameter like Push() so that it can change the caller's head pointer. A good first step to writing Pop() properly is making the memory drawing for what Pop() should do. Below is a drawing showing a Pop() of the first node of a list. The process is basically the reverse of the 3-Step-Link-In used by Push() (would that be "Ni Knil Pets-3"?). The overwritten pointer value is shown in gray, and the deallocated heap memory has a big 'X' drawn on it... 13 Stack Heap 123 head PopTest() The head pointer advances to refer to the node after the unlinked one. The unlinked node is deallocated by a call to free(). Ironically, the unlinked node itself is not changed

immediately. It is no longer appears in the list just because the head pointer no longer points to it. Pop() /* The opposite of Push(). Takes a non-empty list and removes the front node, and returns the data which was in that node. */ int Pop(struct node** headRef) { // your code... 5 InsertNth() A more difficult problem is to write a function InsertNth() which can insert a new node at any index within a list. Push() is similar, but can only insert a node at the head end of the list (index 0). The caller may specify any index in the range [0..length], and the new node should be inserted so as to be at that index. void InsertNthTest() { struct node* head = NULL; // start with the empty list InsertNth(&head, 0, 13); // build {13) InsertNth(&head, 1, 42); // build {13, 42} InsertNth(&head, 1, 5); // build {13, 5, 42} DeleteList(&head); // clean up after ourselves } 14 InsertNth() is complex you will want to make some drawings to think about your solution and afterwards, to check its correctness.

/* A more general version of Push(). Given a list, an index 'n' in the range 0..length, and a data element, add a new node to the list so that it has the given index. */ void InsertNth(struct node** headRef, int index, int data) { // your code... 6 SortedInsert() Write a SortedInsert() function which given a list that is sorted in increasing order, and a single node, inserts the node into the correct sorted position in the list. While Push() allocates a new node to add to the list, SortedInsert() takes an existing node, and just rearranges pointers to insert it into the list. There are many possible solutions to this problem. void SortedInsert(struct node** headRef, struct node* newNode) { // Your code... 7 InsertSort() Write an InsertSort() function which given a list, rearranges its nodes so they are sorted in increasing order. It should use SortedInsert(). // Given a list, change it to be in sorted order (using SortedInsert()). void InsertSort(struct node** headRef) { // Your code 8 Append() Write an Append() function that takes two lists, 'a' and 'b', appends 'b' onto the end of 'a', and then sets 'b' to NULL (since it is now trailing off the end of 'a'). Here is a drawing of a sample call to Append(a, b) with the start state in gray and the end state in black. At the

end of the call, the 'a' list is {1, 2, 3, 4}, and 'b' list is empty. Stack Heap a12 b 34 15 It turns out that both of the head pointers passed to Append(a, b) need to be reference parameters since they both may need to be changed. The second 'b' parameter is always set to NULL. When is 'a' changed? That case occurs when the 'a' list starts out empty. In that case, the 'a' head must be changed from NULL to point to the 'b' list. Before the call 'b' is {3, 4}. After the call, 'a' is {3, 4}. Stack Heap a b 34 // Append 'b' onto the end of 'a', and then set 'b' to NULL. void Append(struct node** aRef, struct node** bRef) { // Your code... 9 FrontBackSplit() Given a list, split it into two sublists one for the front half, and one for the back half. If the number of elements is odd, the extra element should go in the front list. So FrontBackSplit() on the list {2, 3, 5, 7, 11} should yield the two lists {2, 3, 5} and {7, 11}. Getting this right for all the cases is harder than it looks. You should check your solution against a few cases (length = 2, length = 3, length=4) to make sure that the list gets split correctly near the short-list boundary conditions. If it works right for length=4,

it probably works right for length=1000. You will probably need special case code to deal with the (length <2) cases. Hint. Probably the simplest strategy is to compute the length of the list, then use a for loop to hop over the right number of nodes to find the last node of the front half, and then cut the list at that point. There is a trick technique that uses two pointers to traverse the list. A "slow" pointer advances one nodes at a time, while the "fast" pointer goes two nodes at a time. When the fast pointer reaches the end, the slow pointer will be about half way. For either strategy, care is required to split the list at the right point. /* Split the nodes of the given list into front and back halves, and return the two lists using the reference parameters. If the length is odd, the extra node should go in the front list. */ void FrontBackSplit(struct node* source, struct node** frontRef, struct node** backRef) { // Your code... 16 10 RemoveDuplicates() Write a RemoveDuplicates() function which takes a list sorted in increasing order and deletes any duplicate nodes from the list. Ideally, the list should only be traversed once. /* Remove duplicates from a sorted list. */ void RemoveDuplicates(struct node* head) { // Your code...

11 MoveNode() This is a variant on Push(). Instead of creating a new node and pushing it onto the given list, MoveNode() takes two lists, removes the front node from the second list and pushes it onto the front of the first. This turns out to be a handy utility function to have for several later problems. Both Push() and MoveNode() are designed around the feature that list operations work most naturally at the head of the list. Here's a simple example of what MoveNode() should do... void MoveNodeTest() { struct node* a = BuildOneTwoThree(); // the list {1, 2, 3} struct node* b = BuildOneTwoThree(); MoveNode(&a, &b); // a == {1, 1, 2, 3} // b == {2, 3} } /* Take the node from the front of the source, and move it to the front of the dest. It is an error to call this with the source list empty. */ void MoveNode(struct node** destRef, struct node** sourceRef) { // Your code 12 AlternatingSplit() Write a function AlternatingSplit() that takes one list and divides up its nodes to make two smaller lists. The sublists should be made from alternating elements in the original list. So if the original list is {a, b, a, b, a}, then one sublist should be {a, a, a} and the

other should be {b, b}. You may want to use MoveNode() as a helper. The elements in the new lists may be in any order (for some implementations, it turns out to be convenient if they are in the reverse order from the original list.) /* Given the source list, split its nodes into two shorter lists. If we number the elements 0, 1, 2, ... then all the even elements should go in the first list, and all the odd elements in the second. The elements in the new lists may be in any order. */ void AlternatingSplit(struct node* source, struct node** aRef, struct node** bRef) { // Your code 17 13 ShuffleMerge() Given two lists, merge their nodes together to make one list, taking nodes alternately between the two lists. So ShuffleMerge() with {1, 2, 3} and {7, 13, 1} should yield {1, 7, 2, 13, 3, 1}. If either list runs out, all the nodes should be taken from the other list. The solution depends on being able to move nodes to the end of a list as discussed in the Section 1 review. You may want to use MoveNode() as a helper. Overall, many techniques are possible: dummy node, local reference, or recursion. Using this function and FrontBackSplit(), you could simulate the shuffling of cards. /* Merge the nodes of the two lists into a single list taking a node alternately from each list, and return the new list. */

struct node* ShuffleMerge(struct node* a, struct node* b) { // Your code 14 SortedMerge() Write a SortedMerge() function that takes two lists, each of which is sorted in increasing order, and merges the two together into one list which is in increasing order. SortedMerge() should return the new list. The new list should be made by splicing together the nodes of the first two lists (use MoveNode()). Ideally, Merge() should only make one pass through each list. Merge() is tricky to get right it may be solved iteratively or recursively. There are many cases to deal with: either 'a' or 'b' may be empty, during processing either 'a' or 'b' may run out first, and finally there's the problem of starting the result list empty, and building it up while going through 'a' and 'b'. /* Takes two lists sorted in increasing order, and splices their nodes together to make one big sorted list which is returned. */ struct node* SortedMerge(struct node* a, struct node* b) { // your code... 15 MergeSort() (This problem requires recursion) Given FrontBackSplit() and SortedMerge(), it's pretty easy to write a classic recursive MergeSort(): split the list into two smaller lists, recursively sort those lists, and finally merge the two sorted lists together into a single sorted list. Ironically, this problem is easier than either FrontBackSplit() or SortedMerge(). void MergeSort(struct node* headRef) {

// Your code... 18 16 SortedIntersect() Given two lists sorted in increasing order, create and return a new list representing the intersection of the two lists. The new list should be made with its own memory the original lists should not be changed. In other words, this should be Push() list building, not MoveNode(). Ideally, each list should only be traversed once. This problem along with Union() and Difference() form a family of clever algorithms that exploit the constraint that the lists are sorted to find common nodes efficiently. /* Compute a new sorted list that represents the intersection of the two given sorted lists. */ struct node* SortedIntersect(struct node* a, struct node* b) { // Your code 17 Reverse() Write an iterative Reverse() function that reverses a list by rearranging all the .next pointers and the head pointer. Ideally, Reverse() should only need to make one pass of the list. The iterative solution is moderately complex. It's not so difficult that it needs to be this late in the document, but it goes here so it can be next to #18 Recursive Reverse which is quite tricky. The efficient recursive solution is quite complex (see next problem). (A memory drawing and some hints for Reverse() are below.) void ReverseTest() { struct node* head; head = BuildOneTwoThree();

Reverse(&head); // head now points to the list {3, 2, 1} DeleteList(&head); // clean up after ourselves } Stack Heap 123 List reverse before and after. Before (in gray) the list is {1, 2, 3}. After (in black), the pointers have been rearranged so the head list is {3, 2, 1}. ReverseTest() "Push" Reverse Hint Iterate through the main list. Move each node to the front of the result list as you go. It's like doing a Push() operation with each node, except you use pointer re-arrangement on 19 the existing node instead of allocating a new node. You can use MoveNode() to do most of the work, or hand code the pointer re-arrangement. "3 Pointers" Hint This strategy is not as good as the "Push" strategy, but it's the first one I thought of (thanks to Owen Astrachan for pointing out the better solution). Instead of running a single "current" pointer down the list, run three pointers (front, middle, back) down the list in order: front is on one node, middle is just behind it, and back in turn is one behind middle. Once the code to run the three pointers down the list is clear and tested in a drawing, add code to reverse the .next pointer of the middle node during the iteration. Add code to take care of the empty list and to adjust the head pointer itself.

/* Reverse the given linked list by changing its .next pointers and its head pointer. Takes a pointer (reference) to the head pointer. */ void Reverse(struct node** headRef) { // your code... 18 RecursiveReverse() (This problem is difficult and is only possible if you are familiar with recursion.) There is a short and efficient recursive solution to this problem. As before, the code should only make a single pass over the list. Doing it with multiple passes is easier but very slow, so here we insist on doing it in one pass.. Solving this problem requires a real understanding of pointer code and recursion. /* Recursively reverses the given linked list by changing its .next pointers and its head pointer in one pass of the list. */ void RecursiveReverse(struct node** headRef) { // your code... The Tree-List Recursion Problem Once you are done with these problems, see the best and most complex list recursion problem of all time: The great Tree-List-Recursion problem at http://cslibrary.stanford.edu/109/ 20 Section 3 Solutions 1 Count() Solution

A straightforward iteration down the list just like Length(). int Count(struct node* head, int searchFor) { struct node* current = head; int count = 0; while (current != NULL) { if (current->data == searchFor) count++; current = current->next; } return count; } Alternately, the iteration may be coded with a for loop instead of a while... int Count2(struct node* head, int searchFor) { struct node* current; int count = 0; for (current = head; current != NULL; current = current->next) { if (current->data == searchFor) count++; } return count; } 2 GetNth() Solution Combine standard list iteration with the additional problem of counting over to find the right node. Off-by-one errors are common in this sort of code. Check it carefully against a simple case. If it's right for n=0, n=1, and n=2, it will probably be right for n=1000. int GetNth(struct node* head, int index) { struct node* current = head;

int count = 0; // the index of the node we're currently looking at while (current != NULL) { if (count == index) return(current->data); count++; current = current->next; } assert(0); // if we get to this line, the caller was asking // for a non-existent element so we assert fail. } 21 3 DeleteList() Solution Delete the whole list and set the head pointer to NULL. There is a slight complication inside the loop, since we need extract the .next pointer before we delete the node, since after the delete it will be technically unavailable. void DeleteList(struct node** headRef) { struct node* current = *headRef; // deref headRef to get the real head struct node* next; while (current != NULL) { next = current->next; // note the next pointer free(current); // delete the node current = next; // advance to the next node } *headRef = NULL; // Again, deref headRef to affect the real head back // in the caller. }

4 Pop() Solution Extract the data from the head node, delete the node, advance the head pointer to point at the next node in line. Uses a reference parameter since it changes the head pointer. int Pop(struct node** headRef) { struct node* head; int result; head = *headRef; assert(head != NULL); result = head->data; // pull out the data before the node is deleted *headRef = head->next; // unlink the head node for the caller // Note the * -- uses a reference-pointer // just like Push() and DeleteList(). free(head); // free the head node return(result); // don't forget to return the data from the link } 5 InsertNth() Solution This code handles inserting at the very front as a special case. Otherwise, it works by running a current pointer to the node before where the new node should go. Uses a for loop to march the pointer forward. The exact bounds of the loop (the use of < vs <=, n vs. n-1) are always tricky the best approach is to get the general structure of the iteration correct first, and then make a careful drawing of a couple test cases to adjust the n vs. n-1 cases to be correct. (The so called "OBOB" Off By One Boundary cases.) The OBOB cases are always tricky and not that interesting. Write the correct basic structure and then use a test case to get the OBOB cases correct. Once the insertion point has been determined, this solution uses Push() to do the link in. Alternately, the 3-Step Link In

code could be pasted here directly. 22 void InsertNth(struct node** headRef, int index, int data) { // position 0 is a special case... if (index == 0) Push(headRef, data); else { struct node* current = *headRef; int i; for (i=0; i<index-1; i++) { assert(current != NULL); // if this fails, index was too big current = current->next; } assert(current != NULL); // tricky: you have to check one last time Push(&(current->next), data); // Tricky use of Push() -// The pointer being pushed on is not // in the stack. But actually this works // fine -- Push() works for any node pointer. } } 6 SortedInsert() Solution The basic strategy is to iterate down the list looking for the place to insert the new node. That could be the end of the list, or a point just before a node which is larger than the new node. The three solutions presented handle the "head end" case in different ways... // Uses special case code for the head end void SortedInsert(struct node** headRef, struct node* newNode) {

// Special case for the head end if (*headRef == NULL || (*headRef)->data >= newNode->data) { newNode->next = *headRef; *headRef = newNode; } else { // Locate the node before the point of insertion struct node* current = *headRef; while (current->next!=NULL && current->next->data<newNode->data) { current = current->next; } newNode->next = current->next; current->next = newNode; } } // Dummy node strategy for the head end void SortedInsert2(struct node** headRef, struct node* newNode) { struct node dummy; struct node* current = &dummy; dummy.next = *headRef; while (current->next!=NULL && current->next->data<newNode->data) { current = current->next; } newNode->next = current->next; current->next = newNode;

23 *headRef = dummy.next; } // Local references strategy for the head end void SortedInsert3(struct node** headRef, struct node* newNode) { struct node** currentRef = headRef; while (*currentRef!=NULL && (*currentRef)->data<newNode->data) { currentRef = &((*currentRef)->next); } newNode->next = *currentRef; // Bug: this line used to have // an incorrect (*currRef)->next *currentRef = newNode; } 7 InsertSort() Solution Start with an empty result list. Iterate through the source list and SortedInsert() each of its nodes into the result list. Be careful to note the .next field in each node before moving it into the result list. // Given a list, change it to be in sorted order (using SortedInsert()). void InsertSort(struct node** headRef) { struct node* result = NULL; // build the answer here struct node* current = *headRef; // iterate over the original list struct node* next; while (current!=NULL) { next = current->next; // tricky - note the next pointer before we change it SortedInsert(&result, current);

current = next; } *headRef = result; } 8 Append() Solution The case where the 'a' list is empty is a special case handled first in that case the 'a' head pointer needs to be changed directly. Otherwise we iterate down the 'a' list until we find its last node with the test (current->next != NULL), and then tack on the 'b' list there. Finally, the original 'b' head is set to NULL. This code demonstrates extensive use of pointer reference parameters, and the common problem of needing to locate the last node in a list. (There is also a drawing of how Append() uses memory below.) void Append(struct node** aRef, struct node** bRef) { struct node* current; if (*aRef == NULL) { // Special case if a is empty *aRef = *bRef; } else { // Otherwise, find the end of a, and append b there current = *aRef; while (current->next != NULL) { // find the last node current = current->next; } 24 current->next = *bRef; // hang the b list off the last node } *bRef=NULL; // NULL the original b, since it has been appended above

} Append() Test and Drawing The following AppendTest() code calls Append() to join two lists. What does memory look like just before the call to Append() exits? void AppendTest() { struct node* a; struct node* b; // set a to {1, 2} // set b to {3, 4} Append(&a, &b); } As an example of how reference parameters work, note how reference parameters in Append() point back to the head pointers in AppendTest()... Stack Heap a12 b 34 AppendTest() aRef bRef current Append(&a, &b) 9 FrontBackSplit() Solution Two solutions are presented... // Uses the "count the nodes" strategy

void FrontBackSplit(struct node* source, struct node** frontRef, struct node** backRef) { int len = Length(source); int i; struct node* current = source; 25 if (len < 2) { *frontRef = source; *backRef = NULL; } else { int hopCount = (len-1)/2; //(figured these with a few drawings) for (i = 0; i<hopCount; i++) { current = current->next; } // Now cut at current *frontRef = source; *backRef = current->next; current->next = NULL; } } // Uses the fast/slow pointer strategy void FrontBackSplit2(struct node* source, struct node** frontRef, struct node** backRef) { struct node* fast;

struct node* slow; if (source==NULL || source->next==NULL) { // length < 2 cases *frontRef = source; *backRef = NULL; } else { slow = source; fast = source->next; // Advance 'fast' two nodes, and advance 'slow' one node while (fast != NULL) { fast = fast->next; if (fast != NULL) { slow = slow->next; fast = fast->next; } } // 'slow' is before the midpoint in the list, so split it in two // at that point. *frontRef = source; *backRef = slow->next; slow->next = NULL; } } 10 RemoveDuplicates() Solution Since the list is sorted, we can proceed down the list and compare adjacent nodes. When

adjacent nodes are the same, remove the second one. There's a tricky case where the node after the next node needs to be noted before the deletion. // Remove duplicates from a sorted list void RemoveDuplicates(struct node* head) { struct node* current = head; 26 if (current == NULL) return; // do nothing if the list is empty // Compare current node with next node while(current->next!=NULL) { if (current->data == current->next->data) { struct node* nextNext = current->next->next; free(current->next); current->next = nextNext; } else { current = current->next; // only advance if no deletion } } } 11 MoveNode() Solution The MoveNode() code is most similar to the code for Push(). It's short just changing a couple pointers but it's complex. Make a drawing. void MoveNode(struct node** destRef, struct node** sourceRef) { struct node* newNode = *sourceRef; // the front source node assert(newNode != NULL);

*sourceRef = newNode->next; // Advance the source pointer newNode->next = *destRef; // Link the old dest off the new node *destRef = newNode; // Move dest to point to the new node } 12 AlternatingSplit() Solution The simplest approach iterates over the source list and use MoveNode() to pull nodes off the source and alternately put them on 'a' and b'. The only strange part is that the nodes will be in the reverse order that they occurred in the source list. AlternatingSplit() void AlternatingSplit(struct node* source, struct node** aRef, struct node** bRef) { struct node* a = NULL; // Split the nodes to these 'a' and 'b' lists struct node* b = NULL; struct node* current = source; while (current != NULL) { MoveNode(&a, &current); // Move a node to 'a' if (current != NULL) { MoveNode(&b, &current); // Move a node to 'b' } } *aRef = a; *bRef = b; } 27 AlternatingSplit() Using Dummy Nodes

Here is an alternative approach which builds the sub-lists in the same order as the source list. The code uses a temporary dummy header nodes for the 'a' and 'b' lists as they are being built. Each sublist has a "tail" pointer which points to its current last node that way new nodes can be appended to the end of each list easily. The dummy nodes give the tail pointers something to point to initially. The dummy nodes are efficient in this case because they are temporary and allocated in the stack. Alternately, the "local references" technique could be used to get rid of the dummy nodes (see Section 1 for more details). void AlternatingSplit2(struct node* source, struct node** aRef, struct node** bRef) { struct node aDummy; struct node* aTail = &aDummy; // points to the last node in 'a' struct node bDummy; struct node* bTail = &bDummy; // points to the last node in 'b' struct node* current = source; aDummy.next = NULL; bDummy.next = NULL; while (current != NULL) { MoveNode(&(aTail->next), &current); // add at 'a' tail aTail = aTail->next; // advance the 'a' tail if (current != NULL) { MoveNode(&(bTail->next), &current); bTail = bTail->next; } } *aRef = aDummy.next;

*bRef = bDummy.next; } 13 SuffleMerge() Solution There are four separate solutions included. See Section 1 for information on the various dummy node and reference techniques. SuffleMerge() Dummy Node Not Using MoveNode() struct node* ShuffleMerge(struct node* a, struct node* b) { struct node dummy; struct node* tail = &dummy; dummy.next = NULL; while (1) { if (a==NULL) { // empty list cases tail->next = b; break; } else if (b==NULL) { tail->next = a; break; } else { // common case: move two nodes to tail tail->next = a; tail = a; a = a->next; 28 tail->next = b;

tail = b; b = b->next; } } return(dummy.next); } SuffleMerge() Dummy Node Using MoveNode() Basically the same as above, but use MoveNode(). struct node* ShuffleMerge(struct node* a, struct node* b) { struct node dummy; struct node* tail = &dummy; dummy.next = NULL; while (1) { if (a==NULL) { tail->next = b; break; } else if (b==NULL) { tail->next = a; break; } else { MoveNode(&(tail->next), &a); tail = tail->next; MoveNode(&(tail->next), &b);

tail = tail->next; } } return(dummy.next); } SuffleMerge() Local References Uses a local reference to get rid of the dummy nodes entirely. struct node* ShuffleMerge(struct node* a, struct node* b) { struct node* result = NULL; struct node** lastPtrRef = &result; while (1) { if (a==NULL) { *lastPtrRef = b; break; } else if (b==NULL) { *lastPtrRef = a; break; } else { MoveNode(lastPtrRef, &a); lastPtrRef = &((*lastPtrRef)->next); MoveNode(lastPtrRef, &b); lastPtrRef = &((*lastPtrRef)->next); 29

} } return(result); } SuffleMerge() Recursive The recursive solution is the most compact of all, but is probably not appropriate for production code since it uses stack space proportionate to the lengths of the lists. struct node* ShuffleMerge(struct node* a, struct node* b) { struct node* result; struct node* recur; if (a==NULL) return(b); // see if either list is empty else if (b==NULL) return(a); else { // it turns out to be convenient to do the recursive call first -// otherwise a->next and b->next need temporary storage. recur = ShuffleMerge(a->next, b->next); result = a; // one node from a a->next = b; // one from b b->next = recur; // then the rest return(result); } } 14 SortedMerge() Solution SortedMerge() Using Dummy Nodes The strategy here uses a temporary dummy node as the start of the result list. The pointer

tail always points to the last node in the result list, so appending new nodes is easy. The dummy node gives tail something to point to initially when the result list is empty. This dummy node is efficient, since it is only temporary, and it is allocated in the stack. The loop proceeds, removing one node from either 'a' or 'b', and adding it to tail. When we are done, the result is in dummy.next. struct node* SortedMerge(struct node* a, struct node* b) { struct node dummy; // a dummy first node to hang the result on struct node* tail = &dummy; // Points to the last result node -// so tail->next is the place to add // new nodes to the result. dummy.next = NULL; while (1) { if (a == NULL) { // if either list runs out, use the other list tail->next = b; break; } else if (b == NULL) { tail->next = a; break; } 30 if (a->data <= b->data) { MoveNode(&(tail->next), &a); } else {

MoveNode(&(tail->next), &b); } tail = tail->next; } return(dummy.next); } SortedMerge() Using Local References This solution is structurally very similar to the above, but it avoids using a dummy node. Instead, it maintains a struct node** pointer, lastPtrRef, that always points to the last pointer of the result list. This solves the same case that the dummy node did dealing with the result list when it is empty. If you are trying to build up a list at its tail, either the dummy node or the struct node** "reference" strategy can be used (see Section 1 for details). struct node* SortedMerge2(struct node* a, struct node* b) { struct node* result = NULL; struct node** lastPtrRef = &result; // point to the last result pointer while (1) { if (a==NULL) { *lastPtrRef = b; break; } else if (b==NULL) { *lastPtrRef = a; break; }

if (a->data <= b->data) { MoveNode(lastPtrRef, &a); } else { MoveNode(lastPtrRef, &b); } lastPtrRef = &((*lastPtrRef)->next); // tricky: advance to point to // the next ".next" field } return(result); } SortedMerge() Using Recursion Merge() is one of those nice recursive problems where the recursive solution code is much cleaner than the iterative code. You probably wouldn't want to use the recursive version for production code however, because it will use stack space which is proportional to the length of the lists. struct node* SortedMerge3(struct node* a, struct node* b) { struct node* result = NULL; 31 // Base cases if (a==NULL) return(b); else if (b==NULL) return(a); // Pick either a or b, and recur if (a->data <= b->data) { result = a;

result->next = SortedMerge3(a->next, b); } else { result = b; result->next = SortedMerge3(a, b->next); } return(result); } 15 MergeSort() Solution The MergeSort strategy is: split into sublists, sort the sublists recursively, merge the two sorted lists together to form the answer. void MergeSort(struct node** headRef) { struct node* head = *headRef; struct node* a; struct node* b; // Base case -- length 0 or 1 if ((head == NULL) || (head->next == NULL)) { return; } FrontBackSplit(head, &a, &b); // Split head into 'a' and 'b' sublists // We could just as well use AlternatingSplit() MergeSort(&a); // Recursively sort the sublists MergeSort(&b); *headRef = SortedMerge(a, b); // answer = merge the two sorted lists together }

(Extra for experts) Using recursive stack space proportional to the length of a list is not recommended. However, the recursion in this case is ok it uses stack space which is proportional to the log of the length of the list. For a 1000 node list, the recursion will only go about 10 deep. For a 2000 node list, it will go 11 deep. If you think about it, you can see that doubling the size of the list only increases the depth by 1. 16 SortedIntersect() Solution The strategy is to advance up both lists and build the result list as we go. When the current point in both lists are the same, add a node to the result. Otherwise, advance whichever list is smaller. By exploiting the fact that both lists are sorted, we only traverse each list once. To build up the result list, both the dummy node and local reference strategy solutions are shown... // This solution uses the temporary dummy to build up the result list struct node* SortedIntersect(struct node* a, struct node* b) { struct node dummy; 32 struct node* tail = &dummy; dummy.next = NULL; // Once one or the other list runs out -- we're done while (a!=NULL && b!=NULL) { if (a->data == b->data) { Push((&tail->next), a->data); tail = tail->next; a = a->next; b = b->next; }

else if (a->data < b->data) { // advance the smaller list a = a->next; } else { b = b->next; } } return(dummy.next); } // This solution uses the local reference struct node* SortedIntersect2(struct node* a, struct node* b) { struct node* result = NULL; struct node** lastPtrRef = &result; // Advance comparing the first nodes in both lists. // When one or the other list runs out, we're done. while (a!=NULL && b!=NULL) { if (a->data == b->data) { // found a node for the intersection Push(lastPtrRef, a->data); lastPtrRef = &((*lastPtrRef)->next); a=a->next; b=b->next; } else if (a->data < b->data) { // advance the smaller list a=a->next; }

else { b=b->next; } } return(result); } 17 Reverse() Solution This first solution uses the "Push" strategy with the pointer re-arrangement hand coded inside the loop. There's a slight trickyness in that it needs to save the value of the "current->next" pointer at the top of the loop since the body of the loop overwrites that pointer. /* Iterative list reverse. Iterate through the list left-right. 33 Move/insert each node to the front of the result list -like a Push of the node. */ static void Reverse(struct node** headRef) { struct node* result = NULL; struct node* current = *headRef; struct node* next; while (current != NULL) { next = current->next; // tricky: note the next node current->next = result; // move the node onto the result

result = current; current = next; } *headRef = result; } Here's the variation on the above that uses MoveNode() to do the work... static void Reverse2(struct node** headRef) { struct node* result = NULL; struct node* current = *headRef; while (current != NULL) { MoveNode(&result, &current); } *headRef = result; } Finally, here's the back-middle-front strategy... // Reverses the given linked list by changing its .next pointers and // its head pointer. Takes a pointer (reference) to the head pointer. void Reverse(struct node** headRef) { if (*headRef != NULL) { // special case: skip the empty list /* Plan for this loop: move three pointers: front, middle, back down the list in order. Middle is the main pointer running down the list. Front leads it and Back trails it. For each step, reverse the middle pointer and then advance all three to get the next node.

*/ struct node* middle = *headRef; // the main pointer struct node* front = middle->next; // the two other pointers (NULL ok) struct node* back = NULL; while (1) { middle->next = back; // fix the middle node if (front == NULL) break; // test if done 34 back = middle; // advance the three pointers middle = front; front = front->next; } *headRef = middle; // fix the head pointer to point to the new front } } 18 RecursiveReverse() Solution Probably the hardest part is accepting the concept that the RecursiveReverse(&rest) does in fact reverse the rest. Then then there's a trick to getting the one front node all the way to the end of the list. Make a drwaing to see how the trick works. void RecursiveReverse(struct node** headRef) { struct node* first; struct node* rest; if (*headRef == NULL) return; // empty list base case first = *headRef; // suppose first = {1, 2, 3}

rest = first->next; // rest = {2, 3} if (rest == NULL) return; // empty rest base case RecursiveReverse(&rest); // Recursively reverse the smaller {2, 3} case // after: rest = {3, 2} first->next->next = first; // put the first elem on the end of the list first->next = NULL; // (tricky step -- make a drawing) *headRef = rest; // fix the head pointer } The inefficient soluition is to reverse the last n-1 elements of the list, and then iterate all the way down to the new tail and put the old head node there. That solution is very slow compared to the above which gets the head node in the right place without extra iteration.

You might also like