Linked List
Linked List
1 Introduction
This characteristic allows linked lists to be dynamic, meaning they can grow or shrink in size
without needing to be resized, unlike arrays. Linked lists are efficient when performing
operations like insertion or deletion since these can be done without shifting other elements.
1. Singly Linked List: Each node points to the next node only. It has a one-way
connection.
2. Doubly Linked List: Each node has two pointers: one pointing to the next node and
one pointing to the previous node. This allows traversal in both directions.
3. Circular Linked List: The last node points back to the first node, making the list
circular.
Imagine you have a list of three numbers: [10, 20, 30]. The corresponding linked list would
look like this:
Here:
The first node contains the number 10 and points to the next node (20).
The second node contains 20 and points to the next node (30).
The third node contains 30 and points to NULL, indicating the end of the list.
Diagram of a Singly Linked List:
Head
|
v
[10 | Next] -> [20 | Next] -> [30 | NULL]
In this example:
Memory allocation in a linked list is dynamic, meaning that each node is allocated memory as
needed. Unlike arrays, where the size is fixed at the time of creation, a linked list can grow or
shrink without needing to know the size in advance.
In a linked list:
1. Each node is allocated memory separately. When a new node is created, memory is
reserved for the data and the pointer to the next node.
2. No contiguous memory is required. Since each node contains a reference to the next
node, the nodes can be scattered in memory.
This allows for efficient insertion and deletion of nodes because only the pointers need to be
updated, rather than moving large blocks of memory.
Example:
1. Node 1 (Data: 10): The system allocates memory for this node, stores 10 in it, and the
next pointer will point to the second node.
2. Node 2 (Data: 20): The system allocates a separate block of memory for this node,
stores 20, and its next pointer will point to the third node.
3. Node 3 (Data: 30): The system allocates memory for this node and stores 30, with its
next pointer set to NULL.
The nodes are stored in different parts of memory, and their links point to each other, creating
the linked list.
1. Linked List: A sequence of nodes, where each node points to the next.
2. Node: A unit of the linked list containing data and a pointer.
3. Memory Allocation: Each node is allocated memory separately, and no contiguous
block of memory is required.
This dynamic nature of linked lists makes them efficient in situations where frequent
insertion and deletion of elements are required.
A linked list is a type of data structure that organizes data in a sequence where each element
(node) points to the next one. There are several types of linked lists:
We'll discuss each type in detail, along with the operations that can be performed on them.
A Singly Linked List (SLL) is a type of linked list where each node contains two parts:
In a singly linked list, traversal is only possible in one direction — from the first node (head)
to the last node.
Example:
Consider a singly linked list with three nodes containing the values 10, 20, and 30:
Head
|
v
[10 | Next] -> [20 | Next] -> [30 | NULL]
Here:
The most common operations that can be performed on a singly linked list are:
Example Operations:
Insert at the Beginning: Adding a new node with value 5 at the start.
Before:
Head -> [10 | Next] -> [20 | Next] -> [30 | NULL]
Head -> [5 | Next] -> [10 | Next] -> [20 | Next] -> [30 | NULL]
class Node:
"""Represents a node in a singly linked list."""
def __init__(self, data=None):
self.data = data
self.next = None
class SinglyLinkedList:
"""Implements a singly linked list."""
def __init__(self):
self.start = None
def is_empty(self):
"""Checks if the linked list is empty."""
return self.start is None
def delete_start(self):
"""Deletes the first node in the linked list."""
if self.is_empty():
print("List is empty, nothing to delete.")
return
self.start = self.start.next
def delete_end(self):
"""Deletes the last node in the linked list."""
if self.is_empty():
print("List is empty, nothing to delete.")
return
if self.start.next is None:
self.start = None
return
current = self.start
while current.next.next:
current = current.next
current.next = None
if current.next is None:
print(f"Node with data {key} not found.")
else:
current.next = current.next.next
def display(self):
"""Displays all nodes in the linked list."""
if self.is_empty():
print("List is empty.")
return
current = self.start
while current:
print(current.data, end=" -> ")
current = current.next
print("None")
# Example Usage
if __name__ == "__main__":
sll = SinglyLinkedList()
sll.insert_end(10)
sll.insert_end(20)
sll.insert_start(5)
sll.display() # Output: 5 -> 10 -> 20 -> None
sll.insert_after(10, 15)
sll.display() # Output: 5 -> 10 -> 15 -> 20 -> None
sll.delete_node(15)
sll.display() # Output: 5 -> 10 -> 20 -> None
A Circular Linked List is a type of linked list where the last node’s next pointer points back
to the first node instead of NULL. This creates a circle-like structure. Circular linked lists can
be of two types:
1. Singly Circular Linked List: Each node points to the next, and the last node points
back to the first.
2. Doubly Circular Linked List: Each node has two pointers (next and previous), and
the last node’s next pointer points to the first node, while the first node’s previous
pointer points to the last node.
Problem:
In a traditional singly linked list, traversing the list becomes slow and inefficient when
dealing with operations that require continuous looping through the list. Additionally,
managing both the head and tail pointers consumes extra memory and adds complexity.
1. Continuous Traversal:
o In a circular linked list, the last node points to the first node, forming a loop.
This allows for continuous traversal without needing to check for the end
(None), making it perfect for applications like round-robin scheduling or
cyclic buffers.
2. Memory Optimization:
o Instead of having both head and tail pointers, only a single pointer to the
last node is needed to manage the entire list. This reduces memory usage and
simplifies management.
3. Simplified Operations:
o Insertion and deletion operations become easier because you don’t need to
handle special cases for the end of the list. For example, the last.next
always points to the first node, so adding or removing nodes is
straightforward.
Example
Imagine a circular list with nodes 10 -> 20 -> 30 -> (back to 10).
Traversal: You can start at any node and loop through the entire list without
worrying about reaching the end.
Insertion: Adding a node at the end simply requires linking it to the first node,
making the operation simple and quick.
Memory: Only the last node needs to be tracked, making it more efficient than a
traditional list where both head and tail are needed.
Key Benefits:
Faster and Simpler Traversal: No need to check for the end of the list.
Reduced Memory Usage: Only one pointer (last) is needed to manage the list.
Efficient Operations: Easier to add/remove nodes, especially at both ends.
Thus, a circular linked list offers a more efficient solution for continuous and cyclic data
processing.
Consider a circular linked list with three nodes containing values 10, 20, and 30:
Head
|
v
[10 | Next] -> [20 | Next] -> [30 | Next] -> (Back to Head)
In this list, after the last node (30), the next pointer of node 30 points back to the first
node (10), completing the circle.
Circular linked lists support operations similar to singly linked lists, but there are some
differences due to the circular nature. The common operations are:
1. Insertion:
o At the beginning: Add a new node before the head.
o At the end: Add a new node and link it to the head to complete the circle.
o After a specific node: Insert after a given node.
2. Deletion:
o At the beginning: Remove the head node and update the next pointer to the
next node.
o At the end: Remove the last node and adjust the pointer of the second-to-last
node to the head.
3. Traversal: Traversal can be a little tricky because you need to ensure you don’t loop
infinitely. The traversal will stop once you encounter the head node again.
4. class Node:
5. """Represents a node in a circular singly linked list."""
6. def __init__(self, data=None):
7. self.data = data
8. self.next = None
9.
10. class CircularLinkedList:
11. """Implements a singly circular linked list using only the 'last'
pointer."""
12.
13. def __init__(self):
14. self.last = None # Only 'last' pointer is needed
15.
16. def is_empty(self):
17. """Checks if the circular linked list is empty."""
18. return self.last is None
19.
20. def insert_start(self, data):
21. """Inserts a new node at the beginning of the list."""
22. new_node = Node(data)
23. if self.is_empty():
24. self.last = new_node
25. new_node.next = self.last # Point to itself
26. else:
27. new_node.next = self.last.next # New node points to
current start
28. self.last.next = new_node # Last node points to new node
29.
30. def insert_end(self, data):
31. """Inserts a new node at the end of the list."""
32. new_node = Node(data)
33. if self.is_empty():
34. self.last = new_node
35. new_node.next = self.last # Point to itself
36. else:
37. new_node.next = self.last.next # New node points to the
first node
38. self.last.next = new_node # Last node points to the new
node
39. self.last = new_node # Update the last pointer to new node
40.
41. def insert_after(self, target, data):
42. """Inserts a new node after the node with the given target
data."""
43. if self.is_empty():
44. print("List is empty, cannot insert after.")
45. return
46. current = self.last.next # Start from the first node
47. while current != self.last:
48. if current.data == target:
49. new_node = Node(data)
50. new_node.next = current.next
51. current.next = new_node
52. if current == self.last: # If target is the last node,
update the last pointer
53. self.last = new_node
54. return
55. current = current.next
56. # Check if the target is the last node
57. if current.data == target:
58. new_node = Node(data)
59. new_node.next = self.last.next
60. current.next = new_node
61. self.last = new_node # Update the last pointer
62.
63. def delete_start(self):
64. """Deletes the first node in the circular linked list."""
65. if self.is_empty():
66. print("List is empty, nothing to delete.")
67. return
68. if self.last.next == self.last: # Only one node in the list
69. self.last = None # List becomes empty
70. else:
71. self.last.next = self.last.next.next # Point last to
second node
72.
73. def delete_end(self):
74. """Deletes the last node in the circular linked list."""
75. if self.is_empty():
76. print("List is empty, nothing to delete.")
77. return
78. if self.last.next == self.last: # Only one node in the list
79. self.last = None # List becomes empty
80. else:
81. current = self.last.next
82. while current.next != self.last: # Find the second-to-last
node
83. current = current.next
84. current.next = self.last.next # Update the second-to-last
node to point to first node
85. self.last = current # Update last pointer to second-to-
last node
86.
87. def delete_node(self, target):
88. """Deletes the first node with the specified target data."""
89. if self.is_empty():
90. print("List is empty, nothing to delete.")
91. return
92. current = self.last.next
93. if current.data == target: # Target is the first node
94. self.delete_start()
95. return
96. while current.next != self.last and current.next.data !=
target:
97. current = current.next
98. if current.next.data == target: # Node to delete found
99. current.next = current.next.next # Remove the node
100. if current.next == self.last: # If last node is
deleted, update last pointer
101. self.last = current
102. else:
103. print(f"Node with data {target} not found.")
104.
105. def display(self):
106. """Displays all nodes in the circular linked list."""
107. if self.is_empty():
108. print("List is empty.")
109. return
110. current = self.last.next # Start from the first node
111. while current != self.last:
112. print(current.data, end=" -> ")
113. current = current.next
114. print(current.data, end=" -> ") # Print the last node
115. print("Back to start") # Indicate circular nature
116.
117. # Example Usage
118. if __name__ == "__main__":
119. # Create a circular linked list and perform operations
120. cll = CircularLinkedList()
121.
122. # Insert nodes
123. cll.insert_end(10)
124. cll.insert_end(20)
125. cll.insert_start(5)
126. cll.insert_end(30)
127. cll.display() # Output: 5 -> 10 -> 20 -> 30 -> Back to start
128.
129. # Insert a node after a specific node
130. cll.insert_after(10, 15)
131. cll.display() # Output: 5 -> 10 -> 15 -> 20 -> 30 -> Back to
start
132.
133. # Delete the first node
134. cll.delete_start()
135. cll.display() # Output: 10 -> 15 -> 20 -> 30 -> Back to
start
136.
137. # Delete the last node
138. cll.delete_end()
139. cll.display() # Output: 10 -> 15 -> 20 -> Back to start
140.
141. # Delete a specific node (node with data 20)
142. cll.delete_node(20)
143. cll.display() # Output: 10 -> 15 -> Back to start
144.
145. # Attempt to delete a non-existing node
146. cll.delete_node(50) # Output: Node with data 50 not found.
147.
A Doubly Linked List (DLL) is a type of linked list where each node contains three parts:
In a doubly linked list, you can traverse both forward and backward, as each node points to
both the next and the previous node.
Example:
Consider a doubly linked list with three nodes containing values 10, 20, and 30:
NULL <- [10 | Prev | Next] <-> [20 | Prev | Next] <-> [30 | Prev | Next] ->
NULL
The first node’s prev pointer is NULL because it has no previous node.
The last node’s next pointer is NULL because it has no next node.
Each node is connected to both its previous and next node, allowing for two-way
traversal.
In a Doubly Linked List, the operations are similar to those in a singly linked list but with
the added flexibility of two pointers per node. The common operations are:
1. Insertion:
o At the beginning: Insert before the first node. Update the head pointer.
o At the end: Insert after the last node.
o After a specific node: Insert after a given node.
o Before a specific node: Insert before a given node.
2. Deletion:
o At the beginning: Remove the first node and update the head.
o At the end: Remove the last node and update the last pointer.
o Specific node: Remove a node by value or position.
3. Traversal:
o Forward Traversal: Start from the head and go to the next node using the
next pointer.
o Backward Traversal: Start from the tail (last node) and go backward using
the previous pointer.
Example Operations:
Insert at the Beginning: Adding a new node with value 5 at the start.
Before:
NULL <- [10 | Prev | Next] <-> [20 | Prev | Next] <-> [30 | Prev | Next] ->
NULL
1. Singly Linked List: One pointer per node (next). Efficient for simple operations but
only one-way traversal.
2. Circular Linked List: The last node points to the first, forming a circle. It can be
singly or doubly circular.
3. Doubly Linked List: Two pointers per node (next and previous). Allows traversal in
both directions.
Singly Linked Linear list where each node Simple, space- Can only traverse in one
List points to the next node. efficient. direction.
Circular Last node points to the first Efficient for circular Complexity increases for
Linked List node, forming a circle. traversal. deletion.
Doubly Nodes have both prev and Can traverse in both More memory usage due to
Linked List next pointers. directions. extra pointer.
Head -> [10 | Next] -> [20 | Next] -> [30 | NULL]
Head -> [10 | Next] -> [20 | Next] -> [30 | Next] -> (Back to Head)
NULL <- [10 | Prev | Next] <-> [20 | Prev | Next] <-> [30 | Prev | Next] ->
NULL