Beyond the Single Direction
[!NOTE] This module explores the core principles of Doubly & Circular Lists, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
1. Concept Definition
Singly Linked Lists are efficient but frustrating: you can’t look back. If you are at Node 5 and need something from Node 4, you have to start over from the Head.
Enter the Doubly Linked List (DLL).
Each node now carries two pointers:
- Next: Points forward.
- Prev: Points backward.
A Circular Linked List is a variation where the Tail points back to the Head instead of null, creating an infinite loop.
2. Interactive Analysis
Experience bidirectional traversal. Toggle “Circular Mode” to connect Tail to Head.
3. Intuition
Memory Trade-off
- Singly: 4 bytes (int) + 8 bytes (next) = 12 bytes per node.
- Doubly: 4 bytes (int) + 8 bytes (next) + 8 bytes (prev) = 20 bytes per node.
- Cost: ~66% more memory overhead per node.
- Benefit: O(1) deletion if you have the node (no need to search for the previous node).
The Superpower: O(1) Deletion
In a Singly Linked List, to delete Node B, you need a pointer to Node A (its predecessor). This requires scanning from the Head (O(N)).
In a Doubly Linked List, B knows about A via B.prev.
We can delete B in O(1) if we have a reference to it.
// O(1) Deletion
node.prev.next = node.next;
node.next.prev = node.prev;
[!WARNING] Always check for
null. If you delete the Head or Tail,prevornextmight not exist.
4. Implementation
class DoublyNode {
int val;
DoublyNode next;
DoublyNode prev;
public DoublyNode(int val) {
this.val = val;
}
}
class DoublyLinkedList {
DoublyNode head;
DoublyNode tail;
// Add to Head
public void addFirst(int val) {
DoublyNode newNode = new DoublyNode(val);
if (head == null) {
head = tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
// Delete Node (O(1) given reference)
public void remove(DoublyNode node) {
if (node.prev != null) node.prev.next = node.next;
else head = node.next; // Removing head
if (node.next != null) node.next.prev = node.prev;
else tail = node.prev; // Removing tail
}
}
type DoublyNode struct {
Val int
Next *DoublyNode
Prev *DoublyNode
}
type DoublyLinkedList struct {
Head *DoublyNode
Tail *DoublyNode
}
// Add to Head
func (dll *DoublyLinkedList) AddFirst(val int) {
newNode := &DoublyNode{Val: val}
if dll.Head == nil {
dll.Head = newNode
dll.Tail = newNode
} else {
newNode.Next = dll.Head
dll.Head.Prev = newNode
dll.Head = newNode
}
}
// Delete Node (O(1) given reference)
func (dll *DoublyLinkedList) Remove(node *DoublyNode) {
if node.Prev != nil {
node.Prev.Next = node.Next
} else {
dll.Head = node.Next
}
if node.Next != nil {
node.Next.Prev = node.Prev
} else {
dll.Tail = node.Prev
}
}
5. Complexity Analysis
| Operation | Time | Space | Notes |
|---|---|---|---|
| Search | O(N) | O(1) | Must scan. Can search from Head or Tail. |
| Insert (Head/Tail) | O(1) | O(1) | Pointer swaps only. |
| Delete (Given Node) | O(1) | O(1) | No traversal needed. |
| Delete (By Value) | O(N) | O(1) | Must search first. |