Facebook Pixel

440. K-th Smallest in Lexicographical Order

Problem Description

You are given two integers n and k. Your task is to find the k-th smallest integer when all integers from 1 to n are sorted in lexicographical order (dictionary order).

Lexicographical order means numbers are sorted as if they were strings. For example, if n = 13, the lexicographical ordering would be: 1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9

Notice how 10, 11, 12, 13 come immediately after 1 because when compared as strings, "10" comes before "2" (since "1" < "2" when comparing the first character).

The challenge is to directly find the k-th number in this ordering without generating and sorting all numbers from 1 to n, which would be inefficient for large values of n (up to 10^9).

The solution uses a clever approach by treating the numbers as a 10-ary tree (trie) structure where each node represents a numeric prefix. Starting from curr = 1, it counts how many valid numbers exist in each subtree and decides whether to:

  • Move to the next sibling (when the k-th number is not in the current subtree)
  • Go deeper into the current subtree (when the k-th number is within it)

The count function calculates how many valid numbers (โ‰ค n) exist with a given prefix by iterating through each level of the tree and summing up the valid numbers at each level.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that lexicographical ordering follows a pattern similar to how we traverse a tree structure. When we write numbers in lexicographical order, we're essentially doing a depth-first traversal of an imaginary tree where each node represents a number prefix.

Think about how numbers are organized lexicographically: after 1, we don't go to 2 immediately. Instead, we explore all numbers that start with 1: 10, 11, 12, ..., 19, then 100, 101, ..., and so on. This is exactly like traversing a tree where 1 is the root of a subtree containing all numbers with prefix 1.

Since we need the k-th number but can't afford to generate all numbers up to n, we need a way to "skip" entire groups of numbers. This leads to the counting strategy: if we can calculate how many valid numbers exist under any prefix (like how many numbers start with 1 and are โ‰ค n), we can decide whether our target k-th number is in that group or not.

The traversal strategy emerges naturally:

  • Start at 1 (the lexicographically smallest positive integer)
  • At each step, ask: "Is the k-th number in the current subtree?"
  • If yes, dive deeper by appending a digit (multiply by 10)
  • If no, skip to the next sibling (add 1)

The counting mechanism is clever: for a prefix curr, we count level by level. At each level, we're looking at ranges like [curr, next) where next = curr + 1 initially. We then expand both by multiplying by 10 to go to the next level: [curr*10, next*10). The count at each level is constrained by n, so we take min(n - curr + 1, next - curr).

This approach essentially simulates the lexicographical traversal without actually generating the numbers, making it efficient even for large n.

Solution Approach

The solution implements a Trie-based counting with greedy traversal strategy. We conceptually treat the range [1, n] as a 10-ary prefix tree where each node represents a numeric prefix.

Core Algorithm Structure

We maintain a variable curr representing the current number/prefix we're examining, starting at 1. We also adjust k to be 0-indexed by decrementing it initially (k -= 1).

The Counting Function

The count(curr) function calculates how many valid numbers (โ‰ค n) exist with prefix curr:

def count(curr):
    next, cnt = curr + 1, 0
    while curr <= n:
        cnt += min(n - curr + 1, next - curr)
        next, curr = next * 10, curr * 10
    return cnt

This function works level by level:

  • Initially, we're counting numbers in range [curr, next) where next = curr + 1
  • At each iteration, we count valid numbers at the current level: min(n - curr + 1, next - curr)
    • next - curr gives the full range size at this level
    • n - curr + 1 ensures we don't count beyond n
  • We then move to the next level by multiplying both boundaries by 10

For example, if curr = 1 and n = 234:

  • Level 1: Count in [1, 2) โ†’ adds 1
  • Level 2: Count in [10, 20) โ†’ adds 10
  • Level 3: Count in [100, 200) โ†’ adds 100
  • Level 4: Count in [1000, 2000) โ†’ but 1000 > 234, so we stop

