Chapter 3 - Linked Lists - Data Structures and Algorithms - Narasimha Karumanchi
Chapter 3 - Linked Lists - Data Structures and Algorithms - Narasimha Karumanchi
A linked list is a data structure used for storing collections of data. A linked list has the following
properties.
• Successive elements are connected by pointers
• The last element points to NULL
• Can grow or shrink in size during execution of a program
• Can be made just as long as required (until systems memory exhausts)
• Does not waste memory space (but takes some extra memory for pointers). It
allocates memory as list grows.
3.2 Linked Lists ADT
• Delete List: removes all elements of the list (disposes the list)
• Count: returns the number of elements in the list
• Find nth node from the end of the list
There are many other data structures that do the same thing as linked lists. Before discussing
linked lists it is important to understand the difference between linked lists and arrays. Both
linked lists and arrays are used to store collections of data, and since both are used for the same
purpose, we need to differentiate their usage. That means in which cases arrays are suitable and
in which cases linked lists are suitable.
One memory block is allocated for the entire array to hold the elements of the array. The array
elements can be accessed in constant time by using the index of the particular element as the
subscript.
Why Constant Time for Accessing Array Elements?
To access an array element, the address of an element is computed as an offset from the base
address of the array and one multiplication is needed to compute what is supposed to be added to
the base address to get the memory address of the element. First the size of an element of that data
type is calculated and then it is multiplied with the index of the element to get the value to be
added to the base address.
This process takes one multiplication and one addition. Since these two operations take constant
time, we can say the array access can be performed in constant time.
Advantages of Arrays
Disadvantages of Arrays
• Preallocates all needed memory up front and wastes memory space for indices in the
array that are empty.
• Fixed size: The size of the array is static (specify the array size before using it).
• One block allocation: To allocate the array itself at the beginning, sometimes it may
not be possible to get the memory for the complete array (if the array size is big).
• Complex position-based insertion: To insert an element at a given position, we may
need to shift the existing elements. This will create a position for us to insert the
new element at the desired position. If the position at which we want to add an
element is at the beginning, then the shifting operation is more expensive.
Dynamic Arrays
Dynamic array (also called as growable array, resizable array, dynamic table, or array list) is a
random access, variable-size list data structure that allows elements to be added or removed.
One simple way of implementing dynamic arrays is to initially start with some fixed size array.
As soon as that array becomes full, create the new array double the size of the original array.
Similarly, reduce the array size to half if the elements in the array are less than half.
Note: We will see the implementation for dynamic arrays in the Stacks, Queues and Hashing
chapters.
Linked lists have both advantages and disadvantages. The advantage of linked lists is that they can
be expanded in constant time. To create an array, we must allocate memory for a certain number
of elements. To add more elements to the array when full, we must create a new array and copy
the old array into the new array. This can take a lot of time.
We can prevent this by allocating lots of space initially but then we might allocate more than we
need and waste memory. With a linked list, we can start with space for just one allocated element
and add on new elements easily without the need to do any copying and reallocating.
There are a number of issues with linked lists. The main disadvantage of linked lists is access
time to individual elements. Array is random-access, which means it takes O(1) to access any
element in the array. Linked lists take O(n) for access to an element in the list in the worst case.
Another advantage of arrays in access time is spacial locality in memory. Arrays are defined as
contiguous blocks of memory, and so any array element will be physically near its neighbors. This
greatly benefits from modern CPU caching methods.
Although the dynamic allocation of storage is a great advantage, the overhead with storing and
retrieving data can make a big difference. Sometimes linked lists are hard to manipulate. If the
last item is deleted, the last but one must then have its pointer changed to hold a NULL reference.
This requires that the list is traversed to find the last but one link, and its pointer set to a NULL
reference.
Generally “linked list” means a singly linked list. This list consists of a number of nodes in which
each node has a next pointer to the following element. The link of the last node in the list is
NULL, which indicates the end of the list.
Let us assume that the head points to the first node of the list. To traverse the list we do the
following
• Follow the pointers.
• Display the contents of the nodes (or count) as they are traversed.
• Stop when the next pointer points to NULL.
The ListLength() function takes a linked list as input and counts the number of nodes in the list.
The function given below can be used for printing the list data with extra print function.
Note: To insert an element in the linked list at some position p, assume that after inserting the
element the position of this new node is p.
In this case, a new node is inserted before the current head node. Only one next pointer needs to
be modified (new node’s next pointer) and it can be done in two steps:
• Update the next pointer of new node, to point to the current head.
In this case, we need to modify two next pointers (last nodes next pointer and new nodes next
pointer).
• New nodes next pointer points to NULL.
• Last nodes next pointer points to the new node.
Let us assume that we are given a position where we want to insert the new node. In this case
also, we need to modify two next pointers.
• If we want to add an element at position 3 then we stop at position 2. That means we
traverse 2 nodes and insert the new node. For simplicity let us assume that the
second node is called position node. The new node points to the next node of the
position where we want to add this node.
• Position node’s next pointer now points to the new node.
Let us write the code for all three cases. We must update the first element pointer in the calling
function, not just in the called function. For this reason we need to send a double pointer. The
following code inserts a node in the singly linked list.
Note: We can implement the three variations of the insert operation separately.
Time Complexity: O(n), since, in the worst case, we may need to insert the node at the end of the
list.
Space Complexity: O(1), for creating one temporary variable.
First node (current head node) is removed from the list. It can be done in two steps:
• Create a temporary node which will point to the same node as that of head.
• Now, move the head nodes pointer to the next node and dispose of the temporary
node.
In this case, the last node is removed from the list. This operation is a bit trickier than removing
the first node, because the algorithm should find a node, which is previous to the tail. It can be
done in three steps:
• Traverse the list and while traversing maintain the previous node address also. By
the time we reach the end of the list, we will have two pointers, one pointing to the
tail node and the other pointing to the node before the tail node.
• Update previous node’s next pointer with NULL.
In this case, the node to be removed is always located between two nodes. Head and tail links
are not updated in this case. Such a removal can be done in two steps:
• Similar to the previous case, maintain the previous node while traversing the list.
Once we find the node to be deleted, change the previous node’s next pointer to the
next pointer of the node to be deleted.
• Dispose of the current node to be deleted.
Time Complexity: O(n). In the worst case, we may need to delete the node at the end of the list.
Space Complexity: O(1), for one temporary variable.
The advantage of a doubly linked list (also called two – way linked list) is that given a node in
the list, we can navigate in both directions. A node in a singly linked list cannot be removed
unless we have the pointer to its predecessor. But in a doubly linked list, we can delete a node
even if we don’t have the previous node’s address (since each node has a left pointer pointing to
the previous node and can move backward).
Similar to a singly linked list, let us implement the operations of a doubly linked list. If you
understand the singly linked list operations, then doubly linked list operations are obvious.
Following is a type declaration for a doubly linked list of integers:
Doubly Linked List Insertion
Insertion into a doubly-linked list has three cases (same as singly linked list):
• Inserting a new node before the head.
• Inserting a new node after the tail (at the end of the list).
• Inserting a new node at the middle of the list.
In this case, new node is inserted before the head node. Previous and next pointers need to be
modified and it can be done in two steps:
• Update the right pointer of the new node to point to the current head node (dotted
link in below figure) and also make left pointer of new node as NULL.
• Update head node’s left pointer to point to the new node and make new node as
head. Head
Inserting a Node in Doubly Linked List at the Ending
In this case, traverse the list till the end and insert the new node.
• New node right pointer points to NULL and left pointer points to the end of the list.
As discussed in singly linked lists, traverse the list to the position node and insert the new node.
• New node right pointer points to the next node of the position node where we want
to insert the new node. Also, new node left pointer points to the position node.
• Position node right pointer points to the new node and the next node of position node
left pointer points to new node.
Now, let us write the code for all of these three cases. We must update the first element pointer in
the calling function, not just in the called function. For this reason we need to send a double
pointer. The following code inserts a node in the doubly linked list
Time Complexity: O(n). In the worst case, we may need to insert the node at the end of the list.
Space Complexity: O(1), for creating one temporary variable.
In this case, the first node (current head node) is removed from the list. It can be done in two
steps:
• Create a temporary node which will point to the same node as that of head.
• Now, move the head nodes pointer to the next node and change the heads left pointer
to NULL. Then, dispose of the temporary node.
Deleting the Last Node in Doubly Linked List
This operation is a bit trickier than removing the first node, because the algorithm should find a
node, which is previous to the tail first. This can be done in three steps:
• Traverse the list and while traversing maintain the previous node address also. By
the time we reach the end of the list, we will have two pointers, one pointing to the
tail and the other pointing to the node before the tail.
• Update the next pointer of previous node to the tail node with NULL.
In this case, the node to be removed is always located between two nodes, and the head and tail
links are not updated. The removal can be done in two steps:
• Similar to the previous case, maintain the previous node while also traversing the
list. Upon locating the node to be deleted, change the previous node’s next pointer
to the next node of the node to be deleted.
For example, when several processes are using the same computer resource (CPU) for the same
amount of time, we have to assure that no process accesses the resource before all other
processes do (round robin algorithm). The following is a type declaration for a circular linked
list of integers:
In a circular linked list, we access the elements using the head node (similar to head node in
singly linked list and doubly linked lists).
The circular list is accessible through the node marked head. To count the nodes, the list has to be
traversed from the node marked head, with the help of a dummy node current, and stop the
counting when current reaches the starting node head.
If the list is empty, head will be NULL, and in that case set count = 0. Otherwise, set the current
pointer to the first node, and keep on counting till the current pointer reaches the starting node.
Time Complexity: O(n), for scanning the complete list of size n.
Space Complexity: O(1), for creating one temporary variable.
We assume here that the list is being accessed by its head node. Since all the nodes are arranged
in a circular fashion, the tail node of the list will be the node previous to the head node. Let us
assume we want to print the contents of the nodes starting with the head node. Print its contents,
move to the next node and continue printing till we reach the head node again.
Time Complexity: O(n), for scanning the complete list of size n.
Space Complexity: O(1), for temporary variable.
Let us add a node containing data, at the end of a list (circular list) headed by head. The new
node will be placed just after the tail node (which is the last node of the list), which means it will
have to be inserted in between the tail node and the first node.
• Create a new node and initially keep its next pointer pointing to itself.
• Update the next pointer of the new node with the head node and also traverse the list
to the tail. That means in a circular list we should stop at the node whose next node
is head.
• Update the next pointer of the previous node to point to the new node and we get the
list as shown below.
Time Complexity: O(n), for scanning the complete list of size n.
Space Complexity: O(1), for temporary variable.
The only difference between inserting a node at the beginning and at the end is that, after inserting
the new node, we just need to update the pointer. The steps for doing this are given below:
• Create a new node and initially keep its next pointer pointing to itself.
• Update the next pointer of the new node with the head node and also traverse the list
until the tail. That means in a circular list we should stop at the node which is its
previous node in the list.
• Update the previous head node in the list to point to the new node.
The list has to be traversed to reach the last but one node. This has to be named as the tail node,
and its next field has to point to the first node. Consider the following list.
To delete the last node 40, the list has to be traversed till you reach 7. The next field of 7 has to
be changed to point to 60, and this node must be renamed pTail.
• Traverse the list and find the tail node and its previous node.
• Update the next pointer of tail node’s previous node to point to head.
The first node can be deleted by simply replacing the next field of the tail node with the next field
of the first node.
• Find the tail node of the linked list by traversing the list. Tail node is the previous
node to the head node which we want to delete.
• Create a temporary node which will point to the head. Also, update the tail nodes
next pointer to point to next node of head (as shown below).
• Now, move the head pointer to next node. Create a temporary node which will point
to head. Also, update the tail nodes next pointer to point to next node of head (as
shown below).
Time Complexity: O(n), for scanning the complete list of size n.
Space Complexity: O(1), for a temporary variable.
Circular linked lists are used in managing the computing resources of a computer. We can use
circular lists for implementing stacks and queues.
In conventional implementation, we need to keep a forward pointer to the next item on the list and
a backward pointer to the previous item. That means elements in doubly linked list
implementations consist of data, a pointer to the next node and a pointer to the previous node in
the list as shown below.
The ptrdiff pointer field contains the difference between the pointer to the next node and the
pointer to the previous node. The pointer difference is calculated by using exclusive-or (⊕)
operation.
The ptrdiff of the start node (head node) is the ⊕ of NULL and next node (next node to head).
Similarly, the ptrdiff of end node is the ⊕ of previous node (previous to end node) and NULL. As
an example, consider the following linked list.
For the example above, let us assume that we are at C node and want to move to B. We know that
C’s ptrdiff is defined as B ⊕ D. If we want to move to B, performing ⊕ on C’s ptrdiff with D
would give B. This is due to the fact that
(B ⊕ D) ⊕ D = B(since, D ⊕ D= 0)
Similarly, if we want to move to D, then we have to apply ⊕ to C’s ptrdiff with B to give D.
(B ⊕ D) ⊕ B = D (since, B © B=0)
From the above discussion we can see that just by using a single pointer, we can move back and
forth. A memory-efficient implementation of a doubly linked list is possible with minimal
compromising of timing efficiency.
One of the biggest advantages of linked lists over arrays is that inserting an element at any
location takes only O(1) time. However, it takes O(n) to search for an element in a linked list.
There is a simple variation of the singly linked list called unrolled linked lists.
An unrolled linked list stores multiple elements in each node (let us call it a block for our
convenience). In each block, a circular linked list is used to connect all nodes.
Assume that there will be no more than n elements in the unrolled linked list at any time. To
simplify this problem, all blocks, except the last one, should contain exactly elements. Thus,
there will be no more than blocks at any time.
Note that each shift operation, which includes removing a node from the tail of the circular linked
list in a block and inserting a node to the head of the circular linked list in the block after, takes
only O(1). The total time complexity of an insertion operation for unrolled linked lists is therefore
O( ); there are at most O( ) blocks and therefore at most O( ) shift operations.
1. A temporary pointer is needed to store the tail of A.
2. In block A, move the next pointer of the head node to point to the second-to-last
node, so that the tail node of A can be removed.
3. Let the next pointer of the node, which will be shifted (the tail node of A), point
to the tail node of B.
4. Let the next pointer of the head node of B point to the node temp points to.
5. Finally, set the head pointer of B to point to the node temp points to. Now the
node temp points to becomes the new head node of B.
6. temp pointer can be thrown away. We have completed the shift operation to
move the original tail node of A to become the new head node of B.
Performance
With unrolled linked lists, there are a couple of advantages, one in speed and one in space. First,
if the number of elements in each block is appropriately sized (e.g., at most the size of one cache
line), we get noticeably better cache performance from the improved memory locality. Second,
since we have O(n/m) links, where n is the number of elements in the unrolled linked list and m is
the number of elements we can store in any block, we can also save an appreciable amount of
space, which is particularly noticeable if each element is small.
To compare the overhead for an unrolled list, elements in doubly linked list implementations
consist of data, a pointer to the next node, and a pointer to the previous node in the list, as shown
below.
Assuming we have 4 byte pointers, each node is going to take 8 bytes. But the allocation overhead
for the node could be anywhere between 8 and 16 bytes. Let’s go with the best case and assume it
will be 8 bytes. So, if we want to store IK items in this list, we are going to have 16KB of
overhead.
Now, let’s think about an unrolled linked list node (let us call it LinkedBlock). It will look
something like this:
Therefore, allocating a single node (12 bytes + 8 bytes of overhead) with an array of 100
elements (400 bytes + 8 bytes of overhead) will now cost 428 bytes, or 4.28 bytes per element.
Thinking about our IK items from above, it would take about 4.2KB of overhead, which is close
to 4x better than our original list. Even if the list becomes severely fragmented and the item arrays
are only 1/2 full on average, this is still an improvement. Also, note that we can tune the array
size to whatever gets us the best overhead for our application.
Implementation
3.11 Skip Lists
Binary trees can be used for representing abstract data types such as dictionaries and ordered
lists. They work well when the elements are inserted in a random order. Some sequences of
operations, such as inserting the elements in order, produce degenerate data structures that give
very poor performance. If it were possible to randomly permute the list of items to be inserted,
trees would work well with high probability for any input sequence. In most cases queries must
be answered on-line, so randomly permuting the input is impractical. Balanced tree algorithms re-
arrange the tree as operations are performed to maintain certain balance conditions and assure
good performance.
Skip lists are a probabilistic alternative to balanced trees. Skip list is a data structure that can be
used as an alternative to balanced binary trees (refer to Trees chapter). As compared to a binary
tree, skip lists allow quick search, insertion and deletion of elements. This is achieved by using
probabilistic balancing rather than strictly enforce balancing. It is basically a linked list with
additional pointers such that intermediate nodes can be skipped. It uses a random number
generator to make some decisions.
In an ordinary sorted linked list, search, insert, and delete are in O(n) because the list must be
scanned node-by-node from the head to find the relevant node. If somehow we could scan down
the list in bigger steps (skip down, as it were), we would reduce the cost of scanning. This is the
fundamental idea behind Skip Lists.
In a simple linked list that consists of n elements, to perform a search n comparisons are required
in the worst case. If a second pointer pointing two nodes ahead is added to every node, the
number of comparisons goes down to n/2 + 1 in the worst case.
Adding one more pointer to every fourth node and making them point to the fourth node ahead
reduces the number of comparisons to ⌈n/2⌉ + 2. If this strategy is continued so that every node
with i pointers points to 2 * i – 1 nodes ahead, O(logn) performance is obtained and the number
of pointers has only doubled (n + n/2 + n/4 + n/8 + n/16 + .... = 2n).
The find, insert, and remove operations on ordinary binary search trees are efficient, O(logn),
when the input data is random; but less efficient, O(n), when the input data is ordered. Skip List
performance for these same operations and for any data set is about as good as that of randomly-
built binary search trees - namely O(logn).
In simple terms, Skip Lists are sorted linked lists with two differences:
• The nodes in an ordinary list have one next reference. The nodes in a Skip List have
many next references (also called forward references).
• The number of forward references for a given node is determined probabilistically.
We speak of a Skip List node having levels, one level per forward reference. The number of
levels in a node is called the size of the node. In an ordinary sorted list, insert, remove, and find
operations require sequential traversal of the list. This results in O(n) performance per operation.
Skip Lists allow intermediate nodes in the list to be skipped during a traversal - resulting in an
expected performance of O(logn) per operation.
Implementation
3.12 Linked Lists: Problems & Solutions
Solution: Brute-Force Method: Start with the first node and count the number of nodes present
after that node. If the number of nodes is < n – 1 then return saying “fewer number of nodes in the
list”. If the number of nodes is > n – 1 then go to next node. Continue this until the numbers of
nodes after current node are n – 1.
Time Complexity: O(n2), for scanning the remaining list (from current node) for each node.
Space Complexity: O(1).
Problem-3 Can we improve the complexity of Problem-2?
Solution: Yes, using hash table. As an example consider the following list.
In this approach, create a hash table whose entries are < position of node, node address >. That
means, key is the position of the node in the list and value is the address of that node.
By the time we traverse the complete list (for creating the hash table), we can find the list length.
Let us say the list length is M. To find nth from the end of linked list, we can convert this to M- n
+ 1th from the beginning. Since we already know the length of the list, it is just a matter of
returning M- n + 1th key value from the hash table.
Time Complexity: Time for creating the hash table, T(m) = O(m).
Space Complexity: Since we need to create a hash table of size m, O(m).
Problem-4 Can we use the Problem-3 approach for solving Problem-2 without creating the
hash table?
Solution: Yes. If we observe the Problem-3 solution, what we are actually doing is finding the
size of the linked list. That means we are using the hash table to find the size of the linked list. We
can find the length of the linked list just by starting at the head node and traversing the list.
So, we can find the length of the list without creating the hash table. After finding the length,
compute M – n + 1 and with one more scan we can get the M – n+ 1th node from the beginning.
This solution needs two scans: one for finding the length of the list and the other for finding M –
n+ 1th node from the beginning.
Time Complexity: Time for finding the length + Time for finding the M – n + 1th node from the
beginning. Therefore, T(n) = O(n) + O(n) ≈ O(n). Space Complexity: O(1). Hence, no need to
create the hash table.
Problem-5 Can we solve Problem-2 in one scan?
Solution: Yes. Efficient Approach: Use two pointers pNthNode and pTemp. Initially, both point
to head node of the list. pNthNode starts moving only after pTemp has made n moves.
From there both move forward until pTemp reaches the end of the list. As a result pNthNode
points to nth node from the end of the linked list.
Solution: Brute-Force Approach. As an example, consider the following linked list which has a
loop in it. The difference between this list and the regular list is that, in this list, there are two
nodes whose next pointers are the same. In regular singly linked lists (without a loop) each node’s
next pointer is unique.
That means the repetition of next pointers indicates the existence of a loop.
One simple and brute force way of solving this is, start with the first node and see whether there
is any node whose next pointer is the current node’s address. If there is a node with the same
address then that indicates that some other node is pointing to the current node and we can say a
loop exists. Continue this process for all the nodes of the linked list.
Does this method work? As per the algorithm, we are checking for the next pointer addresses,
but how do we find the end of the linked list (otherwise we will end up in an infinite loop)?
Note: If we start with a node in a loop, this method may work depending on the size of the loop.
Problem-7 Can we use the hashing technique for solving Problem-6?
Algorithm:
• Traverse the linked list nodes one by one.
• Check if the address of the node is available in the hash table or not.
• If it is already available in the hash table, that indicates that we are visiting the node
that was already visited. This is possible only if the given linked list has a loop in
it.
• If the address of the node is not available in the hash table, insert that node’s address
into the hash table.
• Continue this process until we reach the end of the linked list or we find the loop.
Time Complexity; O(n) for scanning the linked list. Note that we are doing a scan of only the
input.
Space Complexity; O(n) for hash table.
Problem-8 Can we solve Problem-6 using the sorting technique?
Solution: No. Consider the following algorithm which is based on sorting. Then we see why this
algorithm fails.
Algorithm:
• Traverse the linked list nodes one by one and take all the next pointer values into an
array.
• Sort the array that has the next node pointers.
• If there is a loop in the linked list, definitely two next node pointers will be pointing
to the same node.
• After sorting if there is a loop in the list, the nodes whose next pointers are the same
will end up adjacent in the sorted list.
• If any such pair exists in the sorted list then we say the linked list has a loop in it.
Problem with the above algorithm: The above algorithm works only if we can find the length of
the list. But if the list has a loop then we may end up in an infinite loop. Due to this reason the
algorithm fails.
Problem-9 Can we solve the Problem-6 in O(n)?
Solution: Yes. Efficient Approach (Memoryless Approach): This problem was solved by
Floyd. The solution is named the Floyd cycle finding algorithm. It uses two pointers moving at
different speeds to walk the linked list. Once they enter the loop they are expected to meet, which
denotes that there is a loop.
This works because the only way a faster moving pointer would point to the same location as a
slower moving pointer is if somehow the entire list or a part of it is circular. Think of a tortoise
and a hare running on a track. The faster running hare will catch up with the tortoise if they are
running in a loop. As an example, consider the following example and trace out the Floyd
algorithm. From the diagrams below we can see that after the final step they are meeting at some
point in the loop which may not be the starting point of the loop.
Note: slowPtr (tortoise) moves one pointer at a time and fastPtr (hare) moves two pointers at a
time.
Time Complexity: O(n). Space Complexity: O(1).
Problem-10 are given a pointer to the first element of a linked list L. There are two
possibilities for L: it either ends (snake) or its last element points back to one of the
earlier elements in the list (snail). Give an algorithm that tests whether a given list L is a
snake or a snail.
Solution: It is the same as Problem-6.
Problem-11 Check whether the given linked list is NULL-terminated or not. If there is a
cycle find the start node of the loop.
Solution: The solution is an extension to the solution in Problem-9. After finding the loop in the
linked list, we initialize the slowPtr to the head of the linked list. From that point onwards both
slowPtr and fastPtr move only one node at a time. The point at which they meet is the start of the
loop. Generally we use this method for removing the loops.
Time Complexity: O(n). Space Complexity: O(1).
Problem-12 From the previous discussion and problems we understand that the meeting of
tortoise and hare concludes the existence of the loop, but how does moving the tortoise to
the beginning of the linked list while keeping the hare at the meeting place, followed by
moving both one step at a time, make them meet at the starting point of the cycle?
Solution: This problem is at the heart of number theory. In the Floyd cycle finding algorithm,
notice that the tortoise and the hare will meet when they are n × L, where L is the loop length.
Furthermore, the tortoise is at the midpoint between the hare and the beginning of the sequence
because of the way they move. Therefore the tortoise is n × L away from the beginning of the
sequence as well. If we move both one step at a time, from the position of the tortoise and from
the start of the sequence, we know that they will meet as soon as both are in the loop, since they
are n × L, a multiple of the loop length, apart. One of them is already in the loop, so we just move
the other one in single step until it enters the loop, keeping the other n × L away from it at all
times.
Problem-13 In the Floyd cycle finding algorithm, does it work if we use steps 2 and 3
instead of 1 and 2?
Solution: Yes, but the complexity might be high. Trace out an example.
Problem-14 Check whether the given linked list is NULL-terminated. If there is a cycle, find
the length of the loop.
Solution: This solution is also an extension of the basic cycle detection problem. After finding the
loop in the linked list, keep the slowPtr as it is. The fastPtr keeps on moving until it again comes
back to slowPtr. While moving fastPtr, use a counter variable which increments at the rate of 1.
Recursive version: We will find it easier to start from the bottom up, by asking and answering
tiny questions (this is the approach in The Little Lisper):
• What is the reverse of NULL (the empty list)? NULL.
• What is the reverse of a one element list? The element itself.
• What is the reverse of an n element list? The reverse of the second element followed
by the first element.
Solution: Brute-Force Approach: One easy solution is to compare every node pointer in the first
list with every other node pointer in the second list by which the matching node pointers will lead
us to the intersecting node. But, the time complexity in this case will be O(mn) which will be
high.
Time Complexity: O(mn). Space Complexity: O(1).
Problem-18 Can we solve Problem-17 using the sorting technique?
Solution: No. Consider the following algorithm which is based on sorting and see why this
algorithm fails.
Algorithm:
• Take first list node pointers and keep them in some array and sort them.
• Take second list node pointers and keep them in some array and sort them.
• After sorting, use two indexes: one for the first sorted array and the other for the
second sorted array.
• Start comparing values at the indexes and increment the index according to
whichever has the lower value (increment only if the values are not equal).
• At any point, if we are able to find two indexes whose values are the same, then that
indicates that those two nodes are pointing to the same node and we return that
node.
Time Complexity: Time for sorting lists + Time for scanning (for comparing)
= O(mlogm) +O(nlogn) +O(m + n) We need to consider the one that gives the
maximum value.
Any problem with the above algorithm? Yes. In the algorithm, we are storing all the node
pointers of both the lists and sorting. But we are forgetting the fact that there can be many repeated
elements. This is because after the merging point, all node pointers are the same for both the lists.
The algorithm works fine only in one case and it is when both lists have the ending node at their
merge point.
Problem-19 Can we solve Problem-17 using hash tables?
Solution: Yes.
Algorithm:
• Select a list which has less number of nodes (If we do not know the lengths
beforehand then select one list randomly).
• Now, traverse the other list and for each node pointer of this list check whether the
same node pointer exists in the hash table.
• If there is a merge point for the given lists then we will definitely encounter the node
pointer in the hash table.
Time Complexity: Time for creating the hash table + Time for scanning the second list = O(m) +
O(n) (or O(n) + O(m), depending on which list we select for creating the hash table. But in both
cases the time complexity is the same. Space Complexity: O(n) or O(m).
Problem-20 Can we use stacks for solving the Problem-17?
Solution: Yes.
Algorithm:
• Create two stacks: one for the first list and one for the second list.
• Traverse the first list and push all the node addresses onto the first stack.
• Traverse the second list and push all the node addresses onto the second stack.
• Now both stacks contain the node address of the corresponding lists.
• Now compare the top node address of both stacks.
• If they are the same, take the top elements from both the stacks and keep them in
some temporary variable (since both node addresses are node, it is enough if we
use one temporary variable).
• Continue this process until the top node addresses of the stacks are not the same.
• This point is the one where the lists merge into a single list.
• Return the value of the temporary variable.
Solution: Yes. Using “finding the first repeating number” approach in an array (for algorithm
refer to Searching chapter).
Algorithm:
• Create an array A and keep all the next pointers of both the lists in the array.
• In the array find the first repeating element [Refer to Searching chapter for
algorithm].
• The first repeating number indicates the merging point of both the lists.
Solution: Yes. By combining sorting and search techniques we can reduce the complexity.
Algorithm:
• Create an array A and keep all the next pointers of the first list in the array.
• Sort these array elements.
• Then, for each of the second list elements, search in the sorted array (let us assume
that we are using binary search which gives O(logn)).
• Since we are scanning the second list one by one, the first repeating element that
appears in the array is nothing but the merging point.
Time Complexity: Time for sorting + Time for searching = O(Max(mlogm, nlogn)).
Space Complexity: O(Max(m, n)).
Problem-23 Can we improve the complexity for Problem-17?
Solution: Yes.
Efficient Approach:
• Find lengths (L1 and L2) of both lists - O(n) + O(m) = O(max(m, n)).
• Take the difference d of the lengths -- O(1).
• Make d steps in longer list -- O(d).
• Step in both lists in parallel until links to next node match -- O(min(m, n)).
• Total time complexity = O(max(m, n)).
• Space Complexity = O(1).
Problem-24 How will you find the middle of the linked list?
Solution: Brute-Force Approach: For each of the node, count how many nodes are there in the
list, and see whether it is the middle node of the list.
Algorithm:
• Traverse the list and find the length of the list.
• After finding the length, again scan the list and locate n/2 node from the beginning.
Time Complexity: Time for finding the length of the list + Time for locating middle node = O(n) +
O(n) ≈ O(n).
Space Complexity: O(1).
Problem-26 Can we use the hash table for solving Problem-24?
Time Complexity: Time for creating the hash table. Therefore, T(n) = O(n).
Space Complexity: O(n). Since we need to create a hash table of size n.
Problem-27 Can we solve Problem-24 just in one scan?
Solution: Efficient Approach: Use two pointers. Move one pointer at twice the speed of the
second. When the first pointer reaches the end of the list, the second pointer will be pointing to
the middle node.
Note: If the list has an even number of nodes, the middle node will be of ⌊n/2⌋.
Time Complexity: O(n). Space Complexity: O(1).
Problem-28 How will you display a Linked List from the end?
Solution: Traverse recursively till the end of the linked list. While coming back, start printing the
elements.
Recursive:
Time Complexity: O(n + m), where n and m are lengths of two lists.
Iterative:
Time Complexity: O(n + m), where n and m are lengths of two lists.
Problem-32 Reverse the linked list in pairs. If you have a linked list that holds 1 → 2 → 3
→ 4 → X, then after the function has been called the linked list would hold 2 → 1 → 4 →
3 → X.
Solution:
Recursive:
Iterative:
Algorithm:
• Store the mid and last pointers of the circular linked list using Floyd cycle finding
algorithm.
• Make the second half circular.
• Make the first half circular.
• Set head pointers of the two linked lists.
Algorithm:
1. Get the middle of the linked list.
2. Reverse the second half of the linked list.
3. Compare the first half and second half.
4. Construct the original linked list by reversing the second half again and
attaching it back to the first half.
Solution: Yes. Create a linked list and at the same time keep it in a hash table. For n elements we
have to keep all the elements in a hash table which gives a preprocessing time of O(n).To read
any element we require only constant time O(1) and to read n elements we require n * 1 unit of
time = n units. Hence by using amortized analysis we can say that element access can be
performed within O(1) time.
Time Complexity – O(1) [Amortized]. Space Complexity - O(n) for Hash Table.
Problem-40 Josephus Circle: N people have decided to elect a leader by arranging
themselves in a circle and eliminating every Mth person around the circle, closing ranks as
each person drops out. Find which person will be the last one remaining (with rank 1).
Solution: Assume the input is a circular linked list with N nodes and each node has a number
(range 1 to N) associated with it. The head node has number 1 as data.
Problem-41 Given a linked list consists of data, a next pointer and also a random pointer
which points to a random node of the list. Give an algorithm for cloning the list.
Solution: We can use a hash table to associate newly created nodes with the instances of node in
the given list.
Algorithm:
• Scan the original list and for each node X, create a new node Y with data of X, then
store the pair (X, Y) in hash table using X as a key. Note that during this scan set Y
→ next and Y → random to NULL and we will fix them in the next scan.
• Now for each node X in the original list we have a copy Y stored in our hash table.
We scan the original list again and set the pointers building the new list.
Time Complexity: O(n). Space Complexity: O(n).
Problem-42 Can we solve Problem-41 without any extra space?
Solution: Yes.
Time Complexity: O(3n) ≈ O(n). Space Complexity: O(1).
Problem-43 We are given a pointer to a node (not the tail node) in a singly linked list. Delete
that node from the linked list.
Solution: To delete a node, we have to adjust the next pointer of the previous node to point to the
next node instead of the current one. Since we don’t have a pointer to the previous node, we can’t
redirect its next pointer. So what do we do? We can easily get away by moving the data from the
next node into the current node and then deleting the next node.
To split the linked list, traverse the original linked list and move all odd nodes to a separate
linked list of all odd nodes. At the end of the loop, the original list will have all the even nodes
and the odd node list will have all the odd nodes. To keep the ordering of all nodes the same, we
must insert all the odd nodes at the end of the odd node list.
Solution: A.
Problem-46 Find modular node: Given a singly linked list, write a function to find the last
element from the beginning whose n%k == 0, where n is the number of elements in the list
and k is an integer constant. For example, if n = 19 and k = 3 then we should return 18th
node.
Solution: For this problem the value of n is not known in advance.
Time Complexity: O(n). Space Complexity: O(1).
Problem-47 Find modular node from the end: Given a singly linked list, write a function to
find the first from the end whose n%k == 0, where n is the number of elements in the list
and k is an integer constant. If n = 19 and k = 3 then we should return 16th node.
Solution: For this problem the value of n is not known in advance and it is the same as finding the
kth element from the end of the the linked list.
Time Complexity: O(n). Space Complexity: O(1).
Problem-48 Find fractional node: Given a singly linked list, write a function to find the
element, where n is the number of elements in the list.
Problem-49 Find node: Given a singly linked list, write a function to find the
element, where n is the number of elements in the list. Assume the value of n is not known
in advance.
Solution: For this problem the value of n is not known in advance.
Time Complexity: O(n). Space Complexity: O(1).
Problem-50 Given two lists List 1 = {A1, A2, . . . , An) and List2 = {B1, B2, . . . , Bm} with
data (both lists) in ascending order. Merge them into the third list in ascending order so
that the merged list will be:
Solution:
Time Complexity: The while loop takes O(min(n,m)) time as it will run for min(n,m) times. The
other steps run in O(1). Therefore the total time complexity is O(min(n,m)). Space Complexity:
O(1).
Problem-51 Median in an infinite series of integers
Solution: Median is the middle number in a sorted list of numbers (if we have an odd number of
elements). If we have an even number of elements, the median is the average of two middle
numbers in a sorted list of numbers. We can solve this problem with linked lists (with both sorted
and unsorted linked lists).
First, let us try with an unsorted linked list. In an unsorted linked list, we can insert the element
either at the head or at the tail. The disadvantage with this approach is that finding the median
takes O(n). Also, the insertion operation takes O(1).
Now, let us try with a sorted linked list. We can find the median in O(1) time if we keep track of
the middle elements. Insertion to a particular location is also O(1) in any linked list. But, finding
the right location to insert is not O(logn) as in a sorted array, it is instead O(n) because we can’t
perform binary search in a linked list even if it is sorted. So, using a sorted linked list isn’t worth
the effort as insertion is O(n) and finding median is O(1), the same as the sorted array. In the
sorted array the insertion is linear due to shifting, but here it’s linear because we can’t do a binary
search in a linked list.
Note: For an efficient algorithm refer to the Priority Queues and Heaps chapter.
Problem-52 Given a linked list, how do you modify it such that all the even numbers appear
before all the odd numbers in the modified linked list?
Solution:
Time Complexity: O(n). Space Complexity: O(1).
Problem-53 Given two linked lists, each list node with one integer digit, add these two
linked lists. The result should be stored in the third linked list. Also note that the head node
contains the most significant digit of the number.
Solution: Since the integer addition starts from the least significant digit, we first need to visit the
last node of both lists and add them up, create a new node to store the result, take care of the carry
if any, and link the resulting node to the node which will be added to the second least significant
node and continue.
First of all, we need to take into account the difference in the number of digits in the two numbers.
So before starting recursion, we need to do some calculation and move the longer list pointer to
the appropriate place so that we need the last node of both lists at the same time. The other thing
we need to take care of is carry. If two digits add up to more than 10, we need to forward the
carry to the next node and add it. If the most significant digit addition results in a carry, we need
to create an extra node to store the carry.
The function below is actually a wrapper function which does all the housekeeping like
calculating lengths of lists, calling recursive implementation, creating an extra node for the carry
in the most significant digit, and adding any remaining nodes left in the longer list.
Time Complexity: O(max(List1 length,List2 length)).
Space Complexity: O(min(List1 length, List1 length)) for recursive stack.
Time complexity O(m + n), where m is the lengh of list1 and n is the length of list2. Space
Complexity: O(1).