Lessons
1 · Foundations (Think Like a Programmer)
2 · Data Structures and Recursion
- 05Arrays and Array-Based Problem Solving
- 06Strings: Characters, Immutability, and Efficient Manipulation
- 07Linked Lists: Nodes, Pointers, and Pointer Reasoning
- 08Stacks and Queues
- 09Hashing: HashMap, HashSet, and Frequency Patterns
- 10Recursion and Backtracking
- 11Trees and Binary Search Trees
- 12Heaps and Priority Queues
3 · Core Algorithms and Reusable Patterns
4 · Advanced Interview Topics
Lesson 11 · Data Structures and Recursion
Trees and Binary Search Trees
Tree vocabulary, the four traversals (preorder, inorder, postorder, level-order), recursive and iterative code, and binary search trees: search, insert, and the right way to validate one.
For
Learners who understand recursion and linked lists and are ready for hierarchical data, which is a staple of coding interviews.
Level
Before you start
Articles 7, 8 and 10: linked lists, stacks and queues, and recursion. The call stack idea matters a lot here.
On this page
Key takeaways
- A binary tree node has a value and up to two children. Almost every tree problem is solved by handling null, then combining the answers from the left and right subtrees.
- Preorder visits the root first, inorder visits it between its subtrees, postorder visits it last, and level-order goes level by level using a queue.
- In a binary search tree, everything in the left subtree is smaller than the node and everything in the right is larger. Inorder traversal of a BST is sorted.
- BST search and insert take O(h) time, where h is the height. That is O(log n) when the tree is balanced and O(n) when it degenerates into a line.
- To validate a BST, pass down a valid range for each node. Comparing a node only with its direct children is wrong.
Your computer's folders form a tree. There is one top folder, it contains folders, those contain more folders and files, and so on. An organisation chart is a tree. So is a family tree, the structure of this very article (headings under headings), and the way a company's menu categories branch.
A tree is a hierarchy of nodes. It is the first structure in this course where each element can lead to several others, and it is the foundation for heaps, tries, graphs and much of dynamic programming. The good news: you already know the tools. A tree is recursive by nature (each part of a tree is itself a smaller tree), so the recursion from the previous article fits perfectly.
What you will learn
#- Tree vocabulary: root, leaf, height, depth, subtree.
- How to represent a binary tree in Java.
- Preorder, inorder, postorder and level-order traversal, recursive and iterative.
- How to compute height, size and leaf count.
- What a binary search tree is, and how to search, insert and validate one.
- When tree operations are fast, and when they are not.
Tree vocabulary
# 1 <- root (no parent) / \ 2 3 <- 2 and 3 are siblings, children of 1 / \ \ 4 5 6 <- 4, 5, 6 are leaves (no children)Term | Meaning | In the picture |
|---|---|---|
Node | One element of the tree | Each number |
Edge | A link from a parent to a child | The lines |
Root | The top node, the only one with no parent |
|
Parent / child | A node and the nodes directly below it |
|
Leaf | A node with no children |
|
Subtree | A node together with everything below it | The subtree rooted at |
Depth of a node | Number of edges from the root to the node | Depth of |
Height of a node | Number of edges on the longest path down to a leaf | Height of |
Height of the tree | Height of the root | 2 |
Level | All nodes at the same depth | Level 2 is |
A binary tree is a tree in which every node has at most two children, called the left child and the right child.
Representing a binary tree in Java
#static class Node { int val; Node left; // null means "no left child" Node right; Node(int val) { this.val = val; }}You keep a reference to the root. An empty tree is root == null. A leaf has both left and right equal to null.
The recursive pattern for trees
#A tree is either empty, or a root with a left subtree and a right subtree, and each subtree is itself a tree. So most tree methods have this shape:
solve(node): if node is null: return the answer for an empty tree // base case leftAnswer = solve(node.left) rightAnswer = solve(node.right) return combine(node.val, leftAnswer, rightAnswer)By the leap of faith from the recursion article, you assume solve works on the two smaller subtrees, and just combine. For the height, the combination is "1 plus the larger of the two heights". For the size, it is "1 plus the sizes of both". We will write both.
Traversals
#To traverse a tree is to visit every node. There are four standard orders. The first three are depth-first: they go down a branch as far as possible before moving on. They differ only in when the node itself is visited relative to its subtrees.
Traversal | Order | Typical use |
|---|---|---|
Preorder | node, left, right | Copying a tree, serialising it, printing a hierarchy |
Inorder | left, node, right | Reading a BST in sorted order |
Postorder | left, right, node | Computing something from the children first (size, height, deleting a tree) |
Level-order | level by level, left to right | Shortest distance from the root, printing by level |
For the tree above (1 with children 2 and 3, 2 with children 4 and 5, 3 with a right child 6):
- Preorder:
1 2 4 5 3 6 - Inorder:
4 2 5 1 3 6 - Postorder:
4 5 2 6 3 1 - Level-order:
1 | 2 3 | 4 5 6
Why do the orders differ? In preorder we say "hello" to the node on the way down. In inorder we say it after finishing the whole left side. In postorder we say it on the way back up, after both sides are done. It is the same walk through the tree, and only the moment of the add call changes.
Level-order uses a queue: take a node from the front, visit it, and add its children to the back. Because children are added behind everything already waiting, nodes are processed level by level. To report each level separately, process exactly queue.size() nodes at the start of each round: those are the nodes of the current level.
// File: Main.javaimport java.util.ArrayDeque;import java.util.ArrayList;import java.util.List;import java.util.Queue;public class Main { static class Node { int val; Node left; Node right; Node(int val) { this.val = val; } } static void preorder(Node node, List<Integer> out) { if (node == null) { return; } out.add(node.val); // visit before the subtrees preorder(node.left, out); preorder(node.right, out); } static void inorder(Node node, List<Integer> out) { if (node == null) { return; } inorder(node.left, out); out.add(node.val); // visit between the subtrees inorder(node.right, out); } static void postorder(Node node, List<Integer> out) { if (node == null) { return; } postorder(node.left, out); postorder(node.right, out); out.add(node.val); // visit after the subtrees } static List<List<Integer>> levelOrder(Node root) { List<List<Integer>> levels = new ArrayList<>(); if (root == null) { return levels; } Queue<Node> queue = new ArrayDeque<>(); queue.offer(root); while (!queue.isEmpty()) { int count = queue.size(); // how many nodes are on this level List<Integer> level = new ArrayList<>(); for (int i = 0; i < count; i++) { Node node = queue.poll(); level.add(node.val); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } levels.add(level); } return levels; } static int height(Node node) { // in edges: empty tree is -1, a leaf is 0 if (node == null) { return -1; } return 1 + Math.max(height(node.left), height(node.right)); } static int size(Node node) { if (node == null) { return 0; } return 1 + size(node.left) + size(node.right); } static int countLeaves(Node node) { if (node == null) { return 0; } if (node.left == null && node.right == null) { return 1; } return countLeaves(node.left) + countLeaves(node.right); } public static void main(String[] args) { Node root = new Node(1); root.left = new Node(2); root.right = new Node(3); root.left.left = new Node(4); root.left.right = new Node(5); root.right.right = new Node(6); List<Integer> pre = new ArrayList<>(); List<Integer> in = new ArrayList<>(); List<Integer> post = new ArrayList<>(); preorder(root, pre); inorder(root, in); postorder(root, post); System.out.println("preorder: " + pre); System.out.println("inorder: " + in); System.out.println("postorder: " + post); System.out.println("level-order: " + levelOrder(root)); System.out.println("height=" + height(root) + " size=" + size(root) + " leaves=" + countLeaves(root)); System.out.println("empty tree: height=" + height(null) + " size=" + size(null) + " levels=" + levelOrder(null)); }}preorder: [1, 2, 4, 5, 3, 6]inorder: [4, 2, 5, 1, 3, 6]postorder: [4, 5, 2, 6, 3, 1]level-order: [[1], [2, 3], [4, 5, 6]]height=2 size=6 leaves=3empty tree: height=-1 size=0 levels=[]Compare the printed lists with the ones we worked out by hand. Matching them is a good test of your understanding.
Complexity of traversals and tree recursion
#Each node is visited once and does constant work, so the time is O(n) for n nodes. The extra space is the recursion stack, which is as deep as the height h of the tree: O(h). For a well-balanced tree h is about log2(n). For a tree that is one long chain, h is n - 1, so the stack can reach O(n), and a very deep tree can overflow Java's stack. Level-order uses a queue that can hold up to one whole level, which for a complete tree is about n / 2 nodes, so O(n) space in the worst case.
Iterative inorder traversal
#Recursion uses the call stack for you. You can do the same walk with your own stack. This matters when the tree could be very deep, or when an interviewer asks "can you do it without recursion?".
The idea: keep going left, remembering every node you pass on a stack. When you can go no further left, pop a node, visit it, and then move to its right child and repeat.
// File: Main.javaimport java.util.ArrayDeque;import java.util.ArrayList;import java.util.Deque;import java.util.List;public class Main { static class Node { int val; Node left; Node right; Node(int val) { this.val = val; } } static List<Integer> inorderIterative(Node root) { List<Integer> out = new ArrayList<>(); Deque<Node> stack = new ArrayDeque<>(); Node cur = root; while (cur != null || !stack.isEmpty()) { while (cur != null) { // go as far left as possible, remembering the path stack.push(cur); cur = cur.left; } cur = stack.pop(); // the most recent node with nothing further left out.add(cur.val); // visit it cur = cur.right; // then handle its right subtree } return out; } static List<Integer> preorderIterative(Node root) { List<Integer> out = new ArrayList<>(); if (root == null) { return out; } Deque<Node> stack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { Node node = stack.pop(); out.add(node.val); if (node.right != null) { // push right first so that left is popped first stack.push(node.right); } if (node.left != null) { stack.push(node.left); } } return out; } public static void main(String[] args) { Node root = new Node(1); root.left = new Node(2); root.right = new Node(3); root.left.left = new Node(4); root.left.right = new Node(5); root.right.right = new Node(6); System.out.println(inorderIterative(root)); System.out.println(preorderIterative(root)); System.out.println(inorderIterative(null)); }}[4, 2, 5, 1, 3, 6][1, 2, 4, 5, 3, 6][]Dry run of the iterative inorder on our tree:
Action |
| Stack (top first) | Output so far |
|---|---|---|---|
Go left from |
|
| |
Pop and visit |
|
|
|
Pop and visit |
|
|
|
Go left from |
|
|
|
Pop and visit |
|
|
|
Pop and visit |
| empty |
|
Go left from |
|
|
|
Pop and visit |
| empty |
|
Go left from |
| empty |
|
The loop ends because cur is null and the stack is empty. Time is O(n) and the stack holds at most h nodes.
Binary search trees
#A binary search tree (BST) is a binary tree with an ordering rule:
For every node, all values in its left subtree are smaller than the node's value, and all values in its right subtree are larger.
(We assume no duplicates here. Real implementations must choose a rule for duplicates, such as ignoring them or counting them.)
8 / \ 3 10 / \ \ 1 6 14 / \ / 4 7 13Notice that this rule lets us discard half of the remaining tree at every step. Look for 7: compare with 8. 7 < 8, so go left, and the whole right subtree (10, 14, 13) is irrelevant. At 3, 7 > 3, so go right, and 1 is irrelevant. At 6, 7 > 6, go right: found. This is binary search, but on a pointer structure.
Search
#search(node, target): if node is null: not found if target == node.val: found if target < node.val: search(node.left, target) else: search(node.right, target)Because each step goes down one level, the cost is at most the height of the tree: O(h).
Insert
#To insert a value, search for it. When you fall off the tree (reach null), that empty spot is exactly where the new node belongs. A neat recursive way: insert returns the root of the (possibly updated) subtree, and each caller stores the result back into its child pointer.
Inorder gives sorted order
#Inorder visits left, node, right. In a BST, everything on the left is smaller and everything on the right is larger, so inorder visits the values in increasing order. This is a very useful fact: it turns "kth smallest", "validate a BST" and "convert to a sorted list" into inorder problems.
Deleting a node (the idea)
#Deletion has three cases. A leaf can simply be removed. A node with one child is replaced by that child. A node with two children is replaced by its inorder successor (the smallest node in its right subtree), or its predecessor, and then that successor node is removed from where it was. We do not code it here, but you should be able to explain it.
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { static class Node { int val; Node left; Node right; Node(int val) { this.val = val; } } static Node insert(Node node, int value) { if (node == null) { return new Node(value); // found the empty spot } if (value < node.val) { node.left = insert(node.left, value); // store the result back } else if (value > node.val) { node.right = insert(node.right, value); } // equal: ignore the duplicate return node; } static boolean contains(Node root, int target) { Node cur = root; while (cur != null) { if (target == cur.val) { return true; } cur = target < cur.val ? cur.left : cur.right; } return false; } static void inorder(Node node, List<Integer> out) { if (node == null) { return; } inorder(node.left, out); out.add(node.val); inorder(node.right, out); } static int height(Node node) { return node == null ? -1 : 1 + Math.max(height(node.left), height(node.right)); } // Wrong idea: only compares each node with its direct children. static boolean looksValidLocally(Node node) { if (node == null) { return true; } if (node.left != null && node.left.val >= node.val) { return false; } if (node.right != null && node.right.val <= node.val) { return false; } return looksValidLocally(node.left) && looksValidLocally(node.right); } // Right idea: every node must lie strictly between the bounds inherited from its ancestors. static boolean isValidBst(Node node, long low, long high) { if (node == null) { return true; } if (node.val <= low || node.val >= high) { return false; } return isValidBst(node.left, low, node.val) && isValidBst(node.right, node.val, high); } static boolean isValidBst(Node root) { return isValidBst(root, Long.MIN_VALUE, Long.MAX_VALUE); } static Node buildFrom(int... values) { Node root = null; for (int v : values) { root = insert(root, v); } return root; } public static void main(String[] args) { Node bst = buildFrom(8, 3, 10, 1, 6, 14, 4, 7, 13); List<Integer> sorted = new ArrayList<>(); inorder(bst, sorted); System.out.println(sorted); System.out.println(contains(bst, 6) + " " + contains(bst, 5) + " " + height(bst)); System.out.println(isValidBst(bst)); Node bad = new Node(5); // 5 with left child 3, and 3 has a right child 6 bad.left = new Node(3); bad.left.right = new Node(6); System.out.println(looksValidLocally(bad) + " " + isValidBst(bad)); Node sortedInsert = buildFrom(1, 2, 3, 4, 5, 6, 7); Node balancedInsert = buildFrom(4, 2, 6, 1, 3, 5, 7); System.out.println("height after sorted inserts: " + height(sortedInsert) + ", after balanced order: " + height(balancedInsert)); Node extremes = buildFrom(0, Integer.MIN_VALUE, Integer.MAX_VALUE); System.out.println(isValidBst(extremes)); }}[1, 3, 4, 6, 7, 8, 10, 13, 14]true false 3truetrue falseheight after sorted inserts: 6, after balanced order: 2trueWhy validation needs ranges
#The tree bad above has root 5, left child 3, and 3 has a right child 6. Look at each node and its direct children: 3 < 5 is fine; 6 > 3 is fine. A local check says "valid". But 6 is in the left subtree of 5, so it must be smaller than 5. It is not, so the tree is not a BST.
The correct check carries a range down the recursion. The root may be anything. Going left, the node must be below the parent's value (the upper bound shrinks to the parent). Going right, it must be above the parent's value (the lower bound rises). Every node must be strictly inside the range inherited from all of its ancestors.
Dry run on bad:
Node | Allowed range (low, high) | Value | Inside? |
|---|---|---|---|
| (-infinity, +infinity) | 5 | yes |
| (-infinity, 5) | 3 | yes |
| (3, 5) | 6 | no, 6 is not below 5 |
The check fails exactly where the local check could not see. We use long bounds so that the values Integer.MIN_VALUE and Integer.MAX_VALUE are still valid (the last test above), which a bound of type int could not do cleanly. An alternative way to validate is to run an inorder traversal and check that the output is strictly increasing.
Shape and speed
#The cost of search and insert is O(h), where h is the height. That is the whole story, and the height depends on the order of insertion. Inserting 1, 2, 3, 4, 5, 6, 7 in sorted order makes each new node the right child of the previous one: the tree becomes a chain, h = n - 1, and operations cost O(n). Inserting the same values as 4, 2, 6, 1, 3, 5, 7 gives a perfectly balanced tree with h = 2. The program printed 6 and 2.
The cure is a self-balancing BST (such as an AVL tree or a red-black tree) which rearranges itself after insertions and deletions to keep the height near log2(n). You will not usually implement one. Java's TreeMap and TreeSet are red-black trees, which is why their operations are O(log n).
Tree | Height with | Search / insert |
|---|---|---|
Balanced | about | O(log n) |
Skewed (a chain) |
| O(n) |
Self-balancing ( | kept near | O(log n) guaranteed |
Complexity summary
#Task | Time | Extra space |
|---|---|---|
Any traversal (recursive) | O(n) | O(h) call stack |
Iterative inorder or preorder | O(n) | O(h) explicit stack |
Level-order | O(n) | O(w), where |
Height, size, leaf count | O(n) | O(h) |
BST search, insert | O(h): O(log n) balanced, O(n) skewed | O(1) iterative, O(h) recursive |
BST validation with ranges | O(n) | O(h) |
Edge cases and common mistakes
#- Forgetting the
nullbase case. Every tree method must handlenull. It is also the empty tree. - Confusing height in edges with height in nodes. State your convention.
- Dropping the result of a recursive insert.
insert(node.left, v)alone loses the new node; assignnode.left = insert(node.left, v). - Validating a BST with only local comparisons. Use ranges, or check that an inorder traversal is increasing.
- Using
intbounds.Integer.MIN_VALUEandInteger.MAX_VALUEare legal values, so uselongorIntegerobjects that can benull. - Assuming a BST is balanced. A sorted input produces a chain.
- Duplicates. Decide whether duplicates are rejected, go left, go right, or are counted.
- Recursion depth on a skewed tree. A chain of 100,000 nodes can overflow the stack. Use an iterative version.
- Mixing up the traversals. Remember: pre means the node comes before its subtrees, post means after, and in means between.
Exercises
#Exercise 1
Trace
#For this tree, write the preorder, inorder, postorder and level-order traversals.
7 / \ 3 9 / / \ 1 8 10Is it a valid BST? What do you notice about its inorder traversal?
Exercise 2: Invert a binary tree
#Swap the left and right children of every node, so that the tree becomes its own mirror image. Show the level-order before and after for the tree with root 4, children 2 and 7, and grandchildren 1, 3, 6, 9.
Exercise 3: Lowest common ancestor in a BST
#Given a BST and two values p and q that are both in the tree, find their lowest common ancestor: the deepest node that has both of them in its subtree (a node counts as its own descendant). Use the BST ordering to do it in O(h) time and O(1) extra space.
Exercise 4: Debug
#This insertion method is called with 5, 3, 8, 1, 4. Find out what the tree looks like afterwards, why, and fix it.
static Node insertBuggy(Node root, int v) { if (root == null) { return new Node(v); } if (v < root.val) { insertBuggy(root.left, v); } else { insertBuggy(root.right, v); } return root;}Exercise 5: Interview-style, the k-th smallest element
#Given the root of a BST and k (counting from 1), return the k-th smallest value, or report that there is none if k is larger than the number of nodes. Stop as soon as you have the answer.
Show solutionHide solution
- Preorder (node, left, right):
7 3 1 9 8 10 - Inorder (left, node, right):
1 3 7 8 9 10 - Postorder (left, right, node):
1 3 8 10 9 7 - Level-order:
7 | 3 9 | 1 8 10
It is a valid BST: for 7, the left subtree values 3, 1 are below it and the right subtree values 9, 8, 10 are above it; for 9, the left 8 is below and the right 10 is above. The inorder traversal is sorted, as it always is for a BST.
// File: Main.javaimport java.util.ArrayDeque;import java.util.ArrayList;import java.util.List;import java.util.Queue;public class Main { static class Node { int val; Node left; Node right; Node(int val, Node left, Node right) { this.val = val; this.left = left; this.right = right; } } static void pre(Node n, List<Integer> out) { if (n == null) { return; } out.add(n.val); pre(n.left, out); pre(n.right, out); } static void in(Node n, List<Integer> out) { if (n == null) { return; } in(n.left, out); out.add(n.val); in(n.right, out); } static void post(Node n, List<Integer> out) { if (n == null) { return; } post(n.left, out); post(n.right, out); out.add(n.val); } static List<Integer> level(Node root) { List<Integer> out = new ArrayList<>(); Queue<Node> queue = new ArrayDeque<>(); queue.offer(root); while (!queue.isEmpty()) { Node n = queue.poll(); out.add(n.val); if (n.left != null) { queue.offer(n.left); } if (n.right != null) { queue.offer(n.right); } } return out; } public static void main(String[] args) { Node root = new Node(7, new Node(3, new Node(1, null, null), null), new Node(9, new Node(8, null, null), new Node(10, null, null))); List<Integer> a = new ArrayList<>(); List<Integer> b = new ArrayList<>(); List<Integer> c = new ArrayList<>(); pre(root, a); in(root, b); post(root, c); System.out.println(a + " " + b + " " + c + " " + level(root)); }}[7, 3, 1, 9, 8, 10] [1, 3, 7, 8, 9, 10] [1, 3, 8, 10, 9, 7] [7, 3, 9, 1, 8, 10]Exercise 2
Show solutionHide solution
Recursively: invert the left subtree, invert the right subtree, then swap the two children. The base case is null. Time O(n), stack space O(h). The order of the swap and the recursive calls does not matter, as long as every node swaps its children exactly once.
// File: Main.javaimport java.util.ArrayDeque;import java.util.ArrayList;import java.util.List;import java.util.Queue;public class Main { static class Node { int val; Node left; Node right; Node(int val, Node left, Node right) { this.val = val; this.left = left; this.right = right; } } static Node invert(Node node) { if (node == null) { return null; } Node leftInverted = invert(node.left); Node rightInverted = invert(node.right); node.left = rightInverted; // swap node.right = leftInverted; return node; } static List<List<Integer>> levels(Node root) { List<List<Integer>> result = new ArrayList<>(); if (root == null) { return result; } Queue<Node> queue = new ArrayDeque<>(); queue.offer(root); while (!queue.isEmpty()) { int count = queue.size(); List<Integer> level = new ArrayList<>(); for (int i = 0; i < count; i++) { Node n = queue.poll(); level.add(n.val); if (n.left != null) { queue.offer(n.left); } if (n.right != null) { queue.offer(n.right); } } result.add(level); } return result; } public static void main(String[] args) { Node root = new Node(4, new Node(2, new Node(1, null, null), new Node(3, null, null)), new Node(7, new Node(6, null, null), new Node(9, null, null))); System.out.println(levels(root)); invert(root); System.out.println(levels(root)); System.out.println(invert(null)); }}[[4], [2, 7], [1, 3, 6, 9]][[4], [7, 2], [9, 6, 3, 1]]nullExercise 3
Show solutionHide solution
Start at the root. If both p and q are smaller than the current node, the answer must be in the left subtree, so go left. If both are larger, go right. Otherwise, they are on different sides of the current node (or one of them equals it), so this node is where their paths split, and it is the lowest common ancestor.
Each step goes down one level, so the time is O(h), and a loop uses O(1) space.
// File: Main.javapublic class Main { static class Node { int val; Node left; Node right; Node(int val) { this.val = val; } } static Node insert(Node node, int value) { if (node == null) { return new Node(value); } if (value < node.val) { node.left = insert(node.left, value); } else if (value > node.val) { node.right = insert(node.right, value); } return node; } static int lowestCommonAncestor(Node root, int p, int q) { Node cur = root; while (cur != null) { if (p < cur.val && q < cur.val) { cur = cur.left; } else if (p > cur.val && q > cur.val) { cur = cur.right; } else { return cur.val; } } throw new IllegalArgumentException("empty tree"); } public static void main(String[] args) { Node root = null; for (int v : new int[] {6, 2, 8, 0, 4, 7, 9, 3, 5}) { root = insert(root, v); } System.out.println(lowestCommonAncestor(root, 2, 8)); System.out.println(lowestCommonAncestor(root, 2, 4)); System.out.println(lowestCommonAncestor(root, 3, 5)); System.out.println(lowestCommonAncestor(root, 7, 9)); System.out.println(lowestCommonAncestor(root, 5, 5)); }}62485For 3 and 5: both are below 6, so go left to 2. Both are above 2, so go right to 4. Now 3 < 4 < 5, so they split, and the answer is 4. For a node and itself (5, 5), the search reaches 5 and then stops, because neither condition holds there.
Exercise 4
Show solutionHide solution
The bug is that the result of the recursive call is ignored. When root.left is null, insertBuggy(root.left, v) creates a new node, returns it, and the caller throws it away. The new node is never attached to the tree. So after inserting 5, 3, 8, 1, 4, the tree contains only the root 5.
The fix is to store the returned node: root.left = insert(root.left, v) and root.right = insert(root.right, v).
// File: Main.javapublic class Main { static class Node { int val; Node left; Node right; Node(int val) { this.val = val; } } static Node insertBuggy(Node root, int v) { if (root == null) { return new Node(v); } if (v < root.val) { insertBuggy(root.left, v); } else { insertBuggy(root.right, v); } return root; } static Node insertFixed(Node root, int v) { if (root == null) { return new Node(v); } if (v < root.val) { root.left = insertFixed(root.left, v); } else { root.right = insertFixed(root.right, v); } return root; } static int size(Node n) { return n == null ? 0 : 1 + size(n.left) + size(n.right); } public static void main(String[] args) { Node buggy = null; Node fixed = null; for (int v : new int[] {5, 3, 8, 1, 4}) { buggy = insertBuggy(buggy, v); fixed = insertFixed(fixed, v); } System.out.println("buggy size " + size(buggy) + ", fixed size " + size(fixed)); }}buggy size 1, fixed size 5Exercise 5
Show solutionHide solution
Inorder traversal visits a BST in increasing order, so the k-th node visited is the k-th smallest. Use the iterative inorder from the article, count visited nodes, and return as soon as the count reaches k. We never walk the whole tree.
Time is O(h + k): h steps to reach the leftmost node, then k visits. Extra space is O(h) for the stack. If k is larger than the number of nodes, the loop ends without a result, and we return null.
// File: Main.javaimport java.util.ArrayDeque;import java.util.Deque;public class Main { static class Node { int val; Node left; Node right; Node(int val) { this.val = val; } } static Node insert(Node node, int value) { if (node == null) { return new Node(value); } if (value < node.val) { node.left = insert(node.left, value); } else if (value > node.val) { node.right = insert(node.right, value); } return node; } static Integer kthSmallest(Node root, int k) { Deque<Node> stack = new ArrayDeque<>(); Node cur = root; int seen = 0; while (cur != null || !stack.isEmpty()) { while (cur != null) { stack.push(cur); cur = cur.left; } cur = stack.pop(); seen++; if (seen == k) { return cur.val; // stop early } cur = cur.right; } return null; // fewer than k nodes } public static void main(String[] args) { Node root = null; for (int v : new int[] {5, 3, 6, 2, 4, 1}) { root = insert(root, v); } System.out.println(kthSmallest(root, 1) + " " + kthSmallest(root, 3) + " " + kthSmallest(root, 6) + " " + kthSmallest(root, 7)); System.out.println(kthSmallest(null, 1)); }}1 3 6 nullnullWhere to go next
#- Next: Heaps and Priority Queues use a tree shape stored in an array, to always find the smallest or largest element quickly. We also explain how a heap differs from a BST.
- Later: a tree is a graph with no cycles. Graphs: Representations, BFS, and DFS generalises the level-order and depth-first ideas you just learned. Tries are trees for strings.
- Practice habit: before you code any tree problem, ask three questions: what is the answer for
null, what do I need from the left and right subtrees, and how do I combine them?
Connected lessons
Linked from 2 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
How to Learn Data Structures and Algorithms Without Burning Out
Guide · Career
The OA Landscape: Platforms, Formats, Proctoring and Rules
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

