This is a review of the Princeton University Algorithms, Part I course offered on Coursera.
For the second part of the course, see Algorithms II.
My solutions to the programming assignments are available on GitHub.
Course Overview
| Topic | Data Structures and Algorithms |
|---|---|
| Data Types | Stack, Queue, Bag, Union-Find, Priority Queue |
| Sorting | Quicksort, Mergesort, Heapsort |
| Searching | Binary Search Tree (BST), Red-Black Tree, Hash Table |
| Graphs | Breadth-First Search (BFS), Depth-First Search (DFS), Prim’s, Kruskal’s, Dijkstra’s |
| Strings | Radix Sorts, Tries, Knuth-Morris-Pratt (KMP), Regular Expressions, Data Compression |
| Advanced | B-tree, Suffix Array, Max-Flow Min-Cut |
Union-Find
The union-find data structure addresses the problem of partitioning a set of $N$ objects into a collection of disjoint sets. It is particularly useful for tracking connections between components.
Goal: Given a set of $N$ objects, implement a data structure that supports two primary operations:
union(p, q): Connects two objects,pandq, by merging their respective sets.connected(p, q): Determines if a path exists betweenpandq, meaning they belong to the same set.

The efficiency of a union-find implementation is measured by the time complexity of its union and connected operations.
| Algorithm | Initialization | Union | Connected |
|---|---|---|---|
| Quick-Find | $O(N)$ | $O(N)$ | $O(1)$ |
| Quick-Union | $O(N)$ | $O(N)$ | $O(N)$ |
| Weighted QU | $O(N)$ | $O(\lg N)$ | $O(\lg N)$ |
Quick-Find
The quick-find algorithm uses an integer array id[] of length $N$, where id[i] holds the component identifier for object i.
- Initialization: Each object starts in its own component,
id[i] = i. union(p, q): Change all entries withid[p]toid[q].connected(p, q): Check ifpandqhave the same component identifier.
| |
Analysis:
connectedis $O(1)$.unionis too expensive, requiring $O(N)$ array accesses.- The component trees are kept flat, but maintaining this structure is costly.
Quick-Union
The quick-union algorithm also uses an integer array id[], but id[i] now stores the parent of object i. This creates a forest of trees, where each tree represents a component.
- Initialization: Each object is its own parent,
id[i] = i. union(p, q): Set the root ofpto point to the root ofq.connected(p, q): Check ifpandqhave the same root. Therootis found by chasing parent pointers until a node points to itself.
| |
Analysis:
unionis faster than in quick-find.rootcan be too expensive (worst case $N$ array accesses).- The component trees can become excessively tall.
Weighted Quick-Union
This enhanced version of quick-union improves performance by preventing the creation of tall, thin trees.
- Initialization: An additional array
sz[]is used, wheresz[i]stores the number of objects in the tree rooted ati. union(p, q): Link the root of the smaller tree to the root of the larger tree, then update thesz[]accordingly.connected(p, q): Same as quick-union.- Path compression: An optimization to flatten the tree during
roottraversals. By adding a single line of codeid[i] = id[id[i]], we make every other node on the path point directly to its grandparent, halving the path length.
| |
Analysis:
connectedtakes time proportional to the depth of nodes.uniontakes constant time (given the roots).- The depth of any node is at most $\lg N$.
Applications
- Identifying pixel clusters in digital photos.
- Determining network connectivity.
- Modeling social networks.
- Analyzing electrical connectivity in computer chips.
- Managing sets of elements in mathematics.
- Equivalence of variable names in compilers.
- Studying percolation in physical systems.
HW1: Percolation
This assignment applies union-find to model a classic physics problem.
Problem: Consider an $N\times N$ grid of sites. Each site is independently open with probability $p$. The system is said to percolate if the top row is connected to the bottom row by a path of open sites.

When $N$ is large, a sharp percolation threshold $p^*$ emerges. The goal is to estimate this value.
Monte Carlo simulation:
- Initialize an $N$-by-$N$ grid with all sites blocked.
- Open sites one by one at random until the system percolates.
- The fraction of open sites provides an estimate of $p^*$.
Check for percolation:
- Model the grid using a union-find structure with $N^{2}$ objects, one for each site.
- Sites in the same component are connected by open sites.
- Brute-force: Iterate through all top-row sites and check if any is connected to any bottom-row sites. This would require up to $N^2$ calls to
connected(). - Optimized approach: Introduce two virtual sites, one connected to all top-row sites and another connected to all bottom-row sites. The system percolates if and only if the virtual top site is connected to the virtual bottom site. This check requires only a single call to
connected().

