Introduction TO Linked Lists
Introduction TO Linked Lists
A linked list is a linear data structure consisting of nodes where each node contains data and a
pointer to the next node in the list. There are two kinds of linked lists:
A singly linked list can only be traversed in one direction, meaning, from the first node (head)
towards the last node. The last node points to NULL, indicating the end of the list.
Pros
Insertion and deletion at head is more efficient compared to the corresponding operations in
an array.
Changes are localized when inserting and deleting somewhere in the middle of the linked
list. In the case of an array, all subsequent elements need to be moved around. Of course, if
you first have to search for the position to insert or delete in a linked list, then it still
takes O(n) in the worst case.
Cons
As each node also contains a pointer, it has a memory overhead.
We cannot access a node through indexing directly, rather we have to traverse from the start
to access an element at a given position n.
Reverse traversing is not possible with a singly linked list.
Doubly linked lists have all the advantages of singly-linked lists while providing some additional pros
and cons.
Pros
Insert and delete at the head as well as tail are O(1) operations
The list can be traversed forward as well as backward
Cons
Have greater space overhead than singly-linked lists
Singly vs. Doubly linked list table
Deletion at the tail is one of the operations that distinguishes a doubly-linked list from a singly-linked
list. The following table shows a side by side comparison of key operations on both.
In terms of space, if there are n nodes in a linked list, d is the (average) number of bytes of data in
each node and p is the number of bytes consumed by a pointer, then a singly linked
list consumes n*(d+p) bytes, whereas a doubly linked list consumes n*(d+2p) bytes.
Problem Statement
In the code snippet provided below, you will complete the void insertAtEnd(T data) method. This will
take a generic type T value called data and insert that value at the end of the list. This code would
be the part of the SinglyLinkedList class that we created in the previous lesson, therefore, any
private members of that class can be accessed inside the function as well. After writing the code, run
it and see how many tests you passed.
Method Prototype
void insertAtEnd(T data)
Output
The return type of the method is void. The object on which it is called will be modified only.
Sample Input
linkedlist = 0->1->2 data = 3
Sample Output
linkedlist = 0->1->2->3
Soution
If you grasped the logic behind insertion at the head of a linked list, this shouldn’t be much of a
challenge. If the list is empty, the situation is exactly like insertion at the head. Otherwise, we can
use a loop to reach the tail of the list and set our new node as the nextNode of the last node.
Code
public class SinglyLinkedList<T> {
//Node inner class for SLL
public class Node {
public T data;
public Node nextNode;
Time Complexity
This algorithm traverses the entire linked list, and hence, works in O(n) time.
The steps to search for a node in a Singly Linked List are as follows:
Method Prototype
boolean searchNode(T data)
Output
It returns true or false to show if a certain value does or does not exists in the list.
Sample Input
linkedlist = 5->90->10->4 and value = 4
Sample Output
true
Soution
In this function, we traverse through the list, checking whether the currentNode’s data matches the
searched value data. If the currentNode’s data matches the searched value data, we will
return True else we will return False.
Code
public class SinglyLinkedList<T> {
//Node inner class for SLL
public class Node {
public T data;
public Node nextNode;
//constructor
public SinglyLinkedList() {
headNode = null;
size = 0;
}
currentNode = currentNode.nextNode;
}
return false; //value not found
}
}
Time Complexity
The time complexity for this algorithm is O(n) because we have to traverse through the list
Problem Statement
In the code snippet given below, you have to implement the void deleteByValue(T data) method.
In this method, we will pass a particular value that we want to delete from the list. The node
containing this value could be anywhere on the list. It is also possible that such a node may not exist
at all. Therefore, we would have to traverse the whole list until we find the value which needs to be
deleted. If the value doesn’t exist, we simply return control to the calling function.
Note: This function removes only the first occurrence of the value from the list.
Method Prototype
void deleteByValue(T data)
Output
A linked list with the element removed.
Sample Input
linkedlist = 3->2->1->0, data = 1
Sample Output
linkedlist = 3->2->0
Soution
While traversing through the linked list, there are three cases we can come across:
List is empty
One element in the list
More than one element in the list
If the list is empty, then we have nothing to do; just return the control to calling function. For a single
element in the list, we’ll call deleteAtHead(), and this will delete the value at the head.
If there is more than one element in the list, then we simply start traversing from the headNode and
keep on checking until that node found and delete it.
Code
public class SinglyLinkedList<T> {
//Node inner class for SLL
public class Node {
public T data;
public Node nextNode;
currentNode = currentNode.nextNode;
}
return false; //value not found
}
if(currentNode.data.equals(data)) {
//data is at head so delete from head
deleteAtHead();
return;
}
//traverse the list searching for the data to delete
while (currentNode != null) {
//node to delete is found
if (data.equals(currentNode.data)){
prevNode.nextNode = currentNode.nextNode;
size--;
return;
}
prevNode = currentNode;
currentNode = currentNode.nextNode;
}
}
}
deleteByValue(): It takes a value as a parameter. Additionally, it searches for a node in the Singly
Linked List that has the given data value while keeping track of current and previous nodes. If not
found, it means no such value exists in the list. If found, it performs a series of steps to delete this
value. However, before jumping to those steps, look at some of the terminologies that we used in the
function:
currentNode – node we are currently standing at
prevNode – element that comes before currentNode
nextNode – pointer to the next node of currentNode
Executing the single line of code,
prevNode.nextNode = currentNode.nextNode
links the prevNode to the nextNode. The current node’s link will be destroyed from the list, and the
garbage collector will take care of the deletion automatically.
Time Complexity
In the worst case, you would have to traverse until the end of the list. This means the time
complexity will be O(n).
Memory Allocation
The main difference between a linked list and an array is the way they are allocated memory. Arrays
instantiate a whole block of memory, e.g., array[1000] gets space to store 1000 elements at the start
even if it doesn’t contain any element yet. On the other hand, a linked list only instantiates the
portion of memory it uses.
The table given below will summarize the performance difference between linked lists and arrays.
Method Prototype
int length()
Output
The length of the given linked list.
Sample Input
linkedlist = 0->1->2-3->4
Sample Output
length = 5
Solution
class CheckLength {
public static void main( String args[] ) {
SinglyLinkedList<String> list = new SinglyLinkedList<String>();
list.insertAtEnd("This");
list.insertAtEnd("list");
list.insertAtEnd("is");
list.insertAtEnd("Generic");
list.printList();
System.out.println("Length : " + list.length());
}
}
Explanation
The logic behind it is very similar to that of the search function. The trick is to iterate through the list
and keep count of how many nodes you’ve visited. This count is held in a variable that is returned
when the end of the list is reached.
Time Complexity
Since this is a linear algorithm, the time complexity will be O(n).
Method Prototype
public static void reverse(SinglyLinkedList list)
Output
A Singly linked list with all its content stored in reverse order.
Sample Input
linkedlist = 10->9->4->4-6
Sample Output
linkedlist = 6->4->4->9->10
Solution
public static void reverse(SinglyLinkedList list){
SinglyLinkedList.Node previous = null; //To keep track of the previous element, will be used in
swapping links
SinglyLinkedList.Node current = list.headNode; //firstElement
SinglyLinkedList.Node next = null;
Explanation
The brain of this solution lies in the loop, which iterates through the list. For any current node, its
link with the previous node is reverse and the next stores next node in the list:
Store the current node’s nextNode in next
Set current node’s nextNode to previous (reversal)
Make the current node the new previous so that it can be used for the next iteration
Use next to move on to the next node
In the end, we simply point the head to the last node in our loop.
Time Complexity
The algorithm runs in O(n) since the list is traversed once.
Method Prototype
public static boolean detectLoop(SinglyLinkedList list)
Output
It returns true if a loop exists in the linked list; otherwise, false.
Sample Input
linkedlist = 7->14->21->7
Sample Output
true
Solution
Explanation
This is the most optimized method to find out the loop in the LinkedList. We start traversing the
LinkedList using two pointers called slow and fast. Move slow by one (line # 9) and fast by two (line
# 10). If these pointers meet at the same node, then there is a loop. If these pointers do not meet,
then LinkedList doesn’t have a loop.
Time Complexity
The algorithm runs in Constant with O(n) with the Auxiliary Space complexity of O(1).
Method Prototype
public static Object findMiddle(SinglyLinkedList list)
Output
The value at the middle node in a linked list.
Sample Input
linkedlist = 7->14->10->21
Sample Output
mid = 14
Solution
public static Object findMiddle(SinglyLinkedList list) {
//if list is empty, then return null
if (list.isEmpty())
return null;
Explanation
We are using two pointers, which will work simultaneously. Think of it this way:
The current pointer moves two steps at a time till the end of the list
The mid pointer moves one step at a time
When the current pointer reaches the end, the mid pointer will be at the middle
Time Complexity
We are traversing the linked list at twice the speed, so it is certainly faster. However, the bottleneck
complexity is still O(n).
Remove Duplicates from a Linked List
Problem Statement
In this problem, you have to implement public static void removeDuplicates(SinglyLinkedList
list) method. This method will take a Singly linked list as input and remove all the elements that
appear more than once in the list. An illustration is also provided for your understanding.
Method Prototype
public static void removeDuplicates(SinglyLinkedList list)
Output
The linked list with all the duplicates removed.
Sample Input
linkedlist = 7->14->21->14->22->null
Sample Output
linkedlist = 7->14->21->22->null
Solution
public static void removeDuplicates(SinglyLinkedList list) {
SinglyLinkedList.Node current = list.headNode; // will be used for outer loop
SinglyLinkedList.Node compare = null; // will be used for inner loop
Explanation
This solution is simply a brute force solution. In this solution, we traverse the linked list using two
loops. The pointer current keeps track of the outer loop, and the pointer compare is used for the
inner loop. When both of these pointers point to the same value, then that node is deleted. This is
not the most efficient approach to solve this problem. It can also be solved using a Hash Table.
Time Complexity
This algorithm has two nested loops. Hence, the time complexity is O(n^2).
Note: The solution provided above is not the optimal solution for this problem. A more efficient
approach is to use hashing instead.
Method Prototype
public static Object nthElementFromEnd(SinglyLinkedList list, int n)
Output
It will return nth node from the end of the linked list.
Sample Input
linkedlist = 22->18->60->78->47->39->99 and n=3
Sample Output
47
Solution
public static Object nthElementFromEnd(SinglyLinkedList list, int n) {
int size = list.getSize();
n = size - n + 1; //we can use the size variable to calculate distance from start
if (size == 0 || n > size) {
return null; //returns null if list is empty or n is greater than size
}
SinglyLinkedList.Node current = list.getHeadNode();
int count = 1;
//traverse until count is not equal to n
while (current != null) {
if (count == n)
return current.data;
count++;
current = current.nextNode;
}
return null;
}
Explanation
In the above solution, we first use the getter function list.getSize() to access the length of the list.
Then we find the node which is at x position from the start.
Time Complexity
In the worst-case time complexity of this algorithm is O(n). Because we might have to access the 1st
element from the end.
class LinkedList {
Description
Given the head nodes of two linked lists that may or may not intersect, find out if they do in fact
intersect and return the point of intersection. Return null otherwise
Hints
Find the length of both linked lists.
The linked lists have to physically intersect. This means that their addresses need to be the
same. If two nodes have the same data but their addresses are not the same, the lists won’t
intersect and the function should return NULL.
Solution
The first solution that comes to mind is one with quadratic time complexity, i.e., for each node in the
first linked list, a linear scan must be done in the second linked list. If any node from the first linked
list is found in the second linked list (comparing addresses of nodes, not their data), that is the
intersection point. However, if none of the nodes from the first list is found in the second list, that
means there is no intersection point. Although this works, it is not efficient. A more efficient solution
would be to store the nodes of the first linked list in a HashSet and then go through the second
linked list nodes to check whether any of the nodes exist in the HashSet. This approach has a linear
runtime complexity and linear space complexity. We can use a much better, i.e., O(m + n), linear
time complexity algorithm that doesn’t require extra memory. To simplify our problem, let’s say both
the linked lists are of the same size. In this case, you can start from the heads of both lists and
compare their addresses. If these addresses match, it means it is an intersection point. If they don’t
match, move both pointers forward one step and compare their addresses. Repeat this process until
an intersection point is found, or both of the lists are exhausted. How do we solve this problem if the
lists are not of the same length? We can extend the linear time solution with one extra scan on the
linked lists to find their lengths. Below is the complete algorithm:
Find lengths of both linked lists: L1 and L2 Calculate the difference in length of both linked lists: d = |
L1 - L2| Move head pointer of longer list 'd' steps forward Now traverse both lists, comparing nodes
until we find a match or reach the end of lists
Code
class Intersect{
public static LinkedListNode intersect(LinkedListNode head1, LinkedListNode head2) {
int length_difference = 0;
if(list1length >= list2length) {
length_difference = list1length - list2length;
list1node = head1;
list2node = head2;
} else {
length_difference = list2length - list1length;
list1node = head2;
list2node = head1;
}
while(length_difference > 0) {
list1node = list1node.next;
length_difference--;
}
while(list1node != null) {
if(list1node == list2node) {
return list1node;
}
list1node = list1node.next;
list2node = list2node.next;
}
return null;
}
LinkedList.insertAtTail(list_head_1, node1);
LinkedList.insertAtTail(list_head_1, node2);
LinkedList.insertAtTail(list_head_2, node1);
System.out.print("List 1: ");
LinkedList.display(list_head_1);
System.out.print("List 2: ");
LinkedList.display(list_head_2);
Runtime complexity
The runtime complexity of this solution is linear, O(m + n) Where m is the length of the first linked list
and n is the length of the second linked list.
Memory complexity
The memory complexity of this solution is constant, O(1).
Description
Given a singly linked list and an integer ‘k’, reverse every ‘k’ element. If k <= 1, then the input list is
unchanged. If k >= n (n is the length of the linked list), then reverse the whole linked list.
Hints
Reverse the first k element, then move to the next k element.
Solution
Algorithmically, it is a simple problem, but writing code for this is a bit trickier as it involves keeping
track of a few pointers. Logically, we break down the list to sub-lists each of size ‘k’. We’ll use the
below pointers:
Code
class ReverseK{
static LinkedListNode reverseKNodes(LinkedListNode head, int k) {
int n = k;
while (head != null && n > 0) {
LinkedListNode temp = head.next;
head.next = currentHead;
currentHead = head;
head = temp;
n--;
}
if (reversed == null) {
reversed = currentHead;
}
if (prevTail != null) {
prevTail.next = currentHead;
}
prevTail = currentTail;
}
return reversed;
}
Runtime complexity
The runtime complexity of this solution is linear, O(n).
Memory complexity
The memory complexity of this solution is constant, O(1).
Example 1:
Input: L1=[2, 6, 8], L2=[3, 6, 7], L3=[1, 3, 4] Output: [1, 2, 3, 3, 4, 6, 6, 7, 8]
Example 2:
Input: L1=[5, 8, 9], L2=[1, 7] Output: [1, 5, 7, 8, 9]
Solution
A brute force solution could be to add all elements of the given ‘K’ lists to one list and sort it. If there
are a total of ‘N’ elements in all the input lists, then the brute force solution will have a time
complexity of O(NlogN)* as we will need to sort the merged list. Can we do better than this? How
can we utilize the fact that the input lists are individually sorted? If we have to find the smallest
element of all the input lists, we have to compare only the smallest (i.e. the first) element of all the
lists. Once we have the smallest element, we can put it in the merged list. Following a similar
pattern, we can then find the next smallest element of all the lists to add it to the merged list.
The best data structure that comes to mind to find the smallest number among a set of ‘K’ numbers
is a Heap. Let’s see how can we use a heap to find a better algorithm.
We can insert the first element of each array in a Min Heap.
After this, we can take out the smallest (top) element from the heap and add it to the merged
list.
After removing the smallest element from the heap, we can insert the next element of the
same list into the heap.
We can repeat steps 2 and 3 to populate the merged list in sorted order.
Code
import java.util.*;
class ListNode {
int value;
ListNode next;
ListNode(int value) {
this.value = value;
}
}
class MergeKSortedLists {
// take the smallest (top) element form the min-heap and add it to the result;
// if the top element has a next element add it to the heap
ListNode resultHead = null, resultTail = null;
while (!minHeap.isEmpty()) {
ListNode node = minHeap.poll();
if (resultHead == null) {
resultHead = resultTail = node;
} else {
resultTail.next = node;
resultTail = resultTail.next;
}
if (node.next != null)
minHeap.add(node.next);
}
return resultHead;
}
Time complexity
Since we’ll be going through all the elements of all arrays and will be removing/adding one element
to the heap in each step, the time complexity of the above algorithm will be O(N*logK), where ‘N’ is
the total number of elements in all the ‘K’ input arrays.
Space complexity
The space complexity will be O(K) because, at any time, our min-heap will be storing one number
from all the ‘K’ input arrays