Computer Science › Algorithms & Software
Collisions depend on n squared relative to m, not n relative to m, so they become likely once n reaches roughly the square root of m, far earlier than a hash table being anywhere near full.
No single fixed hash function can resist an adversary who knows it, so universal hashing draws a fresh function at random from a family guaranteeing collision probability at most 1/m for any pair of keys.
The FKS two-level construction eliminates collisions entirely, worst-case O(1) lookup with expected O(n) total space, but only for a static set of keys known completely in advance.
A bit array plus k hash functions, with no keys ever stored, gives false positives that are possible but false negatives that are structurally impossible.
The false positive rate is approximately (1 - e^(-kn/m))^k, minimized at k = (m/n)*ln(2), shrinking exponentially as more bits per element are allocated, turning filter sizing into simple arithmetic.
A tree where a key's characters are spelled out one per edge along a root-to-node path — keys sharing a prefix share that prefix's path, something no hash table can offer.
Inserting and searching a trie one character at a time, and the operation a hash table fundamentally cannot do efficiently — listing every key that starts with a given prefix.
A space-efficient trie variant using three children per node instead of one per alphabet character, trading a little search speed for a dramatically smaller memory footprint.
Two genuine, well-known uses of prefix trees — a search box's autocomplete suggestions, and a router's longest-prefix-match lookup for forwarding IP packets.
A structure for tracking a collection of disjoint groups, supporting exactly two operations — merge two groups, and ask whether two elements are in the same one.
Two naive first attempts at implementing union-find — one makes membership-checking instant but merging slow, the other the reverse — neither good enough on its own.
The first key optimization: always attach the smaller (or shallower) tree under the bigger one's root, which alone caps every tree's height at O(log n).
The second key optimization: while walking up to find a group's root, point every node visited along the way directly at that root, flattening the tree for every future lookup.
Combined, union by rank and path compression make a sequence of operations run in almost — but not quite exactly — constant time each, a famous result involving the inverse Ackermann function.
A binary tree where every node is at least as large as (or at least as small as) both of its children — weaker than a BST's ordering, but exactly enough to make "find the max" instant.
Storing a complete binary tree in a plain array — a node at index i has children at 2i+1 and 2i+2 — avoiding pointers entirely and packing the tree with zero wasted space.
The two operations a priority queue actually needs — adding an element and removing the highest (or lowest) priority one — each done in O(log n) by bubbling along a single root-to-leaf path.
Turning an arbitrary array into a valid heap in O(n) time, not the O(n log n) a naive one-at-a-time insertion would cost — a genuinely surprising result worth deriving carefully.
Heapify the whole array once, then repeatedly extract the max into the back of the array — an O(n log n), in-place sort that falls directly out of the heap operations already covered.
Picking up exactly where Data Structures I left off: a plain BST degrades to O(n) on a lopsided tree, and this discipline exists to fix that guarantee for good.
A binary search tree that enforces one simple rule after every insertion or deletion — the heights of any node's two subtrees never differ by more than one.
The local, constant-time restructuring operation — a rotation — that restores AVL's balance invariant after an insertion or deletion knocks it out of alignment.
A looser balance invariant, enforced with a color on every node instead of a strict height check — the scheme most production libraries (Java's TreeMap, C++'s std::map) actually use.
Both guarantee O(log n) operations — the real difference is in the constants: AVL's stricter balance means faster lookups but more rotations; red-black's looser balance means the reverse.
A hierarchy of sorted linked lists, where an element's height is decided by an independent coin flip at insertion, achieving expected O(log n) with no invariant to maintain and no rotation logic at all.
Insertion and deletion reuse the exact same downward search pass, recording an update array of predecessor nodes per level, with no separate fix-up phase of any kind.
An element reaches level k with probability (1/2)^k, giving expected height Θ(log n) and expected O(log n) search, a guarantee over the skip list's own coin flips that no adversarial input can defeat.
A side-by-side decision guide across every structure covered in Data Structures I and II, hash table, BST, AVL/red-black tree, heap, union-find, trie, skip list, and Bloom filter, organized by what each one is actually good at.
Part of the Computer Science curriculum.
Collisions depend on n squared relative to m, not n relative to m, so they become likely once n reaches roughly the square root of m, far earlier than a hash table being anywhere near full.
No single fixed hash function can resist an adversary who knows it, so universal hashing draws a fresh function at random from a family guaranteeing collision probability at most 1/m for any pair of keys.
The FKS two-level construction eliminates collisions entirely, worst-case O(1) lookup with expected O(n) total space, but only for a static set of keys known completely in advance.
A bit array plus k hash functions, with no keys ever stored, gives false positives that are possible but false negatives that are structurally impossible.
The false positive rate is approximately (1 - e^(-kn/m))^k, minimized at k = (m/n)*ln(2), shrinking exponentially as more bits per element are allocated, turning filter sizing into simple arithmetic.
A tree where a key's characters are spelled out one per edge along a root-to-node path — keys sharing a prefix share that prefix's path, something no hash table can offer.
Inserting and searching a trie one character at a time, and the operation a hash table fundamentally cannot do efficiently — listing every key that starts with a given prefix.
A space-efficient trie variant using three children per node instead of one per alphabet character, trading a little search speed for a dramatically smaller memory footprint.
Two genuine, well-known uses of prefix trees — a search box's autocomplete suggestions, and a router's longest-prefix-match lookup for forwarding IP packets.
A structure for tracking a collection of disjoint groups, supporting exactly two operations — merge two groups, and ask whether two elements are in the same one.
Two naive first attempts at implementing union-find — one makes membership-checking instant but merging slow, the other the reverse — neither good enough on its own.
The first key optimization: always attach the smaller (or shallower) tree under the bigger one's root, which alone caps every tree's height at O(log n).
The second key optimization: while walking up to find a group's root, point every node visited along the way directly at that root, flattening the tree for every future lookup.
Combined, union by rank and path compression make a sequence of operations run in almost — but not quite exactly — constant time each, a famous result involving the inverse Ackermann function.
A binary tree where every node is at least as large as (or at least as small as) both of its children — weaker than a BST's ordering, but exactly enough to make "find the max" instant.
Storing a complete binary tree in a plain array — a node at index i has children at 2i+1 and 2i+2 — avoiding pointers entirely and packing the tree with zero wasted space.
The two operations a priority queue actually needs — adding an element and removing the highest (or lowest) priority one — each done in O(log n) by bubbling along a single root-to-leaf path.
Turning an arbitrary array into a valid heap in O(n) time, not the O(n log n) a naive one-at-a-time insertion would cost — a genuinely surprising result worth deriving carefully.
Heapify the whole array once, then repeatedly extract the max into the back of the array — an O(n log n), in-place sort that falls directly out of the heap operations already covered.
Picking up exactly where Data Structures I left off: a plain BST degrades to O(n) on a lopsided tree, and this discipline exists to fix that guarantee for good.
A binary search tree that enforces one simple rule after every insertion or deletion — the heights of any node's two subtrees never differ by more than one.
The local, constant-time restructuring operation — a rotation — that restores AVL's balance invariant after an insertion or deletion knocks it out of alignment.
A looser balance invariant, enforced with a color on every node instead of a strict height check — the scheme most production libraries (Java's TreeMap, C++'s std::map) actually use.
Both guarantee O(log n) operations — the real difference is in the constants: AVL's stricter balance means faster lookups but more rotations; red-black's looser balance means the reverse.
A hierarchy of sorted linked lists, where an element's height is decided by an independent coin flip at insertion, achieving expected O(log n) with no invariant to maintain and no rotation logic at all.
Insertion and deletion reuse the exact same downward search pass, recording an update array of predecessor nodes per level, with no separate fix-up phase of any kind.
An element reaches level k with probability (1/2)^k, giving expected height Θ(log n) and expected O(log n) search, a guarantee over the skip list's own coin flips that no adversarial input can defeat.
A side-by-side decision guide across every structure covered in Data Structures I and II, hash table, BST, AVL/red-black tree, heap, union-find, trie, skip list, and Bloom filter, organized by what each one is actually good at.