Start learning free

LESSON 349 OF 445 · BINARY TREES

Kth Smallest Element in a BST in Rust: Iterative In-Order, O(h + k)

Practice O(n) Read the full explanation

Given the root of a binary search tree (BST) and an integer k, return the k-th smallest value among all the values in the tree. k counts from 1: k = 1 asks for the smallest value.

In a BST, every value in a node's left subtree is smaller than the node's value, and every value in its right subtree is larger.

The trees below are written in level order (top to bottom, left to right), with None for a missing child.

 

Example 1:

    3
   / \
  1   4
   \
    2

Input: root = [3,1,4,None,2], k = 1
Output: 1
Explanation: In increasing order the values are 1, 2, 3, 4. The 1st smallest is 1.

Example 2:

        5
       / \
      3   6
     / \
    2   4
   /
  1

Input: root = [5,3,6,2,4,None,None,1], k = 3
Output: 3
Explanation: In increasing order the values are 1, 2, 3, 4, 5, 6. The 3rd smallest is 3.

Example 3:

Input: root = [5,3,6,2,4,None,None,1], k = 6
Output: 6
Explanation: The tree has 6 nodes, so the 6th smallest value is the largest one.

 

Constraints:

  • The number of nodes in the tree, n, is in the range [1, 1000].
  • Each node value is between -104 and 104, and all values are different.
  • 1 <= k <= n

 

Follow up: Can you stop as soon as you reach the answer, in O(h + k) time, where h is the height of the tree?
STEP 1 OF 12
    Ctrl + Enter to check

    RUST · BINARY TREES

    Kth Smallest Element In Bst in Rust, explained

    Problem overview

    The function kth_smallest receives the root of a binary search tree and an integer k, and returns the k-th smallest value in the tree. Counting starts at 1, so k = 1 asks for the minimum. All values are different, and k is between 1 and the number of nodes.

    For the tree [3, 1, 4, None, 2] and k = 1, the values in increasing order are 1, 2, 3, 4, so the answer is 1. For the tree [5, 3, 6, 2, 4, None, None, 1] the sorted values are 1, 2, 3, 4, 5, 6: k = 3 gives 3 and k = 6 gives 6.

    Approach: walk the tree in order and stop at the k-th node

    In a binary search tree, an in-order traversal, which visits the left subtree, then the node, then the right subtree, produces the values in increasing order. So the k-th node visited holds the answer.

    The simple way is to collect every value into a vector during a recursive traversal and then read index k - 1. That visits every node even when k is 1. The follow-up asks for O(h + k) time, where h is the tree's height: go down to the smallest value, then visit only k nodes. To stop in the middle of a traversal cleanly, this lesson writes the traversal with an explicit stack instead of recursion, and returns the moment the count reaches k.

    The stack holds the nodes whose left subtrees are still being visited. Here is k = 3 on the second tree. The walk goes left from 5 to 3 to 2 to 1, pushing each, so the stack is 5, 3, 2, 1. It pops 1 (the first visit) and 1 has no right child. It pops 2 (the second visit), which has no right child either. It pops 3, the third visit, and returns 3. The nodes 4, 5 and 6 were never looked at, apart from 5 sitting on the stack.

    Step by step

    1. Create the stack. A Vec<&TreeNode> holds borrowed nodes. push and pop work on the end of the vector, which is all a stack needs.
    2. Create the cursor. node starts as root.as_deref(), an Option<&TreeNode> pointing at the part of the tree that still has to be walked.
    3. Count visits. count starts at 0.
    4. Outer loop. It runs while the cursor holds a node or the stack isn't empty. The traversal is over only when both are exhausted.
    5. Inner loop: go left. while let Some(current) = node pushes current on the stack and moves node to current.left.as_deref(). Each node waits on the stack until everything smaller, its left subtree, has been visited.
    6. Pop the next node. When the inner loop ends, the cursor is None, and the top of the stack is the smallest unvisited node. pop() returns an Option, and unwrap() is safe because the stack can't be empty at this point.
    7. Visit it. Add 1 to count. If count == k, return current.val.
    8. Continue with the right subtree. Set node to current.right.as_deref(). Its values are bigger than the popped node but smaller than the nodes still on the stack, so they come next.
    9. Add a tail value. -1 without a semicolon after the loops gives the function a value on every path. It is never reached, since k is at most the number of nodes.

    The solution in Rust

    impl Solution {
        fn kth_smallest(root: Option<Box<TreeNode>>, k: i32) -> i32 {
            let mut stack: Vec<&TreeNode> = Vec::new();
            let mut node = root.as_deref();
            let mut count = 0;
            while node.is_some() || !stack.is_empty() {
                while let Some(current) = node {
                    stack.push(current);
                    node = current.left.as_deref();
                }
                let current = stack.pop().unwrap();
                count += 1;
                if count == k {
                    return current.val;
                }
                node = current.right.as_deref();
            }
            -1
        }
    }
    

    Rust details worth noticing:

    • References on the stack. The stack holds &TreeNode, not boxes, so pushing a node doesn't move or copy it. The references point into the tree owned by root, which stays alive until the function returns.
    • Two variables called current. The inner while let binds one that exists only inside that loop. The let current = stack.pop().unwrap(); after it is a new variable, so there is no clash.
    • Type of count. It is compared with k, an i32, so Rust infers i32 for it from count == k.
    • unwrap() on pop(). It would panic on an empty stack. Here the inner loop has just pushed a node, or the stack already held one, so it can't happen.
    • Returning from inside loops. return current.val; exits both loops and the function in one go, which is why the traversal can stop early.

    Complexity

    Time: O(h + k). The first descent pushes at most h nodes. After that, every pop is one visit, and the code stops at visit k. Each node is pushed and popped at most once, and the pushes are bounded by the pops plus the height. In the worst case, k = n, that is O(n).

    Space: O(h). The stack holds a path from the root down, so it never has more than h nodes: about log n for a balanced tree and up to n for a tree that is one long chain.

    Edge cases and common mistakes

    • k = 1. The answer is the leftmost node. The first descent finds it and the first pop returns it.
    • k equal to the node count. The traversal visits everything and returns the largest value.
    • A single node. k = 1 returns the root's value.
    • A chain that only goes right. The inner loop pushes one node, the pop visits it, and the cursor moves right. The stack stays tiny.
    • Counting before the pop. Counting nodes as they are pushed counts them in pre-order, not in sorted order. Count when you pop.
    • Forgetting the right subtree. Without node = current.right.as_deref(), the traversal skips every node that is only reachable through a right child, and the count comes out wrong.
    • Testing only node.is_some() in the outer loop. The loop would stop while nodes still wait on the stack.

    Testing

    • [3, 1, 4, None, 2] with k = 1 gives 1.
    • [5, 3, 6, 2, 4, None, None, 1] with k = 3 gives 3.
    • [5, 3, 6, 2, 4, None, None, 1] with k = 6 gives 6.
    • [5, 3, 6, 2, 4, None, None, 1] with k = 1 gives 1.
    • [5, 3, 6, 2, 4, None, None, 1] with k = 2 gives 2.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free