Every structure and algorithm a syllabus expects, with the complexities that get asked and the conditions under which the textbook answer is wrong.
Asymptotic notation describes how cost grows as input size grows, ignoring constants and lower-order terms. The point is to compare algorithms independently of machine speed.
| Notation | Bound | Read as | Formally |
|---|---|---|---|
| Upper | Grows no faster than | for some and all | |
| Lower | Grows at least as fast as | for some and all | |
| Tight | Grows exactly like | Both and hold | |
| Strict upper | Grows strictly slower than | Holds for every |
Anything at or below n log n is generally practical; 2^n and n! are usable only for tiny inputs.
The average cost per operation across a worst-case sequence of operations. A dynamic array's push is O(n) when it must resize, but resizing doubles the capacity, so the O(n) copies are spread over n cheap pushes - the amortised cost is O(1). Note this is not the same as average-case analysis: amortised is a worst-case guarantee over a sequence, with no probability involved.
For a recurrence of the form with , - compare against :
| Case | Condition | Result | Example |
|---|---|---|---|
| 1 | - leaves dominate | Binary tree traversal: | |
| 2 | - balanced | Mergesort: | |
| 3 | and regularity holds - root dominates |
| Operation | Array | Dynamic array | Singly linked list | Doubly linked list |
|---|---|---|---|---|
| Access by index | O(1) | O(1) | O(n) | O(n) |
| Search (unsorted) | O(n) | O(n) | O(n) | O(n) |
| Insert at front | O(n) | O(n) | O(1) | O(1) |
| Insert at back | - | O(1) amortised | O(n), or O(1) with a tail pointer | O(1) |
| Delete a known node | O(n) | O(n) | O(n) - need the predecessor | O(1) |
| Memory overhead | None | Slack capacity | One pointer per node | Two pointers per node |
| Cache locality | Excellent | Excellent | Poor | Poor |
| Stack | Queue | Deque | Priority queue | |
|---|---|---|---|---|
| Discipline | LIFO | FIFO | Both ends | By priority |
| Operations | push, pop, peek | enqueue, dequeue, front | pushFront/Back, popFront/Back | insert, extractMin |
| All O(1)? | Yes | Yes | Yes | No - O(log n) via a heap |
| Classic uses | Call stack, expression evaluation, DFS, undo, bracket matching | BFS, scheduling, buffering, producer-consumer | Sliding-window maximum, work stealing | Dijkstra, Huffman coding, task scheduling |
// 1. Detect a cycle in a linked list - Floyd's tortoise and hare, O(n) time O(1) space
int hasCycle(Node *head) {
Node *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next; // one step
fast = fast->next->next; // two steps
if (slow == fast) return 1; // they can only meet inside a cycle
}
return 0;
}
// 2. Reverse a singly linked list, iteratively
Node *reverse(Node *head) {
Node *prev = NULL, *cur = head;
while (cur) {
Node *next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev; // prev is the new head
}A hash table maps a key to a bucket index via a hash function, giving O(1) average lookup. Everything interesting is about what happens when two keys land in the same bucket.
| Collision strategy | How it works | Trade-off |
|---|---|---|
| Separate chaining | Each bucket holds a linked list (or tree) of entries | Simple, degrades gracefully, tolerates load factor > 1; costs pointers and loses locality |
| Linear probing | On collision try the next slot, wrapping around | Excellent cache locality; suffers primary clustering - runs of full slots grow and merge |
| Quadratic probing | Try slots away | Breaks primary clustering; introduces secondary clustering and may not probe every slot |
| Double hashing | Step size is itself a second hash of the key | Best distribution of the open-addressing schemes; two hash computations per probe |
| Operation | Average | Worst case |
|---|---|---|
| Insert | O(1) | O(n) |
| Search | O(1) | O(n) |
| Delete | O(1) | O(n) |
The worst case is every key hashing to one bucket. Java 8 mitigates it by converting a bucket to a red-black tree past eight entries, capping the worst case at O(log n).
| Traversal | Order | Yields | Typical use |
|---|---|---|---|
| Inorder | Left, Node, Right | For a BST: sorted order | Retrieve sorted keys; validate a BST |
| Preorder | Node, Left, Right | Root first | Copy or serialise a tree; prefix expressions |
| Postorder | Left, Right, Node | Children before parent | Delete a tree; evaluate an expression tree; postfix |
| Level order | Breadth-first, by depth | Level by level | Print by level; find shortest path in an unweighted tree |
The first three are depth-first and naturally recursive (O(h) stack). Level order needs an explicit queue.
| BST operation | Balanced | Degenerate (sorted insertion) |
|---|---|---|
| Search | O(log n) | O(n) |
| Insert | O(log n) | O(n) |
| Delete | O(log n) | O(n) |
Inserting already-sorted keys into a plain BST produces a linked list. This is exactly why balanced trees exist.
Leaf node
One child
Two children
| AVL tree | Red-black tree | B-tree / B+ tree | |
|---|---|---|---|
| Balance rule | Height of the two subtrees differs by at most 1 | Five colour rules; no red node has a red child | All leaves at the same depth; each node holds between and keys |
| Height bound | - more rigidly balanced | - very shallow for large | |
| Lookup | Faster (shorter tree) | Slightly slower | Few node visits, but each is a disk block |
| Insert/delete | More rotations | At most 2 rotations for insert, 3 for delete | Splits and merges |
| Best for | Read-heavy in-memory workloads | Mixed read/write; the general-purpose default | Disk-resident indexes |
| Used by | Some in-memory databases | std::map, Java TreeMap, Linux CFS scheduler | Database indexes, filesystems (NTFS, ext4) |
| Operation | Complexity | Mechanism |
|---|---|---|
| Find min/max | O(1) | It is the root |
| Insert | O(log n) | Append at the end, then sift up |
| Extract min/max | O(log n) | Swap root with last, shrink, then sift down |
| Decrease key | O(log n) | Lower the value, sift up |
| Build heap from n items | O(n) | Sift down from the last internal node backwards |
| Search arbitrary key | O(n) | No ordering between siblings |
| Adjacency matrix | Adjacency list | |
|---|---|---|
| Space | ||
| Edge exists? | O(1) | |
| Enumerate neighbours | ||
| Add an edge | O(1) | O(1) |
| Best for | Dense graphs; algorithms needing fast edge lookup | Sparse graphs - nearly all real ones |
| BFS | DFS | |
|---|---|---|
| Data structure | Queue | Stack (or recursion) |
| Order | Level by level from the source | As deep as possible, then backtrack |
| Complexity | ||
| Space | - can be the whole frontier | - the current path depth |
| Finds | Shortest path in an unweighted graph | Cycles, topological order, connected components, bridges and articulation points |
| Also used for | Bipartiteness checking, web crawling by level | Maze solving, backtracking, strongly connected components |
# Undirected: any visited neighbour that is not the parent closes a cycle.
def has_cycle_undirected(g, u, parent, seen):
seen.add(u)
for v in g[u]:
if v not in seen:
if has_cycle_undirected(g, v, u, seen): return True
elif v != parent:
return True # back edge
return False
# Directed: needs three colours. A grey neighbour means a back edge; a black
# one is merely a cross edge to a finished subtree and is NOT a cycle.
WHITE, GREY, BLACK = 0, 1, 2
def has_cycle_directed(g, u, colour):
colour[u] = GREY
for v in g[u]:
if colour[v] == GREY: return True
if colour[v] == WHITE and has_cycle_directed(g, v, colour): return True
colour[u] = BLACK
return False| Algorithm | Solves | Complexity | Negative weights? |
|---|---|---|---|
| Dijkstra | Single-source shortest path | with a binary heap | No - fails silently |
| Bellman-Ford | Single-source shortest path | Yes, and detects negative cycles | |
| Floyd-Warshall | All-pairs shortest paths | Yes (no negative cycles) | |
| Kruskal | Minimum spanning tree | Yes - weights may be negative | |
| Prim | Minimum spanning tree | with a heap | Yes |
| Topological sort | Linear order of a DAG | N/A - unweighted |
| Algorithm | Best | Average | Worst | Space | Stable | Method |
|---|---|---|---|---|---|---|
| Bubble | Yes | Exchange | ||||
| Selection | No | Selection | ||||
| Insertion | Yes | Insertion | ||||
| Merge | Yes | Divide & conquer | ||||
| Quick | No | Divide & conquer | ||||
| Heap | No | Selection | ||||
| Counting | Yes | Non-comparison | ||||
| Radix | Yes | Non-comparison |
| Technique | Requires | Complexity | Note |
|---|---|---|---|
| Linear search | Nothing | O(n) | The only option on unsorted data |
| Binary search | Sorted, random access | O(log n) | Halves the range each step |
| Jump search | Sorted array | Fixed-size jumps then a linear scan | |
| Interpolation search | Sorted and uniformly distributed | average, O(n) worst | Estimates position from the value |
| Exponential search | Sorted, unbounded | O(log n) | Find a range by doubling, then binary search |
| Hash lookup | A hash table | O(1) average | No ordering, so no range queries |
int binary_search(int a[], int n, int key) {
int lo = 0, hi = n - 1;
while (lo <= hi) {
// NOT (lo + hi) / 2 - that overflows once lo + hi exceeds INT_MAX.
// This bug sat undetected in the JDK for nine years.
int mid = lo + (hi - lo) / 2;
if (a[mid] == key) return mid;
if (a[mid] < key) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}| Paradigm | Idea | Applies when | Examples |
|---|---|---|---|
| Divide & conquer | Split into independent subproblems, solve, combine | Subproblems do not overlap | Mergesort, quicksort, binary search, Strassen |
| Greedy | Take the locally best choice and never reconsider | The greedy-choice property and optimal substructure both hold | Dijkstra, Kruskal, Prim, Huffman, activity selection |
| Dynamic programming | Solve each overlapping subproblem once and store the result | Optimal substructure and overlapping subproblems | Knapsack, LCS, edit distance, matrix chain, Floyd-Warshall |
| Backtracking | Build candidates incrementally, abandon a branch as soon as it cannot work | Constraint satisfaction over a search tree | N-queens, sudoku, subset sum, graph colouring |
| Branch & bound | Backtracking plus a bound that prunes provably worse branches | Optimisation over a large search space | 0/1 knapsack, travelling salesman |
| Classic DP problem | State | Complexity |
|---|---|---|
| 0/1 knapsack | dp[i][w] = best value using first i items within weight w | - pseudo-polynomial |
| Longest common subsequence | dp[i][j] = LCS of the two prefixes | |
| Edit distance | dp[i][j] = edits to turn prefix i into prefix j | |
| Matrix chain multiplication | dp[i][j] = min cost to multiply matrices i..j | |
| Coin change (min coins) | dp[a] = fewest coins summing to a | |
| Longest increasing subsequence | dp[i] = LIS ending at i | , or with binary search |
12 questions on Data Structures & Algorithms · misses join your review queue, on this device only