Linked Lists
A linked list is a linear data structure where elements (nodes) are stored in non-contiguous memory and connected via pointers. Unlike arrays, linked lists excel at O(1) insertion and deletion at known positions but sacrifice O(1) random access.
Linked List Structure
A linked list consists of nodes, where each node contains: 1. Data — The value stored in the node 2. Next pointer — A reference to the next node in the list
The list is accessed through a head pointer that points to the first node. The last node's next pointer is null (None), marking the end.
Types of linked lists: - Singly Linked List — Each node has one pointer (next) - Doubly Linked List — Each node has two pointers (next and prev) - Circular Linked List — The last node points back to the head
Singly vs Doubly Linked Lists
Choosing between singly and doubly linked lists involves tradeoffs:
- Singly Linked List — Less memory (one pointer per node). Can only traverse forward. Deletion requires knowing the previous node.
- Doubly Linked List — More memory (two pointers per node). Can traverse both directions. O(1) deletion given a node reference. Used in LRU Cache, browser history.
- Circular Linked List — Last node points to head. Useful for round-robin scheduling, circular buffers.
- Python's collections.deque is implemented as a doubly linked list, providing O(1) append and popleft.
Implementing a Doubly Linked List
A complete doubly linked list implementation with all core operations:
class Node:
def __init__(self, data):
self.data = data
self.next = None
self.prev = None
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
self._size = 0
def __len__(self):
return self._size
def append(self, data):
"""Add to end - O(1)"""
node = Node(data)
if self.tail is None:
self.head = self.tail = node
else:
node.prev = self.tail
self.tail.next = node
self.tail = node
self._size += 1
def prepend(self, data):
"""Add to front - O(1)"""
node = Node(data)
if self.head is None:
self.head = self.tail = node
else:
node.next = self.head
self.head.prev = node
self.head = node
self._size += 1
def delete(self, node):
"""Delete a given node - O(1) given the node"""
if node.prev:
node.prev.next = node.next
else:
self.head = node.next # Deleting head
if node.next:
node.next.prev = node.prev
else:
self.tail = node.prev # Deleting tail
self._size -= 1
def find(self, data):
"""Search for a value - O(n)"""
current = self.head
while current:
if current.data == data:
return current
current = current.next
return None
def __repr__(self):
result = []
current = self.head
while current:
result.append(str(current.data))
current = current.next
return " <-> ".join(result)
# Test
dll = DoublyLinkedList()
for val in [1, 2, 3, 4, 5]:
dll.append(val)
print(dll) # 1 <-> 2 <-> 3 <-> 4 <-> 5
node = dll.find(3)
dll.delete(node)
print(dll) # 1 <-> 2 <-> 4 <-> 5Classic Linked List Problems
Key algorithmic patterns for linked lists:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# ---- Reverse a Linked List - O(n) time, O(1) space ----
def reverse_list(head):
prev = None
current = head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
return prev # New head
# ---- Detect Cycle (Floyd's Algorithm) ----
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# ---- Find Middle Node ----
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow is at middle
# ---- Merge Two Sorted Lists ----
def merge_sorted(l1, l2):
dummy = ListNode(0)
current = dummy
while l1 and l2:
if l1.val <= l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
current.next = l1 or l2
return dummy.nextKey Takeaways
- Linked lists store nodes in non-contiguous memory connected by pointers — no random access.
- Insertion and deletion at a known position are O(1); searching is O(n).
- Doubly linked lists allow O(1) deletion given a node reference — used in LRU Cache implementations.
- Floyd's cycle detection (slow/fast pointers) detects cycles in O(n) time with O(1) space.
- Use linked lists when frequent insertion/deletion is needed; use arrays for frequent random access.
Contact Us
Have a question or feedback? Fill out the form below or reach us directly at support@nvaitraining.com