The “Backwash” Problem with Virtual Sites:
- When the system percolates (virtual top is connected to virtual bottom), any open site connected to the bottom row will also appear to be connected to the virtual top site. This is the backwash problem.
- Solution 1 (two UF objects): Use two
WeightedQuickUnionUFobjects. The first includes both virtual sites and is used only for the percolation check. The second includes only the virtual top site and is used to correctly check if a site is full (connected to the top). - Solution 2 (manual state): Use a single
WeightedQuickUnionUFobject and manually maintain boolean arrays to track which components are connected to the top and bottom rows. This prevents the backwash issue with less memory overhead.
Solution file: Percolation.java
Bag, Queue, and Stack
These data structures all support core operations like insertion, removal, and iteration. The difference is how they store and retrieve elements.
- Stack: A collection that follows a Last-In, First-Out (LIFO) policy. The most recently added item is the first one to be removed.
- Queue: A collection that follows a First-In, First-Out (FIFO) policy. The least recently added item is the first one to be removed.
- Bag: A collection for adding items where the order of retrieval is not specified and does not matter.
Stack
A stack’s primary operations are push (add an item to the top) and pop (remove an item from the top).
Linked-List Implementation
This implementation uses a singly linked list, where the first node represents the top of the stack.
| |
Resizing-Array Implementation
This implementation uses an array that dynamically resizes to accommodate a changing number of items.
| |
Resizing-Array vs. Linked-List
- Linked-List:
- Every operation takes constant time in the worst case.
- Uses extra time and space to manage the links for each node.
- Resizing-Array:
- Every operation takes constant amortized time.
- Generally results in less wasted space than a linked-list implementation.
Queue
A queue’s primary operations are enqueue (add an item to the back) and dequeue (remove an item from the front).
Linked-List Implementation
To achieve constant-time enqueue and dequeue operations, this implementation maintains references to both the first and last nodes in the queue.
| |
Resizing-Array Implementation
This implementation uses a resizing array with wrap-around indexing to efficiently manage the first and last elements.
| |
Generic
Generic implementations allow a data structure to hold objects of any type, enhancing code reusability.
- Avoids the need for explicit type casting in client code.
- Enables compile-time type checking, preventing runtime errors
The following example shows a generic stack. Note that creating a generic array directly (new Item[capacity]) is not permitted in Java; a cast from Object[] is required.
| |
Iterator
Iterators provide a standard way for clients to loop through the items in a collection without exposing its internal representation. This is achieved by implementing the java.lang.Iterable interface.
Linked-List Implementation
| |
Array Implementation
| |
Java For-Each Loop
When a class implements Iterable, Java’s elegant for-each loop syntax can be used to iterate over its elements.
| |
Applications
- Parsing expressions in compilers.
- Managing the call stack in the Java Virtual Machine.
- Implementing “Undo” functionality in text editors.
- Navigating history with the “Back” button in web browsers.
- Interpreting the PostScript programming language.
Dijkstra’s Two-Stack Algorithm
This classic algorithm evaluates fully parenthesized arithmetic expressions using two stacks: one for operators and one for values.
$$ (\ 1\ +\ (\ (\ 2\ +\ 3\ )\ *\ (\ 4\ *\ 5\ )\ )\ ) $$The algorithm proceeds as follows:
- Value: If a value is read, push it onto the value stack.
- Operator: If an operator is read, push it onto the operator stack.
- Left Parenthesis: Ignore.
- Right Parenthesis: Pop an operator and two values. Compute the result and push it back onto the value stack.
| |
This algorithm can be adapted for postfix notation, where operators follow their operands:
$$ (\ 1\ (\ (\ 2\ 3\ +\ )\ (\ 4\ 5\ *\ )\ *\ )\ +\ ) $$In this case, parentheses are redundant:
$$ 1\ 2\ 3\ +\ 4\ 5\ *\ *\ + $$HW2: Deque and Randomized Queue
This assignment involves implementing two specialized queue-like data structures.
- Deque: A “double-ended queue” is a generalization of a stack and a queue that supports adding and removing items from both the front and the back. Deque.java
- Randomized queue: Similar to a standard queue, but the item removed is chosen uniformly at random from all items currently in the data structure. RandomizedQueue.java
- Client: Write a client program that takes an integer $k$ as a command-line argument, reads a sequence of strings from standard input, and prints exactly $k$ of them, selected uniformly at random. Permutation.java
A common solution for the client is Reservoir Sampling, an algorithm that creates a random sample of $k$ items from a list of unknown size $n$ in a single pass:
- Store the the first $k$ items in a collection.
- For each subsequent item $i$:
- With probability $k/i$, replace a random existing item in the collection with item $i$.
- With probability $1 - k/i$, discard item $i$.
Elementary Sort
Sorting is the process of rearranging a sequence of items into a specific order. This chapter covers several fundamental sorting algorithms.
| Algorithm | In-place? | Stable? | Worst Case | Average Case | Best Case | Remarks |
|---|---|---|---|---|---|---|
| Selection Sort | ✔ | $O(N^2)$ | $O(N^2)$ | $O(N^2)$ | $O(N)$ exchanges, insensitive to input order. | |
| Insertion Sort | ✔ | ✔ | $O(N^2)$ | $O(N^2)$ | $O(N)$ | Efficient for small or partially-sorted arrays. |
| Shellsort | ✔ | ? | ? | $O(N)$ | Sub-quadratic performance with concise code. | |
| Mergesort | ✔ | $O(N\lg N)$ | $O(N\lg N)$ | $O(N\lg N)$ | Guaranteed $O(N\lg N)$ performance, stable. | |
| Quicksort | ✔ | $O(N^2)$ | $O(N\lg N)$ | $O(N\lg N)$ | Probabilistic $O(N\lg N)$ guarantee; often fastest in practice. | |
| 3-way Quicksort | ✔ | $O(N^2)$ | $O(N\lg N)$ | $O(N)$ | Improves quicksort when many duplicate keys are present. | |
| Heapsort | ✔ | $O(N\lg N)$ | $O(N\lg N)$ | $O(N\lg N)$ | Guaranteed $O(N\lg N)$ performance, in-place. |
Comparable Interface
To make a user-defined data type sortable, it must have a defined order. Java’s Comparable interface provides a standard way to specify this “natural order”.
- Built-in Java types that are
Comparable:Integer,Double,String,Date,File. - User-defined types: A custom class can implement the
Comparableinterface by providing acompareTo()method.
The compareTo(That that) method should return:
- A negative integer if
thisobject is less thanthatobject. - Zero if
thisobject is equal tothatobject. - A positive integer if
thisobject is greater thanthatobject.
| |
Most sorting algorithms are built on two helper functions: less() for comparison and exch() for swapping elements.
| |
Selection Sort
Selection sort divides the array into a sorted region (left) and an unsorted region (right).
- Find the smallest remaining element in the unsorted region.
- Swap it with the leftmost element of the unsorted region.
- Expand the sorted region by one element to the right.
| |
Insertion Sort
Insertion sort also divides the array into a sorted left region and an unsorted right region.
- Take the first element from the unsorted region.
- Insert it into its correct position within the sorted region by shifting larger elements to the right.
- Repeat until the unsorted region is empty.
| |
Shellsort
Shellsort is an improvement over insertion sort that allows the exchange of items that are far apart. It works by sorting the array in multiple passes, using a different “gap” or “increment” h for each pass.
An h-sorted array is one where all sublists of items h positions apart are sorted. The key idea is that a g-sorted array remains g-sorted after being h-sorted. Shellsort uses a decreasing sequence of h values, ending with h=1. The final pass (h=1) is just an insertion sort, but it runs efficiently on the nearly-sorted array
Commonly used h sequences:
- Powers of two: $1, 2, 4, 8, 16, \dots$ (Not recommended)
- Power of two minus one: $1, 3, 7, 15, 31, \dots$ (Not good but better than power of two)
- $3x + 1$ sequence: $1, 4, 13, 40, 121, \dots$ (Good performance, easy to compute)
- Sedgewick sequence: $1, 5, 19, 41, 109, \dots$ (Excellent empirical performance)
| |
Shuffling
The goal of shuffling is to rearrange an array into a uniformly random permutation.
- Shuffle sort (Conceptual): Assign a random real number to each element and sort the array based on these numbers. This is computationally expensive.
- Fisher-Yates shuffle (Modern Method): This is the standard, efficient algorithm. Iterate through the array from left to right. For each element at index
i, swap it with an element at a randomly chosen indexrfrom the range[0, i].
| |
Convex Hull
The convex hull of a set of $N$ points is the smallest perimeter fence that can encloses all the points.
Applications
- Robot motion planning: The shortest path for a robot to get from point $s$ to $t$ while avoiding a polygonal obstacle is either a straight line or follows the convex hull of $s$, $t$, and the obstacle’s vertices.

- Farthest pair problem: Given $N$ points in a plane, the pair with the greatest Euclidean distance between them must be vertices of the convex hull.

Graham Scan
The Graham scan is an efficient algorithm for finding the convex hull that relies on sorting.
- Choose the point
pwith the smallest y-coordinate to be the pivot. - Sort all other points by the polar angle they make with
p. - Iterate through the sorted points, adding them to a stack representing the hull. If adding a point creates a clockwise or collinear turn with the top two points on the stack, pop the stack until a counter-clockwise turn is restored.

Mergesort
Mergesort is a classic divide-and-conquer sorting algorithm renowned for its guaranteed $O(N \lg N)$ performance. It is not an in-place algorithm, as it requires auxiliary space proportional to the input size.
The algorithm follows three steps:
- Divide: Divide the array into two equal halves.
- Conquer: Recursively sort each half.
- Combine: Merge the two sorted halves into a single, sorted array.
| |
The assert keyword in Java is a useful tool for verifying program invariants. Assertions can be enabled or disabled at runtime:
java -ea MyProgram(enables assertions)java -da MyProgram(disables assertions, which is the default)
Practical Improvements
Several optimizations can significantly improve mergesort’s real-world performance.
- Cutoff to insertion sort: For small subarrays, mergesort’s overhead is too high. A common optimization is to switch to insertion sort for subarrays smaller than a certain
CUTOFFsize (typically 5-15). - Check if already sorted: If the largest item in the first half is less than or equal to the smallest item in the second half (
a[mid] <= a[mid+1]), the array is already sorted, and the expensivemergestep can be skipped. - Eliminate array copy: Reduce memory usage by switching the roles of the input and auxiliary arrays at each level of the recursion.
| |
Bottom-Up Mergesort
This non-recursive version of mergesort iterates through the array, merging subarrays of increasing size. It performs a series of passes over the entire array, merging subarrays of size 1, then size 2, then 4, and so on, until the whole array is sorted.
| |
Comparator Interface
While Comparable defines a type’s natural order, the Comparator interface allows for alternate orderings. To use a comparator, you implement its compare() method.
| |
To sort using a Comparator, pass it as the second argument to Arrays.sort() or a custom sort method.
| |
Stability
A sorting algorithm is stable if it preserves the relative order of items with equal keys The image below shows a counter example.

