Historical Context The linked list was first described by Hans Peter Luhn at IBM (1953). The concept appeared earlier in Konrad Zuse's Plankalkül (1948). Stacks and queues were formalised by Dijkstra in his 1960 ALGOL 60 paper. The AVL tree was invented by Adelson-Velsky and Landis (1962). These structures underpin every standard library. Understanding them at the implementation level separates systems programmers from application programmers.## Intuition
Building blocks of all data structures: Linked lists, stacks, and queues are like the atoms of data structures — almost every complex data structure is built from these primitives. Linked lists give dynamic sizing, stacks enforce LIFO order, and queues enforce FIFO order.
Why it matters: Understanding these fundamentals lets you implement more complex data structures from scratch and choose the right abstraction for the job. Stacks handle undo/redo, queues handle task scheduling, and linked lists handle dynamic collections.
The key insight: Linked lists trade space for flexibility — each node wastes pointer space but gains O(1) insertion/deletion. Arrays trade flexibility for cache efficiency — contiguous memory enables O(1) access but O(n) insertion.
A singly linked list is a sequence of nodes where each node contains a value and a reference to the Next node. The list is accessed through a head pointer; traversal requires following pointers from The head.
def __init__ ( self , val = 0 , next = None ):
def insert_at_head ( head , val ):
"""Insert at the head of the list. O(1)."""
return ListNode(val, head)
def insert_at_tail ( head , val ):
"""Insert at the tail. O(n) without tail pointer, O(1) with tail pointer."""
def delete_node ( head , val ):
"""Delete the first node with the given value. O(n)."""
if current.next.val == val:
current.next = current.next.next
Position (given a pointer to the preceding node). The critical disadvantage is $O(n)$ random access And poor cache locality. In practice, arrays dominate because cache effects matter more than Theoretical complexity for typical data sizes.def reverse_linked_list ( head ):
Reverse a singly linked list iteratively.
def reverse_recursive ( head ):
Reverse a singly linked list recursively.
Time: O(n), Space: O(n) — recursion stack
if head is None or head.next is None :
new_head = reverse_recursive(head.next)
Floyd’s algorithm uses two pointers moving at different speeds. If there is a cycle, the fast Pointer will eventually catch up to the slow pointer.
Detect if a linked list has a cycle.
while fast and fast.next:
def find_cycle_start ( head ):
Find the node where the cycle begins.
while fast and fast.next:
# Reset slow to head, move both at speed 1
Why this works: Let μ \mu μ be the distance from head to cycle start, and λ \lambda λ be the cycle Length. When slow and fast meet, slow has travelled μ + a λ \mu + a\lambda μ + aλ steps and fast has travelled μ + a λ + b λ \mu + a\lambda + b\lambda μ + aλ + bλ steps for some non-negative integers a , b a, b a , b . Since fast moves twice as Fast: 2 ( μ + a λ ) = μ + a λ + b λ 2(\mu + a\lambda) = \mu + a\lambda + b\lambda 2 ( μ + aλ ) = μ + aλ + bλ Which gives μ = ( b − a ) λ \mu = (b - a)\lambda μ = ( b − a ) λ . So the Distance from the meeting point to the cycle start (going around the cycle) is exactly μ \mu μ — the Same as the distance from head to cycle start.
Beyond cycle detection, the fast/slow pointer pattern is useful for:
Find the middle node of a linked list.
If even length, returns the second middle node.
while fast and fast.next:
Check if a linked list is a palindrome.
if not head or not head.next:
while fast.next and fast.next.next:
second_half = reverse_linked_list(slow.next)
if first.val != second.val:
# Restore (optional, but good practice)
reverse_linked_list(second_half)
Each node has both a next and prev pointer.
def __init__ ( self , val = 0 , prev = None , next = None ):
Doubly linked lists support O ( 1 ) O(1) O ( 1 ) deletion given a pointer to the node (no need to find the Predecessor) and reverse traversal. The trade-off is extra memory per node (one pointer) and more Complex insertion/deletion logic.
def delete_doubly_node ( node ):
Delete a node from a doubly linked list given the node itself.
node.prev.next = node.next
node.next.prev = node.prev
A skip list is a probabilistic data structure that provides O ( log n ) O(\log n) O ( log n ) expected-time search, Insertion, and deletion — the same asymptotic complexity as a balanced BST, but with simpler Implementation.
The structure consists of multiple levels of linked lists. The bottom level contains all elements. Each higher level is a “fast lane” that contains a random subset of elements from the level below.
def __init__ ( self , val , levels ):
self .forward = [ None ] * levels
Expected time: O(log n) for search, insert, delete
P = 0.5 # probability of promoting to next level
self .head = SkipListNode( float ( ' -inf ' ), self . MAX_LEVEL )
while random.random() < self .P and level < self . MAX_LEVEL :
def search ( self , target ):
for i in range ( self .level - 1 , - 1 , - 1 ):
while current.forward[i] and current.forward[i].val < target:
current = current.forward[i]
current = current.forward[ 0 ]
return current is not None and current.val == target
update = [ None ] * self . MAX_LEVEL
for i in range ( self .level - 1 , - 1 , - 1 ):
while current.forward[i] and current.forward[i].val < val:
current = current.forward[i]
new_level = self ._random_level()
if new_level > self .level:
for i in range ( self .level, new_level):
new_node = SkipListNode(val, new_level)
for i in range (new_level):
new_node.forward[i] = update[i].forward[i]
update[i].forward[i] = new_node
Kernel (for process address space management). They are preferred over balanced BSTs in these Contexts because they are simpler to implement correctly in concurrent settings — insertion and Deletion only need to lock the nodes being modified, not the entire structure.A stack is a LIFO (Last In, First Out) data structure with two primary operations: push (add to Top) and pop (remove from top).
Stack implemented with a dynamic array.
All operations: O(1) amortised
raise IndexError ( " pop from empty stack " )
raise IndexError ( " peek at empty stack " )
return len ( self .data) == 0
Expression Evaluation (Shunting-Yard Algorithm):
Dijkstra’s shunting-yard algorithm converts infix notation to postfix (Reverse Polish Notation), Which can then be evaluated with a simple stack-based algorithm.
def evaluate_expression ( expr ):
Evaluate a simple arithmetic expression with +, -, *, /, (, ).
if op == ' + ' : return a + b
if op == ' - ' : return a - b
if op == ' * ' : return a * b
if op == ' / ' : return a // b # integer division
while i < len (expr) and expr[i].isdigit():
val = val * 10 + int (expr[i])
while ops and ops[ - 1 ] != ' ( ' :
values.append(apply_op(values.pop(), values.pop(), ops.pop()))
while ops and precedence(ops[ - 1 ]) >= precedence(c):
values.append(apply_op(values.pop(), values.pop(), ops.pop()))
values.append(apply_op(values.pop(), values.pop(), ops.pop()))
Bracket Matching:
def is_valid_parentheses ( s ):
Check if brackets are properly matched and nested.
matching = { ' ) ' : " ('', " ] ' : "[ '' , "} ' : " {''}
if not stack or stack[ - 1 ] != matching[c]:
Call Stack:
Every recursive function call is pushed onto the call stack. Deep recursion can cause stack overflow (the call stack has limited size, 1-8 MB). Iterative solutions using an explicit stack Avoid this limit.
A queue is a FIFO (First In, First Out) data structure with enqueue (add to back) and dequeue (remove from front) operations.
Queue implemented as a circular buffer (ring buffer).
def __init__ ( self , capacity ):
self .data = [ None ] * capacity
self .head = 0 # front of queue
self .tail = 0 # one past the back of queue
if self .count == self .capacity:
raise IndexError ( " queue is full " )
self .data[ self .tail] = val
self .tail = ( self .tail + 1 ) % self .capacity
raise IndexError ( " dequeue from empty queue " )
val = self .data[ self .head]
self .head = ( self .head + 1 ) % self .capacity
raise IndexError ( " peek at empty queue " )
return self .data[ self .head]
Queues, audio playback buffers, log rotation, producer-consumer patterns, and pipe implementations. The key advantage is that enqueue and dequeue never require memory allocation or copying — they just Advance indices modulo the capacity.A deque (double-ended queue) supports insertion and deletion at both ends in O ( 1 ) O(1) O ( 1 ) .
from collections import deque
## Python's built-in deque — implemented as a doubly-linked list of fixed-size blocks
d.append( 1 ) # O(1) — enqueue back
d.appendleft( 0 ) # O(1) — enqueue front
d.pop() # O(1) — dequeue back
d.popleft() # O(1) — dequeue front
d[ 0 ] # O(1) — access by index (slower than list for large deques)
A priority queue supports insertion of elements with associated priorities and extraction of the Minimum (or maximum) priority element. The standard implementation uses a binary heap.
A binary heap is a complete binary tree where every node is less than or equal to its children (min-heap) or greater than or equal to its children (max-heap). Stored as an array where for node at Index i i i : parent is at ( i − 1 ) / 2 (i-1)/2 ( i − 1 ) /2 Left child is at 2 i + 1 2i+1 2 i + 1 Right child is at 2 i + 2 2i+2 2 i + 2 .
insert: O(log n), extract_min: O(log n), peek: O(1)
while i > 0 and self .data[ self ._parent(i)] > self .data[i]:
self .data[ self ._parent(i)], self .data[i] = self .data[i], self .data[ self ._parent(i)]
if left < n and self .data[left] < self .data[smallest]:
if right < n and self .data[right] < self .data[smallest]:
self .data[i], self .data[smallest] = self .data[smallest], self .data[i]
self ._sift_up( len ( self .data) - 1 )
raise IndexError ( " extract from empty heap " )
self .data[ 0 ] = self .data[ - 1 ]
raise IndexError ( " peek at empty heap " )
"""Build a heap from an array. O(n)."""
for i in range ( len ( self .data) // 2 - 1 , - 1 , - 1 ):
Operation Binary Heap Sorted Array Unsorted Array Insert O ( log n ) O(\log n) O ( log n ) O ( n ) O(n) O ( n ) O ( 1 ) O(1) O ( 1 ) Extract min O ( log n ) O(\log n) O ( log n ) O ( 1 ) O(1) O ( 1 ) O ( n ) O(n) O ( n ) Peek O ( 1 ) O(1) O ( 1 ) O ( 1 ) O(1) O ( 1 ) O ( n ) O(n) O ( n ) Build from array O ( n ) O(n) O ( n ) O ( n log n ) O(n \log n) O ( n log n ) O ( 1 ) O(1) O ( 1 )
A monotonic stack maintains elements in either strictly increasing or strictly decreasing order. It Is used to find the next greater/lesser element, previous greater/lesser element, and similar Patterns.
def next_greater_element ( arr ):
Find the next greater element for each element in the array.
If no greater element exists, result is -1.
stack = [] # stores indices; values are monotonically decreasing
while stack and arr[stack[ - 1 ]] < arr[i]:
def largest_rectangle_histogram ( heights ):
Find the area of the largest rectangle in a histogram.
stack = [] # indices of bars in increasing height order
# Use height 0 as sentinel for remaining bars
current_height = heights[i] if i < n else 0
while stack and heights[stack[ - 1 ]] > current_height:
height = heights[stack.pop()]
width = i if not stack else i - stack[ - 1 ] - 1
max_area = max (max_area, height * width)
A monotonic queue (deque) maintains elements in monotonic order and is used for sliding window Maximum/minimum problems.
from collections import deque
def sliding_window_maximum ( arr , k ):
Find the maximum in each sliding window of size k.
dq = deque() # stores indices; values are monotonically decreasing
for i in range ( len (arr)):
# Remove elements outside the window
while dq and dq[ 0 ] <= i - k:
# Remove elements smaller than current (they can never be the maximum)
while dq and arr[dq[ - 1 ]] < arr[i]:
result.append(arr[dq[ 0 ]])
Maximum of any future window that includes the current element. Removing them from the deque Maintains the invariant that the deque contains a decreasing sequence of values, and the maximum is Always at the front.Union-Find is a data structure that tracks a partition of elements into disjoint sets, supporting Two operations: find (which set does an element belong to?) and union (merge two sets).
Union-Find with path compression and union by rank.
find: O(alpha(n)) amortised (inverse Ackermann, effectively O(1))
union: O(alpha(n)) amortised
self .parent = list ( range (n))
self .count = n # number of disjoint sets
self .parent[x] = self .find( self .parent[x]) # path compression
if self .rank[root_x] < self .rank[root_y]:
root_x, root_y = root_y, root_x
self .parent[root_y] = root_x
if self .rank[root_x] == self .rank[root_y]:
def connected ( self , x , y ):
return self .find(x) == self .find(y)
The amortised time complexity of both find and union is O ( α ( n ) ) O(\alpha(n)) O ( α ( n )) Where α \alpha α is the Inverse Ackermann function. For all practical values of n n n (up to 2 2 2 65536 2^{2^{2^{65536}}} 2 2 2 65536 ), α ( n ) ≤ 4 \alpha(n) \le 4 α ( n ) ≤ 4 . This is effectively constant time.
The two optimisations work together:
Path compression : During findMake every node on the path point directly to the rootUnion by rank : During unionAttach the shorter tree under the root of the taller treeWithout either optimisation, find is O ( log n ) O(\log n) O ( log n ) and union is O ( log n ) O(\log n) O ( log n ) . Without both, worst Case is O ( n ) O(n) O ( n ) .
def count_connected_components ( n , edges ):
Count connected components in an undirected graph.
Time: O(n + m * alpha(n)), Space: O(n)
where m = number of edges
def detect_cycle_undirected ( n , edges ):
Detect if an undirected graph has a cycle using Union-Find.
Time: O(n + m * alpha(n)), Space: O(n)
The most common linked list bug is modifying the head pointer (e.g., during insertion at head or Reversal) and losing the reference to the entire list. Always use a dummy head node or return the New head explicitly. For operations that might modify the head, use:
dummy = ListNode( 0 , head)
## ... operate on dummy.next ...
Always check for None before accessing .next or .val. The pattern while current and current.next is safer than while current when you need to access current.next.val. Edge cases: empty list, single-node list, and the last node of the list.
Stacks are LIFO, queues are FIFO. Using the wrong one produces incorrect results for Ordering-sensitive problems. BFS requires a queue; DFS can use either a stack or recursion. Level-order traversal requires a queue. Expression evaluation uses a stack.
Monotonic stack and queue algorithms often use a sentinel value (e.g., 0 for histogram heights, -infinity for next greater element) to flush remaining elements from the data structure. Forgetting the sentinel means elements at the end of the array are never processed.
Without path compression, Union-Find degrades to O ( log n ) O(\log n) O ( log n ) per operation. Without union by rank, It degrades to O ( n ) O(n) O ( n ) worst case. Always use both optimisations. The code overhead is minimal (3 Extra lines) and the performance difference is enormous for large inputs.
Python’s default recursion limit is 1000. For linked lists with more than 1000 nodes, recursive Solutions (recursive reversal, recursive palindrome check) will crash with RecursionError. Use Iterative solutions for production code, or increase the limit with sys.setrecursionlimit() if you Are certain the input size is bounded.
Standard binary heaps do not support efficient decrease-key operations (common in Dijkstra’s Algorithm). The workaround — insert a new entry and ignore stale entries — works but increases the Heap size. For algorithms that require frequent decrease-key, a Fibonacci heap provides O ( 1 ) O(1) O ( 1 ) Amortised decrease-key, but has large constant factors and is rarely used in practice.
This topic covers the core concepts of linked lists, stacks, and queues, including underlying theory, practical implementation, and key applications.
Key concepts include:
Python data structures (lists, dicts, sets) list comprehensions and generators object-oriented Python decorators and context managers error handling with try/except Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Hashing and Hash Tables : Covers hash-based data structures that provide O(1) average-case operations compared to linear structures.Sorting Algorithms : Sorting techniques that can be applied to linked lists and arrays.Dynamic Programming : Explores recursive problem-solving that builds on stack and queue concepts.