Algorithms and Data Structures · Chapter 7
Trees and heaps
Lists, stacks and queues arrange data in a row. A tree branches: every node can have several successors. In a well-built search tree you find a value in just a few steps, and a heap hands you the smallest element right away. Almost every method on trees is recursive, because every part of a tree is itself a tree.
Terms
In a binary tree, every node has at most two children, a left one and a right one. In the tasks, a node looks like this:
class TreeNode { int value; TreeNode left; TreeNode right;}
- The root is the top node, the only one without a parent. Through it you reach the whole tree.
- A leaf has no children:
leftandrightarenull. All other nodes are called inner nodes. - A subtree is a node with everything hanging below it. So the left subtree of the root is a tree in its own right.
- Here, the height counts the levels: a single node has height 1, an empty tree height 0. Some books count the edges instead, then everything is 1 smaller.
Recursion and traversal
A method on a tree almost always follows the same pattern: the base case is the empty tree (node == null), otherwise you process the node and call yourself for node.left and node.right. Every node is visited exactly once, which costs O(n).
Visiting all nodes is called traversing the tree. The three usual orders differ only in when the node itself is handled:
- Preorder: node, left, right. The root comes first.
- Inorder: left, node, right. The root is in the middle.
- Postorder: left, right, node. The root comes last.
public class Main { static int countNodes(TreeNode node) { if (node == null) { return 0; } return 1 + countNodes(node.left) + countNodes(node.right); }
static int height(TreeNode node) { if (node == null) { return 0; } return 1 + Math.max(height(node.left), height(node.right)); }
static void preorder(TreeNode node) { if (node == null) { return; } System.out.print(node.value + " "); preorder(node.left); preorder(node.right); }
static void inorder(TreeNode node) { if (node == null) { return; } inorder(node.left); System.out.print(node.value + " "); inorder(node.right); }
static void postorder(TreeNode node) { if (node == null) { return; } postorder(node.left); postorder(node.right); System.out.print(node.value + " "); }
public static void main(String[] args) { // 1 // / \ // 2 3 // / \ // 4 5 TreeNode root = new TreeNode(1, new TreeNode(2, new TreeNode(4), new TreeNode(5)), new TreeNode(3)); System.out.println("Nodes: " + countNodes(root)); System.out.println("Height: " + height(root)); System.out.print("Preorder: "); preorder(root); System.out.println(); System.out.print("Inorder: "); inorder(root); System.out.println(); System.out.print("Postorder: "); postorder(root); System.out.println(); }}
class TreeNode { int value; TreeNode left; TreeNode right;
TreeNode(int value) { this.value = value; }
TreeNode(int value, TreeNode left, TreeNode right) { this.value = value; this.left = left; this.right = right; }}For the tree in the example, this gives 1 2 4 5 3, 4 2 5 1 3 and 4 5 2 3 1. In an exam it helps to draw the tree and tick off every node when you visit it.
Binary search trees
A binary search tree keeps the values in order: for every node, all values in the left subtree are smaller, all values in the right subtree are larger. When searching, you compare with the node and go left or right, just like binary search in an array. A new value is inserted where the search hits null.
- The smallest value is at the very bottom left, the largest at the very bottom right.
- Inorder gives the values sorted.
- Searching and inserting take as many steps as the tree is high. If it is balanced, that is about log₂ n, so O(log n): 1,000,000 values in around 20 levels.
- If you insert values that are already sorted (1, 2, 3, …), every new node hangs to the right of the previous one. The tree degenerates into a list, and searching then costs O(n). Self-balancing trees (AVL, red-black, in Java
TreeMap) prevent this.
public class Main { static TreeNode insert(TreeNode root, int value) { if (root == null) { return new TreeNode(value); } if (value < root.value) { root.left = insert(root.left, value); } else if (value > root.value) { root.right = insert(root.right, value); } return root; }
static boolean contains(TreeNode root, int value) { TreeNode node = root; while (node != null) { if (value == node.value) { return true; } node = value < node.value ? node.left : node.right; } return false; }
static int min(TreeNode root) { TreeNode node = root; while (node.left != null) { node = node.left; } return node.value; }
static void inorder(TreeNode node) { if (node == null) { return; } inorder(node.left); System.out.print(node.value + " "); inorder(node.right); }
public static void main(String[] args) { TreeNode root = null; for (int v : new int[] {50, 30, 70, 20, 40, 60, 80}) { root = insert(root, v); } System.out.println("contains 40? " + contains(root, 40)); System.out.println("contains 45? " + contains(root, 45)); System.out.println("min: " + min(root)); System.out.print("sorted: "); inorder(root); System.out.println(); }}
class TreeNode { int value; TreeNode left; TreeNode right;
TreeNode(int value) { this.value = value; }}Heaps in an array
A min-heap is a binary tree with the heap property: every node is smaller than or equal to its children. So the minimum is always at the root. Unlike in a search tree, siblings are not ordered among each other.
A heap is always complete, filled level by level from the left. That is why it fits into an array without gaps:
- root at index 0
- children of
i:2 * i + 1and2 * i + 2 - parent of
i:(i - 1) / 2(integer division)
Insert: append at the end and let it sift up as long as the value is smaller than its parent. Remove the minimum: put the last element at index 0 and let it sift down, always swapping with the smaller child. Both run at most once from top to bottom through the tree: O(log n). Java offers this ready-made as PriorityQueue.
import java.util.Arrays;
public class Main { static int[] data = new int[10]; static int size = 0;
static void insert(int value) { data[size] = value; int i = size; size++; // sift up: swap with the parent while smaller while (i > 0 && data[i] < data[(i - 1) / 2]) { swap(i, (i - 1) / 2); i = (i - 1) / 2; } }
static int poll() { int min = data[0]; size--; data[0] = data[size]; int i = 0; // sift down: swap with the smaller child while larger while (2 * i + 1 < size) { int child = 2 * i + 1; if (child + 1 < size && data[child + 1] < data[child]) { child++; } if (data[i] <= data[child]) { break; } swap(i, child); i = child; } return min; }
static void swap(int i, int j) { int tmp = data[i]; data[i] = data[j]; data[j] = tmp; }
public static void main(String[] args) { for (int v : new int[] {5, 3, 8, 1, 4}) { insert(v); System.out.println("insert " + v + ": " + Arrays.toString(Arrays.copyOf(data, size))); } System.out.println("poll: " + poll()); System.out.println("heap: " + Arrays.toString(Arrays.copyOf(data, size))); }}Heapsort
Heapsort sorts with a max-heap directly in the array:
- Build the heap: from
a.length / 2 - 1down to 0, let every inner node sift down. After that, the maximum is at index 0. - Swap the maximum with the last element of the heap. It is now at its final place, and the heap is one element shorter. Let the new root sift down and repeat.
Each sift-down costs O(log n), and it happens n times: O(n log n), even in the worst case. Unlike quicksort, heapsort needs no extra memory and has no bad case with O(n²). On the other hand it is not stable: equal values can swap their order.
import java.util.Arrays;
public class Main { static void heapSort(int[] a) { // 1. build a max-heap: let every inner node sink, from back to front for (int i = a.length / 2 - 1; i >= 0; i--) { siftDown(a, i, a.length); } // 2. move the maximum to the end, shrink the heap, repair it for (int end = a.length - 1; end > 0; end--) { swap(a, 0, end); siftDown(a, 0, end); } }
static void siftDown(int[] a, int i, int size) { while (2 * i + 1 < size) { int child = 2 * i + 1; if (child + 1 < size && a[child + 1] > a[child]) { child++; } if (a[i] >= a[child]) { return; } swap(a, i, child); i = child; } }
static void swap(int[] a, int i, int j) { int tmp = a[i]; a[i] = a[j]; a[j] = tmp; }
public static void main(String[] args) { int[] a = {4, 10, 3, 5, 1}; heapSort(a); System.out.println(Arrays.toString(a)); }}Common mistakes
Recursive call not hooked in
insert(root.left, value);on its own does create a new node, but nobody keeps it. Correct isroot.left = insert(root.left, value);. The same goes for the first insert into an empty tree:root = insert(root, 5);, otherwiserootstaysnull.Search tree checked only against the children
It is not enough that every left child is smaller and every right child is larger. In the tree with root 8, left child 3 and its right child 9, every pair fits, yet the 9 is in the left subtree of the 8. Check every node against a lower and an upper bound that you pass down on the way.
Wrong indices in the heap
With the root at index 0, the children are
2 * i + 1and2 * i + 2. The formulas2 * iand2 * i + 1only apply if the root is at index 1. And when sifting down, a child only exists if its index is smaller thansize, not smaller thandata.length: beyond that there are old values.