Kth Smallest Element in a BST
An in-order walk of a BST visits the values in sorted order, so the k-th node it visits is the answer. Solved two ways, a full in-order list in O(n) and an in-order walk with a stack that stops at k in O(h + k), with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given the root of a binary search tree (BST) and a number rank. In a BST, every value in the
left subtree of a node is smaller than the node, and every value in the right subtree is larger.
Return the rank-th smallest value in the tree. rank counts from 1, so rank = 1 asks for the smallest value.
Trees in the examples are written level by level, from left to right. null marks a missing child.
Example 1
Input: root = [7, 3, 10, 1, 5, null, 12, null, 2], rank = 3
Output: 3
Sorted, the values are 1, 2, 3, 5, 7, 10, 12. The 3rd one is 3.
Example 2
Input: root = [4, 2, 6], rank = 3
Output: 6
Sorted: 2, 4, 6. The 3rd one is the largest, 6.
Example 3
Input: root = [1], rank = 1
Output: 1
The only value is also the smallest.
Constraints
Section titled “Constraints”- The tree has n nodes. Call
rankk, with 1 ≤ k ≤ n ≤ 10,000. - 0 ≤ Node.val ≤ 10,000
Clarifying questions
Section titled “Clarifying questions”- Does rank count from 0 or from 1?
- From 1. rank = 1 means the smallest value.
- Is rank always valid?
- Yes. 1 ≤ rank ≤ n, so the answer always exists.
- Can values repeat?
- No. In a BST here, left is strictly smaller and right strictly larger.
- Is the tree balanced?
- Not always. It can be a chain, so its height h can be as large as n.
Spot the pattern
Section titled “Spot the pattern”The tree is a BST, and “k-th smallest” is about sorted order. An in-order traversal (left subtree, then the node, then the right subtree) of a BST visits the values in sorted order. So the k-th node it visits is the answer, and the walk can stop right there.
Solutions
Section titled “Solutions”This problem has two approaches. There is no useful step between them.
Brute force O(n)
The idea
Section titled “The idea”Walk the whole tree in order and write every value into a list. Because the tree is a BST, the list comes out sorted. Then return the value at position k − 1, since lists count from 0.
It is short and correct. But it visits every node and stores every value, even when k is 1 and the answer is the very first value written.
Walkthrough
Section titled “Walkthrough”- def inorder(node, values):
- if node is None: return
- inorder(node.left, values)
- values.append(node.val)
- inorder(node.right, values)
- values = []
- inorder(root, values)
- return values[rank - 1]
In a BST, an in-order walk (left subtree, then the node, then the right subtree) visits the values from smallest to largest. Write them all down.
class Solution { // Brute force: write the whole tree out in order (left, node, right), // which gives the values sorted. Then pick position k. n steps, n memory. public int kthSmallest(TreeNode root, int rank) { List<Integer> values = new ArrayList<>(); inorder(root, values); // rank counts from 1, the list from 0. return values.get(rank - 1); }
private void inorder(TreeNode node, List<Integer> values) { if (node == null) return; // In a BST, everything smaller sits on the left, so it comes first. inorder(node.left, values); values.add(node.val); inorder(node.right, values); }}import sys
# A tree can be as deep as it has nodes (a chain), and Python stops at 1,000 nested calls by default.# Raise the limit to cover the deepest tree the constraints allow.sys.setrecursionlimit(20_000)
# Brute force: write the whole tree out in order (left, node, right),# which gives the values sorted. Then pick position k. n steps, n memory.def kth_smallest(root: TreeNode | None, rank: int) -> int: values = [] inorder(root, values) # rank counts from 1, the list from 0. return values[rank - 1]
def inorder(node: TreeNode | None, values: list[int]) -> None: if node is None: return # In a BST, everything smaller sits on the left, so it comes first. inorder(node.left, values) values.append(node.val) inorder(node.right, values)public class Solution{ // Brute force: write the whole tree out in order (left, node, right), // which gives the values sorted. Then pick position k. n steps, n memory. public int KthSmallest(TreeNode root, int rank) { var values = new List<int>(); Inorder(root, values); // rank counts from 1, the list from 0. return values[rank - 1]; }
private void Inorder(TreeNode node, List<int> outs) { if (node == null) return; // In a BST, everything smaller sits on the left, so it comes first. Inorder(node.left, outs); outs.Add(node.val); Inorder(node.right, outs); }}// Brute force: write the whole tree out in order (left, node, right),// which gives the values sorted. Then pick position k. n steps, n memory.func kthSmallest(root *TreeNode, rank int) int { values := inorder(root, nil) // rank counts from 1, the list from 0. return values[rank-1]}
func inorder(node *TreeNode, values []int) []int { if node == nil { return values } // In a BST, everything smaller sits on the left, so it comes first. values = inorder(node.Left, values) values = append(values, node.Val) return inorder(node.Right, values)}// Brute force: write the whole tree out in order (left, node, right),// which gives the values sorted. Then pick position k. n steps, n memory.function kthSmallest(root: TreeNode | null, rank: number): number { const values: number[] = []; inorder(root, values); // rank counts from 1, the list from 0. return values[rank - 1];}
function inorder(node: TreeNode | null, values: number[]): void { if (node === null) return; // In a BST, everything smaller sits on the left, so it comes first. inorder(node.left, values); values.push(node.val); inorder(node.right, values);}1 Find the work that repeats
There is no loop. The work repeats through recursion, so we count the calls of
inorder. Each call does a fixed job: one check, two more calls and at most one append. So each call counts as 1 step.- def inorder(node, values):
- if node is None: return
- inorder(node.left, values)
- values.append(node.val)
- inorder(node.right, values)
- values = []
- inorder(root, values)
- return values[rank - 1]
2 Count the runs on Example 1
Example 1 has 7 nodes, and each gets one call. Each of the 8 empty child spots also gets a call, which returns at once.
7 + 8 = 15 calls.
total calls7 nodes + 8 empty children = 153 Write the count for any n
A binary tree with n nodes has n + 1 empty child spots. Each node and each empty spot gets one call:
n + (n + 1) = 2n + 1 callsCheck it with Example 1: 2 × 7 + 1 = 15. The final lookup
values[rank - 1]is 1 more step, which does not change anything.4 Keep only the part that grows fastest
- Drop the + 1. One extra call does not change the growth.
- Drop the 2. When n doubles, 2n doubles too.
What is left is n.
So the time is O(n).
S Space: count the extra memory the same way
The list
valuesholds all n values. The call stack also holds up to h calls, where h is the height, and h is at most n. The list is the bigger part, so the space is O(n).values (n items)call stack (h frames)
Optimal O(h + k)
The idea
Section titled “The idea”Do the same in-order walk, but by hand with a stack, a list where the last item in is the first out. Then the walk can stop the moment it reaches the k-th value.
The smallest value left is always at the far left. So walk left from the current node and push every node on the way. Pop the top: nothing smaller is still waiting, so it is the next value in order. Count it. If it is the k-th, return it. If not, the next values are in its right subtree, so go right and walk left again from there.
Why it never misses the answer: the stack produces exactly the in-order sequence, which is the sorted order. Counting pops counts sorted values, so the k-th pop is the k-th smallest.
Walkthrough
Section titled “Walkthrough”- stack, node = [], root
- while True:
- while node:
- stack.append(node)
- node = node.left
- node = stack.pop()
- rank -= 1
- if rank == 0:
- return node.val
- node = node.right
The smallest value is at the far left. Walk there, and push each node on the way onto a stack, so we can come back up in order. Dim nodes are not reached yet, and stk marks nodes on the stack.
class Solution { // In-order walk with a stack, stopped at the k-th node. It only goes // down one path and then k nodes forward, so about h + k steps. public int kthSmallest(TreeNode root, int rank) { Deque<TreeNode> stack = new ArrayDeque<>(); TreeNode node = root; while (true) { // The smallest value left is at the far left. Remember the // path there, because each node on it comes next in order. while (node != null) { stack.push(node); node = node.left; } node = stack.pop(); // This is the next value in sorted order. if (--rank == 0) return node.val; // Values just above this one live in its right subtree. node = node.right; } }}# In-order walk with a stack, stopped at the k-th node. It only goes# down one path and then k nodes forward, so about h + k steps.def kth_smallest(root: TreeNode | None, rank: int) -> int: stack, node = [], root while True: # The smallest value left is at the far left. Remember the # path there, because each node on it comes next in order. while node: stack.append(node) node = node.left node = stack.pop() # This is the next value in sorted order. rank -= 1 if rank == 0: return node.val # Values just above this one live in its right subtree. node = node.rightpublic class Solution{ // In-order walk with a stack, stopped at the k-th node. It only goes // down one path and then k nodes forward, so about h + k steps. public int KthSmallest(TreeNode root, int rank) { var stack = new Stack<TreeNode>(); TreeNode node = root; while (true) { // The smallest value left is at the far left. Remember the // path there, because each node on it comes next in order. while (node != null) { stack.Push(node); node = node.left; } node = stack.Pop(); // This is the next value in sorted order. if (--rank == 0) return node.val; // Values just above this one live in its right subtree. node = node.right; } }}// In-order walk with a stack, stopped at the k-th node. It only goes// down one path and then k nodes forward, so about h + k steps.func kthSmallest(root *TreeNode, rank int) int { stack := []*TreeNode{} node := root for { // The smallest value left is at the far left. Remember the // path there, because each node on it comes next in order. for node != nil { stack = append(stack, node) node = node.Left } node = stack[len(stack)-1] stack = stack[:len(stack)-1] // This is the next value in sorted order. rank-- if rank == 0 { return node.Val } // Values just above this one live in its right subtree. node = node.Right }}// In-order walk with a stack, stopped at the k-th node. It only goes// down one path and then k nodes forward, so about h + k steps.function kthSmallest(root: TreeNode | null, rank: number): number { const stack: TreeNode[] = []; let node = root; while (true) { // The smallest value left is at the far left. Remember the // path there, because each node on it comes next in order. while (node) { stack.push(node); node = node.left; } node = stack.pop()!; // This is the next value in sorted order. if (--rank === 0) return node.val; // Values just above this one live in its right subtree. node = node.right; }}1 Find the work that repeats
The work is in the lit lines: each push walks one step left, and each pop takes one value. Both are a fixed job, so each push and each pop counts as 1 step. We count them.
- stack, node = [], root
- while True:
- while node:
- stack.append(node)
- node = node.left
- node = stack.pop()
- rank -= 1
- if rank == 0:
- return node.val
- node = node.right
2 Count the runs on Example 1
The table shows every step on Example 1. The walk pushes 7, 3 and 1 on its way down. Then it pops 1, pushes 2, pops 2, and pops 3, which is the 3rd pop.
4 pushes + 3 pops = 7 steps.
step 1 2 3 4 5 6 7 action push 7 push 3 push 1 pop 1 push 2 pop 2 pop 3 k after 3 3 3 2 2 1 0 3 Write the count for any n
The walk stops at the k-th pop, so there are exactly k pops. A node is pushed only once, and every pushed node is either popped or still on the stack at the end. The stack holds at most one path, so at most h nodes. So:
pops = k, pushes ≤ k + h, total ≤ 2k + hCheck it with Example 1: k = 3 and h = 4, so the bound is 10. The real run used 7.
4 Keep only the part that grows fastest
Drop the 2. Doubling k does not change how the count grows with k. What is left is h + k. When k is small and the tree is balanced, that is far less than n. In the worst case, k = n and the walk visits every node, so it is never slower than the brute force.
So the time is O(h + k).
S Space: count the extra memory the same way
The stack holds the nodes on one path from the root down, so at most h of them. The code adds only
node. So the space is O(h): about log n for a balanced tree, and n for a chain.stack (up to h nodes)node
Tested at the limits
Section titled “Tested at the limits”2,000 random search trees of 1 to 10 values, with every possible rank. The brute force and the optimal solution gave the same answer every time.
| Largest inputs | Result |
|---|---|
| 10,000 nodes in one long line to the left (the most allowed), with rank 10,000, so the walk passes every node. Optimal. | 0.001 s, and about 0.08 MB of extra memory: the stack holds all 10,000 nodes at once, which is the O(h) space. |
| 10,000 nodes, balanced, with rank 10,000. Optimal. | 0.001 s, and under 0.01 MB of extra memory: the stack holds at most about 14 nodes. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.