Trees · Binary Search Tree Operations

BST Kth Smallest and Largest

Producing a BST's fully sorted order via a single inorder traversal, then reading the kth-from-the-front and kth-from-the-back elements directly by index - turning both "kth smallest" and "kth largest" into simple array indexing once the sorted order is materialized.

  • 5 min read
  • 8 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: stop at the kth visit
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

A BST keeps its values in order, but no node knows its own rank, that is, whether it's the 3rd smallest or the 300th. Inorder traversal visits the values in ascending order, so a node's rank is how many values came before it in inorder. This skill covers three ways to answer "what's the kth smallest (or largest)?": build the whole sorted list, count as you walk and stop at k, or store subtree sizes so that any rank takes one walk down from the root.

We'll go from simple to more technical:

  1. The pattern: the full inorder list, indexed from either end.
  2. A second flavor: count inorder visits and stop at k.
  3. Where rank queries show up (top-k, LIMIT, percentiles).
  4. What breaks it: counting at the wrong moment, off-by-one indices, not stopping.

The approach

Java

int[] kthSmallestAndLargest(TreeNode root, int k) {
    List<Integer> inorder = new ArrayList<>();
    traverse(root, inorder);            // fills the list in ascending order
    int n = inorder.size();
    return new int[]{ inorder.get(k - 1), inorder.get(n - k) };
}

Python

def kth_smallest_and_largest(root: TreeNode, k: int) -> list[int]:
    inorder = []
    traverse(root, inorder)              # fills the list in ascending order
    n = len(inorder)
    return [inorder[k - 1], inorder[n - k]]    # inorder[-k] is the same kth largest

Once the whole inorder list exists, both ends are one index away: the kth smallest is at index k - 1, the kth largest at index n - k (0-indexed). That costs O(n) time and space to build - worth it when many rank queries hit the same unchanged tree, wasteful for a single one.

A second flavor: stop at the kth visit

For one query, count as you visit and stop as soon as the count reaches k:

Java

int count = 0, answer;

void kthSmallest(TreeNode node, int k) {
    if (node == null || count >= k) return;          // already found - stop exploring
    kthSmallest(node.left, k);
    count++;                                         // this is the node's inorder VISIT
    if (count == k) { answer = node.val; return; }
    kthSmallest(node.right, k);
}

Python

def kth_smallest(root: TreeNode, k: int) -> int:
    count, answer = 0, None

    def visit(node: TreeNode | None) -> None:
        nonlocal count, answer
        if node is None or count >= k:
            return                       # already found - stop exploring
        visit(node.left)
        count += 1                       # this is the node's inorder VISIT
        if count == k:
            answer = node.val
            return
        visit(node.right)

    visit(root)
    return answer

The walk goes down the left spine (O(h)) and then visits exactly k nodes before stopping - O(h + k) time, O(h) space, no list. The kth largest is the same walk mirrored: reverse inorder (right subtree, node, left subtree) visits values in descending order, so its kth visit is the kth largest - no need to know n first.

The one place to get wrong is where count++ goes. It has to come after the left subtree - that's the moment a node is visited in inorder. Counting on entry (before recursing left) counts nodes in preorder, which has nothing to do with rank.

Where you'll see it

"Top k" and "kth ranked" queries - the 10 cheapest listings, the 3rd-highest bid, the median response time - are rank queries over ordered data. Walking in from the correct end and stopping after k is how an ordered index answers ORDER BY price LIMIT 10 without reading the rest of the table.

Which tool for which job?

You needUse
One rank query, small kcount inorder visits and stop at k: O(h + k)
The kth largestreverse inorder (right, node, left), stop at k
Many queries on a tree that doesn't changebuild the inorder list once, index k - 1 or n - k
Many queries on a tree that changesstore subtree sizes: O(h) per query
Python, sorted keys with rank accesssortedcontainers.SortedList (s[k - 1])

Common mistakes