The Main Traversal Logic

curr = 1
k -= 1  # Convert to 0-indexed
while k:
    cnt = count(curr)
    if k >= cnt:
        k -= cnt
        curr += 1  # Skip to next sibling
    else:
        k -= 1
        curr *= 10  # Go deeper into subtree
return curr

At each step, we make a greedy decision:

  1. Calculate subtree size: cnt = count(curr) tells us how many numbers have prefix curr

  2. Decision branching:

    • If k >= cnt: The target is not in this subtree. We skip all cnt numbers and move to the next sibling (curr + 1)
    • If k < cnt: The target is within this subtree. We descend one level deeper (curr * 10) and decrement k by 1 (accounting for the current node itself)
  3. Termination: When k becomes 0, we've found our target number

Example Walkthrough

For n = 13, k = 2:

  • Start: curr = 1, k = 1 (after converting to 0-indexed)
  • count(1) = 5 (numbers: 1, 10, 11, 12, 13)
  • Since k = 1 < 5, target is in subtree of 1
  • Update: curr = 10, k = 0
  • k = 0, so return 10

This approach efficiently navigates through the lexicographical ordering without generating all numbers, achieving O(log n) time complexity per level with at most O(log n) levels.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through finding the 6th lexicographically smallest number when n = 15.

The lexicographical ordering for n = 15 is: 1, 10, 11, 12, 13, 14, 15, 2, 3, 4, 5, 6, 7, 8, 9

Initial Setup:

  • curr = 1, k = 6 - 1 = 5 (converting to 0-indexed)

Step 1:

  • Current position: curr = 1
  • Count how many numbers have prefix 1: count(1)
    • Level 1: [1, 2) โ†’ 1 number
    • Level 2: [10, 20) but limited by n=15 โ†’ min(15-10+1, 20-10) = 6 numbers (10,11,12,13,14,15)
    • Total: 7 numbers with prefix 1
  • Decision: Since k = 5 < 7, our target is in the subtree of 1
  • Action: Go deeper โ†’ curr = 1 * 10 = 10, k = 5 - 1 = 4

Step 2:

  • Current position: curr = 10
  • Count how many numbers have prefix 10: count(10)
    • Level 1: [10, 11) โ†’ 1 number (just 10)
    • Level 2: [100, 110) but 100 > 15, so stop
    • Total: 1 number with prefix 10
  • Decision: Since k = 4 >= 1, our target is NOT in the subtree of 10
  • Action: Skip to sibling โ†’ curr = 10 + 1 = 11, k = 4 - 1 = 3

Step 3:

  • Current position: curr = 11
  • Count how many numbers have prefix 11: count(11)
    • Level 1: [11, 12) โ†’ 1 number (just 11)
    • Level 2: [110, 120) but 110 > 15, so stop
    • Total: 1 number with prefix 11
  • Decision: Since k = 3 >= 1, our target is NOT in the subtree of 11
  • Action: Skip to sibling โ†’ curr = 11 + 1 = 12, k = 3 - 1 = 2

Step 4:

  • Current position: curr = 12
  • Count how many numbers have prefix 12: count(12)
    • Level 1: [12, 13) โ†’ 1 number (just 12)
    • Total: 1 number with prefix 12
  • Decision: Since k = 2 >= 1, our target is NOT in the subtree of 12
  • Action: Skip to sibling โ†’ curr = 12 + 1 = 13, k = 2 - 1 = 1

Step 5:

  • Current position: curr = 13
  • Count how many numbers have prefix 13: count(13)
    • Level 1: [13, 14) โ†’ 1 number (just 13)
    • Total: 1 number with prefix 13
  • Decision: Since k = 1 >= 1, our target is NOT in the subtree of 13
  • Action: Skip to sibling โ†’ curr = 13 + 1 = 14, k = 1 - 1 = 0

Step 6:

  • k = 0, so we've found our answer!
  • Return curr = 14

