Just like arrays, Linked Lists are also used to store a collection of same type of data. Data type can be as simple as a primtive integer to a complex class object. However the structure of these two data structures is very different.
What is a Linked List?
A linked list consists of nodes, that are linked together like a chain. Each node holds data, along with a pointer to the next node. It may be singly linked, meaning it can only be traversed in one direction, or it can be doubly linked, meaning it can be traversed in both directions.
Basic operations in a linked list are insertion, search and deletion.
Linked Lists can be used to implement hashmaps, file systems, adjacency lists etc, for dynamic memory allocation, to perform arithmetic operations on long integers, for maintaining a directory of names etc.
Array vs. Linked Lists
Arrays and Linked Lists differ in the way they are allocated memory, how elements are inserted or deleted and also how elements are searched.
Memory Allocation
Arrays instantiate a whole block of memory per element at once, even if it does not contain any elements yet. For example array[10], will reserve 10 memory spaces, as soon as it is declared. Linked Lists are more dynamic in nature and they only reseve the memory as and when the need arises.
Insertion or Deletion
When it comes to insertion or deletion, linked lists only take 1 unit of time, as we just need to change the pointers. For Arrays however, it takes n units of time as we have to shift each array element left or right after the operation.
Searching
While searching for an element, linked lists are slower, because we only have access to the head node. We need to traverse the whole linked list, element by element, looking for the element to be searched. For arrays, it just takes 1 unit of time as the indices for each value are known.
What is a Node?
A Node is a basic building block of a linked list. It stores the data along with a pointer to the next node (like a link in a chain). It can store data types ranging from primitive values to a class object.
Singly Linked List
Signly Linked List consists of a chain of nodes, each node holding a pointer to only the next node (and not the previous node). This means a singly linked list can only be traversed in one direction. To access the linked list, we only need pointer to the head node. Below is a simple implementation:
(is_empty() method is used to check if the list is an empty list)
Node Implementation for an SLL:
SLL Implementation
Insertion in an SLL
There are 3 ways to insert a value in a linked list:
Insert at Head
Insert at End
Insert After
Deleting in an SLL
There are 3 ways to delete a node from a linked list
Delete at Head
Delete at End
Delete by Value
Full SLL Implementation
Validating the SLL implementation
Output
Doubly Linked List
In a Singly Linked List, we saw that in order to perform basic operations like insert_after(), insert_at_end(), search_by_value(), delete_by_value(), delete_at_end(), we have to traverse the whole list, starting from head node. Also we need to keep track of the previous nodes while inserting or deleting nodes.
This is where Doubly Linked List comes to the rescue. A DLL is bidirectional, meaning each node stores the data (of course), a reference to the next node and ALSO, a reference to the previous node, making it easier to insert and delete nodes from the list.
Node Implementation for a DLL
DLL Implementation
Insertion in a DLL
There are 3 types of insertion in a DLL:
Insert at Head
Insert at End
Insert After
Deletion in a DLL
There are 3 types of deletion in a DLL:
Delete at Head
Delete at End
Delete by Value
Full DLL Implementation
Validating the DLL Implementation
Output
Linked List with Tail
In this version of Linked Lists, the last element is referred to as tail, in addition to the first element referred to as head. Both SLL and DLL can be implemeted with a tail pointer.
Having a tail pointer, makes it signicantly easy to add or delete elements from end of the linked list.
In terms of time complexity:
Insertion at end in an SLL with tail will only take constant time - O(1) as compared to linear time - O(n) in a simple SLL.
Similarly, insertion at end in a DLL with tail will also only take constant time - O(1) as compared to linear time - O(n) in a simple DLL.
When it comes to deletion at end, and SLL with tail will still take linear time - O(n) because we need to traverse the whole list to keep track of the previous element of the last node.
However for a DLL with tail, we don’t need to traverse the whole list, as the last node (tail) already has a pointer to the previous node of the last node, so it takes constant time - O(1)
Meaning, we see real advantage of using a tail pointer in the case of delete_at_end() for DLLs.
Implementing DLL with Tail
Validating DLL with Tail
Output
Challenges
Challenges 4-10
In the challenges below, we use a singly linked list to solve the problem.
Challenge 4 : Find the Length of a Linked List
Set variable current to the head node of the linked list.
Iterate the linked list until the current element is not None.
Increment a counter with each pass, final value of counter will be the length of the linked list.
Output
Challenge 5 : Reverse a Linked List
In-place link swaps
Assume 3 variables prev_node, current and next_node to keep track of nodes while swapping the links.
Set prev_node and next_node to None, current to linked list head.
Now iterate the linked-list while current is not None.
set next_node to current.next.
set current.next to prev_node.
set prev_node to current.
set current to next_node.
After all nodes are iterated, all links are reversed.
Now, current is None and prev_node contains first element of the linked list after reversal.
Finally, set the head variable to prev_node and list is reversed.
Output
Challenge 6 : Detect Loop in a Linked List
The most optimum solution to detect loop in a linked list is outlined in Floyd’s Cycle Detection Algorithm.
Using Floyd’s Cycle Detection Algorithm:
Set 2 pointers, slow and fast and set them to head of the linked list.
Slow pointer jumps 1 step from current element to next element.
Fast pointer jumps 2 steps from current element to bext of next element.
Iterate the linked list, until slow or fast or fast.next is None.
Eventually slow and fast pointers will either point to the same node if there is a loop, else there is NO loop.
Output
Challenge 7: Find the Middle Value of a Linked List
This is very similar to Floyd’s Cycle Detection Algorithm above
Set 2 variables, mid and current to the linked list head.
For every single jump of mid variable to the next node, current node jumps twice the next node’s next node.
Iterate the list, until either mid, current or current.next is None.
When current = None means current traversed whole linked list.
Since mid variable was only traversing at half speed as compared to current, mid variable now contains the middle node.
Output
Challenge 8 : Remove Duplicates from a Linked List
Solution 1: Nested Loop
We traverse the linked list in a nested fashion.
For each node, we traverse the whole linked list deleting the nodes which match the outer node.
While variable current is not None, set variable compare to current.
While compare.next is not None, check if the outer current node’s value is the same as compare.next’s value.
If yes, then set compare.next to compare.next’s next node (meaning delete compare.next), else set compare to compare.next.
Once inner loop concludes, restart the process by setting current to current’s next node - back to step 3. (the original list is already altered as we removed the duplicate during the inner loop).
Finally, return the linked list with unique elements.
Output
Solution 2: Using Hash Set
Hash sets have the property that it cannot contain any duplicates. Meaning if we can add all linked list elements to a set and recontruct our list from the set, we would eliminate all duplicates.
set prev and current variables to linked list head.
create a new set to store elements.
check if current node’s value already exists in the set.
If yes, then set prev.next to current.next (skipping current element -> duplicate) and current = current.next.
Else add current’s value to set, set prev to current and current = current.next.
Iterate the loop until current is not None.
Finally, the linked list only contains unique elements.
Output
Challenge 9 : Union & Intersection of Lists
Union of 2 linked list means a linked list with unique elements from both linked lists.
Intersection of 2 linked lists means a linked only containing only the common elements from both linked lists.
For Union:
Write a method to delete duplicate elements froma linked list (as above - using sets).
Iterate list 1 and get to the last node in the list.
Now point last node’s next to 2nd linked list’s head, linking both lists into a single list.
Now remove duplicates from this single list using the method written earlier.
Union linked list is ready.
For Intersection:
Write a method to check if a linked list contains an element (check the element against the whole list).
Set current variable to linked list 1’s head.
Iterate linked list 1 and check if the same element also exists in the linked list 2.
If yes, add the element to the output linked list (at the end).
Move to the next iteration and repeat the check in step 3.
Finally output linked list will contain the intersection list.
Output
Challenge 10 : Return the Nth node from End
This solution uses a simple logic. Nth node from the end means (len(linked list) - n + 1)th node from the beginning.
Compute the size of the linked list (as in challenge 4).
Compute m = size - n + 1
Iterate the linked list, until the count = m.
Return node’s value at count = m
Output
project showcase + professional profile | Read the fine print. README