- Stable: Insertion sort, Mergesort
- Unstable: Selection sort, Shellsort, Quicksort, Heapsort
HW3: Collinear Points
This assignment applies sorting to solve a geometric problem.
Problem: Given a set of $n$ distinct points in a plane, find every maximal line segment that connects a subset of 4 or more of the points.
- Point data type: First, create an immutable
Pointdata type. Point.java - Brute-force solution: Examine every combination of 4 points and check if they are collinear. This is done by verifying that the slopes between all pairs of points are equal. This approach is very slow, with a complexity of $O(N^4)$. BruteCollinearPoints.java
- Fast, sorting-based solution: A much more efficient approach solves the problem in $O(N^2\lg N)$ time. For each point $p$, treat it as an origin: FastCollinearPoints.java
- Calculate the slope that every other point $q$ makes with $p$.
- Sort the other points based on these slopes.
- In the sorted array, any set of three or more consecutive points with equal slopes are collinear with $p$.
Quicksort
Quicksort is an in-place, divide-and-conquer sorting algorithm. While its worst-case performance is $O(N^2)$, its average-case performance is $O(N\lg N)$, and in practice, it is often the fastest sorting algorithm available.
The algorithm follows three main steps:
- Shuffle: Shuffle the array to ensure that the performance guarantee is probabilistic and avoids worst-case behavior on already-sorted or partially-sorted data.
- Partition: Partition the array around an entry
a[j](the pivot) such that:- The entry
a[j]is in its final sorted position. - No entries to the left of
jare greater thana[j]. - No entries to the right of
jare smaller thana[j].
- The entry
- Sort: Recursively sort the two resulting subarrays.
| |
Practical Improvements
- Cutoff: Switch to insertion sort for small subarrays.
- Median-of-three pivot: To better handle partially-sorted arrays, choose the pivot as the median of the first, middle, and last items in the subarray. This makes a worst-case partitioning scenario less likely.
| |
Quickselect
The partition method can be repurposed to find the k-th smallest item in an array in expected linear time. After partitioning, the pivot a[j] is in its final sorted position. By comparing j to k, we can recursively search in only one of the two subarrays.
| |
3-Way Partitioning
Standard quicksort can be slow when an array contains many duplicate keys. Dijkstra’s 3-way partitioning solves this by dividing the array into three parts: elements less than the pivot, elements equal to the pivot, and elements greater than the pivot. This ensures that items equal to the pivot are not included in the recursive calls.
| |
Priority Queue
A priority queue is an abstract data type that supports inserting items and deleting the largest (or smallest) item.
| Implementation | Insert | Delete Max | Find Max |
|---|---|---|---|
| Unordered Array | $O(1)$ | $O(N)$ | $O(N)$ |
| Ordered Array | $O(N)$ | $O(1)$ | $O(1)$ |
| Binary Heap | $O(\lg N)$ | $O(\lg N)$ | $O(1)$ |
Array Implementation
Simple array-based implementations can be used for priority queues, but they are inefficient for handling both insertions and deletions.
Unordered Array Implementation
In this approach, we simply add new items to the end of the array. While insertion is fast, finding and removing the maximum element requires a full scan of the array.
| |
Ordered Array Implementation
This approach maintains the array in sorted order. Removing the maximum element is now fast, but insertion requires shifting existing elements to maintain the sorted order.
| |
Binary Heap
A binary heap is a data structure that can efficiently implement a priority queue. It is a complete binary tree that satisfies the heap-ordering property: the key in each node is greater than or equal to the keys of its children. A heap is typically represented using a simple array.
Key operations maintain the heap property:
- Swim: If a child’s key becomes larger than its parent’s, we “swim” it up by repeatedly exchanging it with its parent until the heap order is restored.
- Sink: If a parent’s key becomes smaller than one of its children’s, we “sink” it down by repeatedly exchanging it with its larger child until the heap order is restored.
- Insert: Add the new item at the end of the array and swim it up into position.
- Delete max: Exchange the root (the maximum item) with the last item, remove the max, and sink the new root down into position.
For easier arithmetic, the heap implementation uses 1-based indexing.
| |
Heapsort
Heapsort is an efficient, in-place sorting algorithm that uses a binary heap.
- Heap construction: First, the unsorted array is converted into a max-heap. This is done efficiently by calling
sink()on all non-leaf nodes, starting from the last one and moving backward to the root. - Sortdown: Repeatedly remove the maximum item from the heap (which is at index 1) and swap it with the last item in the heap. Then, decrement the heap size and
sink()the new root to restore the heap property.
| |
HW4: 8 Puzzle
The 8-puzzle is a classic sliding puzzle on a 3-by-3 grid with $8$ square blocks labeled 1 through 8 and a blank square. The goal is to rearrange the blocks into their sorted order using the minimum number of moves. Blocks can be slid horizontally or vertically into the blank square. Here is an example:
$$ \begin{matrix} & 1 & 3 \\ 4 & 2 & 5 \\ 7 & 8 & 6 \end{matrix} \quad\to\quad \begin{matrix} 1 & & 3 \\ 4 & 2 & 5 \\ 7 & 8 & 6 \end{matrix} \quad\to\quad \begin{matrix} 1 & 2 & 3 \\ 4 & & 5 \\ 7 & 8 & 6 \end{matrix} \quad\to\quad \begin{matrix} 1 & 2 & 3 \\ 4 & 5 & \\ 7 & 8 & 6 \end{matrix} \quad\to\quad \begin{matrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & \end{matrix} $$This problem can be solved using the A search algorithm*, which uses a priority queue to explore the most promising board states first.
- Create a search node for each board state, containing:
- The board configuration.
- The number of moves made to reach this board.
- A reference to the previous search node.
- Maintain a priority queue of search nodes, ordered by a priority function.
- Insert the initial search node into the priority queue.
- Repeatedly remove the search node with the minimum priority. If it’s the goal board, the solution is found. Otherwise, all valid neighboring search nodes are added to the priority queue.
The priority of a search node is its moves made so far + a heuristic estimate of the moves remaining. Common heuristic choices:
- Hamming priority: The number of blocks in the wrong position.
- Manhattan priority: The sum of the Manhattan distances (sum of row and column differences) of each block from its goal position. This is a more effective heuristic.
Optimizations:
- Avoid cycles: Do not enqueue a neighbor if its board is the same as the predecessor’s board.
- Precompute the Manhattan priority: When a search node is created, precompute its Manhattan priority and store it as an instance variable.
Solutions:
- Board: An immutable data type for an n-by-n board. Board.java
- Solver: The A* search algorithm implementation. Solver.java
Symbol Table
A symbol table is a data structure that stores a collection of key-value pairs, built on two core operation:
put: Add a new key-value pair to the table.get: Retrieve the value associated with a given key.
| |
Implementing equals for Custom Keys
For a symbol table to function correctly with user-defined key types, the key’s class must properly implement the equals() method. A robust implementation follows a standard recipe:
- Reference check: Check if the two objects are the same instance (
y == this). - Null check: Check if the other object is
null. - Class check: Check if the objects are of the same class (
y.getClass() != this.getClass()). - Field comparison: Cast the other object and compare all significant fields.
Using the final keyword on the class is recommended because implementing equals() correctly in the presence of inheritance is complex and error-prone.
| |
Binary Search Tree
A binary search tree is a binary tree in symmetric order. For any given node, all keys in its left subtree are smaller than the node’s key, and all keys in its right subtree are larger. This structure allows for efficient searching and sorting.
Node Structure
Each Node in the tree contains four fields: a key, a value, and references to the left and right subtrees.
| |
Basic Operations
get: To find a key, start at the root. If the target key is smaller than the current node’s key, go left; if it’s larger, go right. Repeat until the key is found or a null link is encountered.put: Follow the same search path asget. If the key is found, update its value. If a null link is reached, insert a new node at that position.
| |
Ordered Operations
Max, Min, Floor and Ceiling
The BST structure naturally supports ordered operations.
- Min/Max: The minimum key is found by traversing left from the root until a null link is hit. The maximum is found by traversing right.
- Floor/Ceiling: The floor of a key is the largest key in the BST that is less than or equal to the given key. The ceiling is the smallest key greater than or equal to the given key.
| |
Size, Rank, and Traversal
To support certain ordered operations efficiently, we can augment each node to store the size of the subtree rooted at that node.
| |
Rank: Find the number of keys strictly less than a given key. It uses the subtree counts to do this efficiently.
| |
Inorder traversal: A recursive inorder traversal (left subtree, node, right subtree) visits the keys of a BST in ascending order. This can be used to return all keys as an Iterable.
| |
Deletion
Deleting a node from a BST is more complex.
Deleting The Minimum
- Traverse left until a node with a null left link is found.
- Replace that node with its right child.
- Update subtree counts along the search path.
| |
Hibbard Deletion
To delete an arbitrary node, there are three cases:
- Node has no children: Set the link from its parent to null.
- Node has one child: Replace the node with its child.
- Node has two children:
- Find its successor (the minimum key in its right subtree).
- Replace the node with the successor.
- Delete the successor from the right subtree.
Finally, update the subtree counts along the path.
| |
Balanced Search Tree
While binary search trees (BSTs) offer efficient search capabilities, their performance degrades to $O(N)$ in the worst case when the tree becomes tall and skinny, resembling a linked list. Balanced search trees are data structures that guarantee logarithmic performance for all operations by maintaining a balanced structure.

2-3 Tree
A 2-3 tree is a specific type of B-tree that allows nodes to hold more than one key, thereby maintaining perfect balance.
- A 2-node contains one key and has two children.
- A 3-node contains two keys and has three children.
A 2-3 tree maintains two key properties:
- Symmetric order: An inorder traversal of the tree yields the keys in ascending order.
- Perfect balance: Every path from the root to a null link has the same length.

Search
Searching a 2-3 tree is similar to searching a BST. At each node, compare the search key with the key(s) in the node to determine which child link to follow.
Insertion
Insertion in a 2-3 tree preserves its balance properties.
- Inserting into a 2-node: If you insert a new key into a 2-node at the bottom of the tree, it simply becomes a 3-node.
- Inserting into a 3-node: This is more complex
- Temporarily add the new key to the 3-node, creating an illegal 4-node.
- Split the 4-node: move the middle key up to the parent node and split the remaining two keys into two separate 2-nodes.
- This process may cascade up the tree. If the root itself becomes a 4-node, it is split into three 2-nodes, which increases the height of the tree by one.
Directly implementing a 2-3 tree is cumbersome due to the need to manage different node types and handle numerous cases for splitting nodes. A red-black tree provides a more elegant way to implement a 2-3 tree.
Red-Black Tree
A red-black tree is a binary search tree that simulates a 2-3 tree by using “red” and “black” links. A 3-node is represented as two 2-nodes connected by a left-leaning red link.
The structure maintains the following properties:
- No node has two red links connected to it.
- Every path from the root to a null link has the same number of black links (this corresponds to the perfect balance of a 2-3 tree).
- Red links always lean left.

Representation
Each node stores a color attribute (RED or BLACK), which represents the color of the link pointing to it from its parent.
| |
Elementary Operations
The balance of a red-black tree is maintained during insertions and deletions using three elementary operations:
Left rotation: Converts a right-leaning red link to a left-leaning one.
1 2 3 4 5 6 7 8 9private Node rotateLeft(Node h) { assert isRed(h.right); Node x = h.right; h.right = x.left; x.left = h; x.color = h.color; h.color = RED; return x; }Right rotation: Converts a left-leaning red link to (temporarily) a right-leaning one (to solve two red links in a row).
1 2 3 4 5 6 7 8 9private Node rotateRight(Node h) { assert isRed(h.left); Node x = h.left; h.left = x.right; x.right = h; x.color = h.color; h.color = RED; return x; }Color flip: Splits a temporary 4-node (a black node with two red children) by passing the “redness” up the tree.
1 2 3 4 5 6 7 8private void flipColors(Node h) { assert !isRed(h); assert isRed(h.left); assert isRed(h.right); h.color = RED; h.left.color = BLACK; h.right.color = BLACK; }
Insertion
A new key is inserted just as in a standard BST, with the new link colored red. Then, the elementary operations are applied up the tree to restore the red-black properties.
| |
The search algorithm for a red-black tree is identical to that of a standard BST; the colors are ignored during a search.
B-Tree
A B-tree is a generalization of a 2-3 tree that is widely used for databases and file systems. It is optimized to minimize disk I/O operations by storing a large number of keys in each node, ensuring that each node fits within a single disk page.
A B-tree of order $M$ has the following properties:
- Each node contains up to $M-1$ key-link pairs.
- The root has at least 2 children (unless it is a leaf).
- All other internal nodes have at least $\lceil M/2 \rceil$ children.
- All leaves are at the same depth.
A search or an insertion in a B-tree of order $M$ with $N$ keys requires between $\log_{M} N$ and $\log_{M/2} N$ disk accesses (probes). In practice, with a large $M$ (e.g., $M=1024$), the height of the tree is extremely small—often no more than 4—making it exceptionally efficient for disk-based storage systems.
Geometric Applications of BST
Binary Search Trees and their variants can be powerfully applied to solve geometric search problems by mapping multidimensional data into a one-dimensional key structure.

1D Range Search
This problem extends the ordered symbol table API with two new operations for handling one-dimensional ranges.
Range count: Find the number of keys within a given range $[k_{1}, k_{2}]$. With a BST that supports the
rank()operation, this can be done efficiently.1 2 3 4public int size(Key lo, Key hi) { if (contains(hi)) return rank(hi) - rank(lo) + 1; else return rank(hi) - rank(lo); }Range Search: Find all keys that fall within a given range $[k_1, k_2]$. This is performed with a recursive search that intelligently prunes subtrees that cannot possibly contain keys within the query range.
Line Segment Intersection
A classic application of 1D range search is finding all intersection points among a set of horizontal and vertical line segments. The sweep line algorithm elegantly reduces this 2D geometric problem to a sequence of 1D range searches.
Imagine a vertical line “sweeping” across the plane from left to right. A BST is used to store the y-coordinates of all horizontal segments that the sweep line currently intersects.
- When the sweep line encounters the left endpoint of a horizontal segment, its y-coordinate is inserted into the BST.
- When it encounters the right endpoint of a horizontal segment, its y-coordinate is removed from the BST.
- When it encounters a vertical segment, we perform a range search on the BST to find all y-coordinates that fall within the vertical segment’s y-interval. Any y-coordinate found in the BST corresponds to an intersection point.
K-d Tree
K-d trees (K-dimensional trees) extend the concept of a symbol table to handle multidimensional keys, such as points in a plane (a 2-d tree).
Space-Partitioning Tree
K-d trees are a type of space-partitioning tree, which recursively divides a geometric space. Examples include:
- Grid: Divide space uniformly into squares.
- 2-d tree: Recursively divide space into two half-planes.
- Quadtree: Recursively divide space into four quadrants.
- BSP tree: Recursively divide space using arbitrary lines.

Grid Implementation
A simple approach to 2D search is to divide the space into a uniform grid of $M \times M$ squares.
- A 2D array is used to store a list of points for each grid square.
- Insert: To add a point $(x, y)$, it is added to the list corresponding to its grid square.
- Range search: To find points in a query rectangle, only the grid squares that intersect the rectangle need to be examined.
This method is fast and simple for uniformly distributed points but performs poorly for clustered data. If the grid is too fine, it wastes space; if it’s too coarse, the lists of points become too long. A reasonable choice for $N$ points is a $\sqrt{N} \times \sqrt{N}$ grid.
2-d Tree
A 2-d tree is a BST where the splitting axis alternates at each level of the tree.
This structure supports two key operations:
- Range search: To find all points within a query rectangle, the algorithm recursively searches the tree. It prunes any subtree whose corresponding rectangular region does not intersect the query rectangle.
- Nearest neighbor search: To find the point closest to a query point, the algorithm recursively searches the tree, prioritizing the subtree on the same side as the query point. It prunes any subtree if the distance from the query point to the subtree’s bounding box is greater than the distance to the nearest neighbor found so far.
Applications
- Ray tracing
- 2D range search
- Flight simulators
- N-body simulation
- Collision detection
- Astronomical databases
- Nearest neighbor search
- Adaptive mesh generation
- Hidden surface removal and shadow casting
Interval Search Tree
An interval search tree is a data structure designed to store 1D intervals and efficiently find all intervals that intersect a given query interval.
Non-degeneracy assumption: No two intervals share the same left endpoint.
The implementation uses a BST where the interval’s left endpoint serves as the key. In addition, each node is augmented to store the maximum endpoint of any interval in the subtree rooted at that node.
- Insertion: Insert an interval using its left endpoint as the key. Then, update the
maxendpoint value in each node along the search path. - Intersection search: To find an intersection for a query interval
(lo, hi):- If the interval in the current node intersects the query interval, a match is found.
- If the max endpoint in the left subtree is less than
lo, no intersection is possible in the left subtree, so go right. - Otherwise, go left.
Rectangle Intersection
The sweep line algorithm can be adapted to find all intersections among a set of axis-aligned rectangles. This 2D problem is reduced to a 1D interval search problem.
Non-degeneracy assumption: All x and y coordinates are distinct.
The sweep line moves from left to right, and an interval search tree stores the y-intervals of all rectangles currently intersecting the sweep line.
- When the sweep line hits a rectangle’s left edge, its y-interval is searched against the tree to find all intersections, and then its y-interval is inserted into the tree.
- When the sweep line hits a rectangle’s right edge, its y-interval is removed from the tree.
HW5: K-d Tree
This assignment involves writing a data type for a set of points in the unit square. It requires implementing a 2-d tree to support efficient range search and nearest neighbor search.
Two immutable data types are provided. Point2D represents points in the plane and RectHV represents axis-aligned rectangles.
- Brute-force implementation: Use red-black tree. PointSET.java
- 2-d tree implementation: KdTree.java
Hash Table
A hash table is a data structure that implements a symbol table by storing items in a key-indexed array. The array index for a given key is a function of the key itself. A complete implementation requires three components:
- Hash function: A function to compute an array index from a key.
- Equality test: A method to check if two keys are equal.
- Collision handling: A strategy to handle cases where different keys hash to the same index.
Hash Function
An ideal hash function should be easy to compute and distribute keys uniformly across the array indices.
A key’s journey from object to array index involves two steps:
- Hash Code: The
hashCode()method, which every Java object has, returns a 32-bit signedint. Implementations for some built-in types are shown below. - Hash Function: A function maps the hash code to an integer index between $0$ and $M-1$, where $M$ is the size of the array.
A common way to implement the mapping function is to remove the sign bit and then use the remainder operator:
| |
Java library implementations for primitive types:
| |
Collision Handling: Separate Chaining
To handle collisions, we can use an array of $M$ linked lists, where $M < N$.
- Hash: Map the key to an array index $i$.
- Insert: Add the key-value pair to the front of the linked list at index $i$, after checking for duplicates.
- Search: Traverse only the linked list at index $i$.
Typically, the array size is chosen to be $M \approx N/5$ to keep the average list length short.
| |
Collision Handling: Linear Probing
Linear probing is an open addressing method where colliding keys are placed in the next available slot in the array. This approach requires the array size $M$ to be greater than the number of keys $N$.
- Hash: Map the key to an array index $i$.
- Insert: If index $i$ is occupied, try $i+1$, then $i+2$, and so on (with wraparound) until an empty slot is found.
- Search: Starting at index $i$, search sequentially until the key is found or an empty slot is encountered, which indicates the key is not in the table.
To maintain good performance, the array size is typically chosen to be $M \approx 2N$, which keeps the table about half full. Under this condition, the average number of probes for a search hit is approximately 1.5.
| |