The 6th number in lexicographical order is 14, which matches our expected ordering.

Solution Implementation

1class Solution:
2    def findKthNumber(self, n: int, k: int) -> int:
3        """
4        Find the k-th smallest number in lexicographical order from 1 to n.
5      
6        Args:
7            n: The upper bound of the range [1, n]
8            k: The position of the number to find (1-indexed)
9          
10        Returns:
11            The k-th smallest number in lexicographical order
12        """
13      
14        def count_steps_between_prefixes(current_prefix: int) -> int:
15            """
16            Count how many numbers exist in the lexicographical range 
17            starting with current_prefix up to the next prefix.
18          
19            For example, if current_prefix = 1, this counts all numbers
20            starting with 1 (like 1, 10, 11, ..., 19, 100, 101, ...) 
21            up to but not including those starting with 2.
22          
23            Args:
24                current_prefix: The current prefix to count from
25              
26            Returns:
27                Number of valid numbers with this prefix within range [1, n]
28            """
29            next_prefix = current_prefix + 1
30            steps_count = 0
31          
32            # Count numbers at each level of the tree
33            # Level 1: single digits (1-9)
34            # Level 2: double digits (10-99)
35            # Level 3: triple digits (100-999), etc.
36            while current_prefix <= n:
37                # Count valid numbers at this level
38                # Either count all numbers between current_prefix and next_prefix-1
39                # Or stop at n if it's smaller
40                steps_count += min(n - current_prefix + 1, next_prefix - current_prefix)
41              
42                # Move to next level (add a digit)
43                next_prefix *= 10
44                current_prefix *= 10
45              
46            return steps_count
47      
48        # Start with number 1
49        current_number = 1
50      
51        # We've already counted the first number (1), so decrement k
52        k -= 1
53      
54        # Find the k-th number by navigating the lexicographical tree
55        while k > 0:
56            # Count how many numbers are under the current prefix
57            steps_to_next_prefix = count_steps_between_prefixes(current_number)
58          
59            if k >= steps_to_next_prefix:
60                # Skip entire subtree and move to next sibling
61                k -= steps_to_next_prefix
62                current_number += 1  # Move to next prefix at same level
63            else:
64                # The target is within current subtree, go deeper
65                k -= 1  # Count current number
66                current_number *= 10  # Go to first child (append 0)
67              
68        return current_number
69
1class Solution {
2    private int n;
3  
4    /**
5     * Find the k-th smallest number in lexicographical order from 1 to n
6     * @param n The upper bound of the range [1, n]
7     * @param k The position of the number to find (1-indexed)
8     * @return The k-th smallest number in lexicographical order
9     */
10    public int findKthNumber(int n, int k) {
11        this.n = n;
12        long currentNumber = 1;
13        k--; // Convert to 0-indexed, as we start from number 1
14      
15        // Traverse the lexicographical tree until we find the k-th number
16        while (k > 0) {
17            // Count how many numbers exist in the subtree rooted at currentNumber
18            int subtreeNodeCount = count(currentNumber);
19          
20            if (k >= subtreeNodeCount) {
21                // The k-th number is not in this subtree, move to the next sibling
22                k -= subtreeNodeCount;
23                currentNumber++;
24            } else {
25                // The k-th number is in this subtree, go down to the first child
26                k--;
27                currentNumber *= 10;
28            }
29        }
30      
31        return (int) currentNumber;
32    }
33  
34    /**
35     * Count the number of valid integers in the subtree rooted at 'current'
36     * in the lexicographical tree, bounded by n
37     * @param current The root of the subtree
38     * @return The count of valid numbers in this subtree
39     */
40    public int count(long current) {
41        long nextSibling = current + 1;
42        long totalCount = 0;
43      
44        // Count nodes level by level in the lexicographical tree
45        while (current <= n) {
46            // At each level, count valid numbers between current and nextSibling-1
47            // But not exceeding n
48            totalCount += Math.min(n - current + 1, nextSibling - current);
49          
50            // Move to the next level (multiply by 10 for lexicographical tree)
51            nextSibling *= 10;
52            current *= 10;
53        }
54      
55        return (int) totalCount;
56    }
57}
58
1class Solution {
2public:
3    int maxNumber;  // Maximum number boundary
4
5    /**
6     * Find the k-th smallest number in lexicographical order from 1 to n
7     * @param n: The upper bound of numbers (1 to n)
8     * @param k: The position of the number to find (1-indexed)
9     * @return: The k-th smallest number in lexicographical order
10     */
11    int findKthNumber(int n, int k) {
12        this->maxNumber = n;
13      
14        // Convert to 0-indexed for easier calculation
15        --k;
16      
17        // Start from prefix 1
18        long long currentPrefix = 1;
19      
20        // Navigate through the lexicographical tree
21        while (k > 0) {
22            // Count how many numbers exist with current prefix in range [1, n]
23            int stepsInSubtree = countNumbersWithPrefix(currentPrefix);
24          
25            if (k >= stepsInSubtree) {
26                // Skip entire subtree and move to next sibling
27                k -= stepsInSubtree;
28                ++currentPrefix;
29            } else {
30                // The target is in current subtree, go deeper
31                --k;
32                currentPrefix *= 10;
33            }
34        }
35      
36        return static_cast<int>(currentPrefix);
37    }
38
39private:
40    /**
41     * Count how many numbers have the given prefix and are within range [1, maxNumber]
42     * @param prefix: The current prefix to count numbers for
43     * @return: Count of valid numbers with this prefix
44     */
45    int countNumbersWithPrefix(long long prefix) {
46        long long nextPrefix = prefix + 1;  // Next sibling prefix
47        int totalCount = 0;
48      
49        // Count numbers level by level in the tree
50        while (prefix <= maxNumber) {
51            // At current level, count valid numbers between [prefix, nextPrefix)
52            // But ensure we don't exceed maxNumber
53            totalCount += min(static_cast<long long>(maxNumber) - prefix + 1, 
54                            nextPrefix - prefix);
55          
56            // Move to next level (append a digit)
57            nextPrefix *= 10;
58            prefix *= 10;
59        }
60      
61        return totalCount;
62    }
63};
64
1/**
2 * Finds the k-th smallest number in lexicographical order from 1 to n
3 * @param n - The upper bound of the range [1, n]
4 * @param k - The position of the number to find (1-indexed)
5 * @returns The k-th smallest number in lexicographical order
6 */
7function findKthNumber(n: number, k: number): number {
8    /**
9     * Counts how many numbers exist in the subtree rooted at currentPrefix
10     * within the range [1, n] in lexicographical order
11     * @param currentPrefix - The current prefix to count from
12     * @returns The count of numbers in the subtree
13     */
14    function countNumbersInSubtree(currentPrefix: number): number {
15        let nextPrefix: number = currentPrefix + 1;
16        let count: number = 0;
17      
18        // Count numbers level by level in the lexicographical tree
19        while (currentPrefix <= n) {
20            // Add the count of valid numbers at this level
21            // Either all numbers from currentPrefix to nextPrefix-1, 
22            // or limited by n if n is smaller
23            count += Math.min(n - currentPrefix + 1, nextPrefix - currentPrefix);
24          
25            // Move to the next level (append a digit)
26            currentPrefix *= 10;
27            nextPrefix *= 10;
28        }
29      
30        return count;
31    }
32
33    let currentNumber: number = 1;
34    // Decrement k since we start from the first number
35    k--;
36
37    // Navigate through the lexicographical tree to find the k-th number
38    while (k > 0) {
39        // Count how many numbers are in the subtree rooted at currentNumber
40        const subtreeCount: number = countNumbersInSubtree(currentNumber);
41      
42        if (k >= subtreeCount) {
43            // Skip the entire subtree and move to the next sibling
44            k -= subtreeCount;
45            currentNumber += 1;
46        } else {
47            // The target is within this subtree, go deeper
48            k -= 1;
49            currentNumber *= 10;
50        }
51    }
52
53    return currentNumber;
54}
55

Time and Space Complexity

Time Complexity: O(log^2 n)

The algorithm traverses a conceptual trie structure of numbers from 1 to n in lexicographical order. The main loop runs at most O(log n) times because in the worst case, we traverse down the trie by multiplying curr by 10, which can happen at most logโ‚โ‚€ n times.

Within each iteration of the main loop, we call the count function which also runs O(log n) times. The count function calculates how many numbers exist with a given prefix by iterating through each level of the trie (each digit position), which takes O(log n) operations since we're essentially counting numbers digit by digit.

Therefore, the overall time complexity is O(log n) ร— O(log n) = O(log^2 n).

Space Complexity: O(1)

The algorithm uses only a constant amount of extra space. The variables used are:

  • curr: tracks the current number/prefix
  • k: decremented counter
  • In the count function: next, cnt for temporary calculations

No additional data structures like arrays or recursive call stacks are used, so the space complexity remains constant regardless of the input size.

Common Pitfalls

1. Off-by-One Error in Index Conversion

Pitfall: Forgetting to convert from 1-indexed to 0-indexed positioning, or incorrectly handling the conversion.

Many developers mistakenly keep k as 1-indexed throughout the algorithm, leading to returning the (k+1)-th number instead of the k-th number.

Incorrect approach:

# Wrong: Not adjusting k at all
curr = 1
while k > 0:  # This will iterate one extra time
    cnt = count(curr)
    if k > cnt:  # Wrong comparison
        k -= cnt
        curr += 1
    else:
        curr *= 10

Correct approach:

# Convert to 0-indexed immediately
k -= 1
curr = 1
while k > 0:
    # ... rest of logic

2. Integer Overflow in Count Function

Pitfall: When calculating the range in the count function, intermediate calculations can overflow for large values of n.

Problematic code:

def count(curr):
    next = curr + 1
    cnt = 0
    while curr <= n:
        # This multiplication can overflow
        cnt += min(n + 1, next) - curr  # Wrong formula
        next *= 10
        curr *= 10
    return cnt

Correct approach:

def count(curr):
    next = curr + 1
    cnt = 0
    while curr <= n:
        # Use min() to avoid overflow and ensure correct counting
        cnt += min(n - curr + 1, next - curr)
        next *= 10
        curr *= 10
    return cnt

3. Incorrect Subtree Traversal Logic

Pitfall: Confusing when to move to sibling vs. when to go deeper into the subtree, especially with the k decrement.

Common mistake:

while k > 0:
    cnt = count(curr)
    if k > cnt:  # Wrong: should be k >= cnt
        k -= cnt
        curr += 1
    else:
        # Forgot to decrement k here
        curr *= 10

Correct logic:

while k > 0:
    cnt = count(curr)
    if k >= cnt:
        # Skip entire subtree
        k -= cnt
        curr += 1
    else:
        # Go deeper, accounting for current node
        k -= 1
        curr *= 10

4. Boundary Condition in Count Function

Pitfall: Not properly handling the upper bound n when counting numbers in a range.

Incorrect:

def count(curr):
    next = curr + 1
    cnt = 0
    while curr <= n:
        # Wrong: doesn't account for n being within the range
        cnt += next - curr
        next *= 10
        curr *= 10
    return cnt

This would count numbers beyond n. For example, if curr = 10, next = 20, and n = 15, it would incorrectly count 10 numbers instead of 6.

Solution: Always use min(n - curr + 1, next - curr) to ensure you don't count beyond n. The n - curr + 1 gives the exact count of numbers from curr to n inclusive.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More