MistakeWhat happensDo this instead
Counting on entry, before the left subtreecounts preorder: k = 1 returns the rootcount after the left subtree
n - k - 1 or n - k + 1 for the kth largestoff by onekth smallest k - 1, kth largest n - k
No early stopO(n) for an O(h + k) questionreturn once the count reaches k
Rebuilding the list per queryO(n) every timebuild once, or store subtree sizes
headSet(x).size() in Java as a rankwalks the whole view: O(rank)a size-augmented tree

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. In [5, 3, 8, 2, 4, 7, 9], a kth-smallest walk increments its counter on entry, before recursing left. What does it return for k = 1 and k = 3?

Show answer

5 and 2 - the 1st and 3rd nodes in preorder (5, 3, 2, ...), not in sorted order. The correct answers are 2 and 4. A node's inorder visit happens after its left subtree is done, so that's where the count belongs.

2. How do you find the kth largest without knowing how many nodes there are?

Show answer

Walk in reverse inorder - right subtree, node, left subtree - which visits values in descending order, and stop at the kth visit. The n - k index formula needs n; the mirrored walk doesn't.

3. A tree changes constantly and must answer rank queries fast. What would you store, and what does it buy?

Show answer

The size of each node's subtree. At each node, compare k with the left subtree's size plus one to decide whether the answer is here, to the left, or to the right (with k reduced) - one O(h) path per query. Inserts and deletes update the sizes along their own path, so the extra cost per update is also O(h).

Key takeaways

  • Inorder visits a BST in ascending order, so rank is just the count of inorder visits.
  • A full inorder list costs O(n) once and then answers any rank in O(1): kth smallest at k - 1, kth largest at n - k.
  • For a single query, count as you visit and stop at k: O(h + k). Reverse inorder gives the kth largest the same way.
  • Count at the visit - after the left subtree - not on entry, or you're counting preorder.
  • For frequent queries on a changing tree, store subtree sizes: O(h) per query.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
One query, small kcount inorder visits, stop at k
Kth largestreverse inorder
Many queries, static treethe inorder list once
Many queries, changing treesubtree sizes, O(h)
PythonSortedList

Snippets worth knowing by heart

Java

int kth(TreeNode cur, int k) {                 // with a size field in each node
    while (true) {
        int left = cur.left == null ? 0 : cur.left.size;
        if (k == left + 1) return cur.val;
        if (k <= left) cur = cur.left;
        else { k -= left + 1; cur = cur.right; }
    }
}

Python

from itertools import islice
kth = next(islice(inorder(root), k - 1, None))   # lazy: stops after k visits

The traps

  • Counting on entry: that counts preorder, so k = 1 returns the root. Count after the left subtree.
  • Off-by-one indices: n - k + 1 picks a neighbor. Sorted, the kth smallest is k - 1, the kth largest n - k.
  • No early stop: the walk visits all n nodes. Return once the count reaches k.
  • headSet(x).size() walks the whole view, so a rank costs O(rank). Store subtree sizes instead.

Staff-level points, one line each

  • Balanced 65,535 keys: k = 10 touched 23 nodes and k = 40,000 touched 40,005, while the size descent took 15 and 10 steps.
  • Insert and delete adjust sizes along their paths; rotations recompute two nodes.
  • Sizes stayed right through 5,000 random inserts and deletes.
  • islice over a lazy inorder generator stopped after 10 visits for k = 10.
  • TreeSet.headSet(x).size() iterates: at least 1,000x slower than size().

Say it in an interview

  • "Inorder is sorted, so rank is the number of inorder visits."
  • "For one query I count and stop: O(h + k)."
  • "If the tree changes and queries are frequent, I'd store subtree sizes for O(h) per query."
  • "The kth largest is reverse inorder, or with sizes, rank n - k + 1."

Practice

This skill comes with 8 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice BST Kth Smallest and Largest free

Kth Smallest and Largest Element in a BST

MediumProCoding problemJava · Python

Given the root of a binary search tree and an integer k, find both the kth smallest and kth largest values in the tree.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Median of a BST

MediumProCoding problemJava · Python

Given the root of a binary search tree, return the median of its values. With an odd number of nodes, the median is the middle value in sorted order; with an even number, it's the average of the two middle values.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice BST Kth Smallest and Largest free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime