🌍 K-th Smallest in Lexicographical Order: LeetCode 440 (C++ | JavaScript | Python) - Featured Image
Programming9 min read

🌍 K-th Smallest in Lexicographical Order: LeetCode 440 (C++ | JavaScript | Python)

LeetCode 440 is a challenging Hard-level problem that combines greedy algorithms, prefix tree traversal, and mathematical counting to efficiently find the k-th smallest number in lexicographical order without generating the entire sequence.

📜 Problem Statement

Given two integers n and k, return the k-th smallest number in lexicographical (dictionary) order from the range [1, n].

Constraints:

  • 1 <= k <= n <= 10^9

  • The solution must handle large inputs efficiently

Core Problem Examples

Example 1: Basic Case

Input: n = 13, k = 2
Output: 10
Explanation: 
Lexicographical order: [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9]
The 2nd smallest number is 10

Example 2: Edge Case

Input: n = 1, k = 1
Output: 1
Explanation: Only one number exists in range [1, 1]

Example 3: Large Numbers

Input: n = 100, k = 10
Output: 17
Explanation: Efficient traversal without generating full sequence

🧠 Algorithm Strategy: Prefix Tree Traversal with Gap Counting

Visual Representation: 10-ary Prefix Tree

The lexicographical order can be visualized as a 10-ary tree where each node represents a number prefix:

        Root
       /  |  \
      1   2   3 ... 9
     /|   |   |
   10 11  20  30 ...
   |
  100 101 ...

Key Algorithm Concepts

Concept 1: Lexicographical Tree Traversal

Numbers in lexicographical order follow a preorder traversal pattern:

  • Visit current node (add to result)

  • Traverse left subtree (multiply by 10)

  • Traverse right siblings (increment by 1)

Concept 2: Gap Counting Strategy

Instead of generating all numbers, we calculate gaps between prefixes to determine:

  • Whether to move to next sibling (skip current subtree)

  • Whether to go deeper into current subtree

Concept 3: Greedy Decision Making

At each step, we decide the optimal path based on:

  • Current position in the sequence

  • Remaining steps to reach k-th position

  • Size of subtrees ahead

The Gap Counting Algorithm: Core Logic

Understanding the getGap Function

The getGap(a, b, n) function calculates how many valid numbers exist between prefix a and prefix b within the constraint n.

Gap Calculation Example

For n = 13, calculating getGap(1, 2, 13):

Level 0: [1] → count = 1
Level 1: [10, 11, 12, 13] → count = 4  
Total gap = 5 numbers between prefix 1 and 2

Step-by-Step Algorithm Walkthrough

For n = 13, k = 2:

  1. Start: curr = 1, i = 1

  2. Calculate gap between 1 and 2: gap = 5

  3. Check condition: i + gap = 1 + 5 = 6 > k = 2

  4. Decision: Go deeper into subtree 1

  5. Update: curr = 10, i = 2

  6. Result: Return 10 (the 2nd number)

Optimized Implementation Solutions

C++ Solution (Most Efficient)

class Solution {
public:
    int findKthNumber(int n, int k) {
        long curr = 1;
        
        for (int i = 1; i < k;) {
            long gap = getGap(curr, curr + 1, n);
            
            if (i + gap <= k) {
                // Skip current subtree, move to next sibling
                i += gap;
                curr++;
            } else {
                // Go deeper into current subtree
                i++;
                curr *= 10;
            }
        }
        
        return curr;
    }
    
private:
    long getGap(long a, long b, long n) {
        long gap = 0;
        
        while (a <= n) {
            gap += min(n + 1, b) - a;
            a *= 10;
            b *= 10;
        }
        
        return gap;
    }
};

JavaScript ES6+ Solution

/**
 * Find k-th smallest number in lexicographical order
 * @param {number} n - Upper bound
 * @param {number} k - Target position
 * @return {number} - K-th smallest number
 */
const findKthNumber = function(n, k) {
    let curr = 1;
    let i = 1;
    
    const getGap = (a, b, n) => {
        let gap = 0;
        
        while (a <= n) {
            gap += Math.min(n + 1, b) - a;
            a *= 10;
            b *= 10;
        }
        
        return gap;
    };
    
    while (i < k) {
        const gap = getGap(curr, curr + 1, n);
        
        if (i + gap <= k) {
            i += gap;
            curr++;
        } else {
            i++;
            curr *= 10;
        }
    }
    
    return curr;
};

Python Solution with Type Hints

class Solution:
    def findKthNumber(self, n: int, k: int) -> int:
        """
        Find k-th smallest number in lexicographical order
        Time: O(log²n), Space: O(1)
        """
        def getGap(a: int, b: int, n: int) -> int:
            """Calculate numbers between prefix a and b"""
            gap = 0
            
            while a <= n:
                gap += min(n + 1, b) - a
                a *= 10
                b *= 10
                
            return gap
        
        curr = 1
        i = 1
        
        while i < k:
            gap = getGap(curr, curr + 1, n)
            
            if i + gap <= k:
                i += gap
                curr += 1
            else:
                i += 1
                curr *= 10
                
        return curr

Java Solution

public class Solution {
    public int findKthNumber(int n, int k) {
        long curr = 1;
        
        for (int i = 1; i < k;) {
            long gap = getGap(curr, curr + 1, n);
            
            if (i + gap <= k) {
                i += gap;
                curr++;
            } else {
                i++;
                curr *= 10;
            }
        }
        
        return (int) curr;
    }
    
    private long getGap(long a, long b, long n) {
        long gap = 0;
        
        while (a <= n) {
            gap += Math.min(n + 1, b) - a;
            a *= 10;
            b *= 10;
        }
        
        return gap;
    }
}

Advanced Test Cases

Comprehensive Test Suite

Small Range Tests

// Test Case 1: Minimum input
n = 1, k = 1 → Output: 1
Explanation: Only one number in range

// Test Case 2: Single digit range
n = 9, k = 5 → Output: 5
Explanation: [1,2,3,4,5,6,7,8,9] - direct indexing

// Test Case 3: Two digit boundary
n = 10, k = 2 → Output: 10
Explanation: [1,10,2,3,4,5,6,7,8,9]

Medium Range Tests

// Test Case 4: Classic example
n = 13, k = 2 → Output: 10
Explanation: [1,10,11,12,13,2,3,4,5,6,7,8,9]

// Test Case 5: Mid-range selection
n = 100, k = 50 → Output: 38
Explanation: Efficient subtree skipping

Large Scale Tests

// Test Case 6: Large n, small k
n = 1000000, k = 100 → Output: 1000
Explanation: Demonstrates O(log²n) efficiency

// Test Case 7: Large n, large k
n = 1000000, k = 500000 → Output: 5xxxxx
Explanation: Complex tree traversal patterns

Edge Case Analysis

Boundary Conditions

  1. Minimum Values: n = 1, k = 1

  2. Power of 10 Boundaries: n = 10, 100, 1000

  3. Maximum k: k = n (last lexicographical number)

  4. Large Numbers: n = 10^9 (constraint maximum)

Complexity Analysis

Time Complexity Breakdown

Overall Time Complexity: O(log²n)

  • Outer loop iterations: O(log n) - maximum tree depth

  • getGap function calls: O(log n) per call - digit levels to traverse

  • Combined: O(log n × log n) = O(log²n)

Space Complexity Analysis

Space Complexity: O(1)

  • Only using constant extra variables

  • No recursion stack or additional data structures

  • Optimal space efficiency for large inputs

Performance Comparison

Approach Time Complexity Space Complexity Scalability Brute Force Generation O(n) O(n) Poor for large n Recursive Tree Traversal O(log²n) O(log n) Good, but stack risk Iterative Gap Counting O(log²n) O(1) Excellent

Common Implementation Pitfalls and Solutions

Critical Mistakes to Avoid

Mistake 1: Integer Overflow

// Wrong: Using int for large calculations
int gap = getGap(curr, curr + 1, n);

// Correct: Using long to prevent overflow
long gap = getGap(curr, curr + 1, n);

Mistake 2: Incorrect Gap Calculation

// Wrong: Off-by-one errors in gap calculation
gap += min(n, b) - a;  // Missing +1

// Correct: Proper range calculation
gap += min(n + 1, b) - a;

Mistake 3: Wrong Termination Condition

// Wrong: Infinite loop potential
while (i <= k) {  // Should be i < k
    // ... logic
}

Debugging Strategies

Strategy 1: Trace Small Examples

Walk through n = 13, k = 2 step by step:

  1. Visualize the prefix tree structure

  2. Calculate gaps manually for verification

  3. Track variable changes at each iteration

Strategy 2: Validate Gap Function

Test getGap function independently:

// Test: getGap(1, 2, 13) should return 5
// Verify: [1, 10, 11, 12, 13] = 5 numbers

Strategy 3: Boundary Testing

  • Test with k = 1 (first element)

  • Test with k = n (last element)

  • Test with powers of 10

Interview Preparation and Advanced Topics

Technical Interview Focus Points

What interviewers evaluate:

  1. Problem comprehension: Understanding lexicographical vs numerical order

  2. Algorithm design: Developing efficient tree traversal strategy

  3. Mathematical reasoning: Gap counting logic and optimization

  4. Code quality: Clean, bug-free implementation

  5. Complexity analysis: Accurate time/space complexity assessment

Follow-up Questions and Extensions

Question 1: Memory-Constrained Environment

"How would you solve this with O(1) space complexity?"

Answer: Our solution already achieves O(1) space! The iterative approach with gap counting uses only constant extra memory.

Question 2: Range Queries

"Find all numbers between k1-th and k2-th positions efficiently."

vector<int> findKthRange(int n, int k1, int k2) {
    vector<int> result;
    // Adapt existing algorithm for range queries
    return result;
}

Question 3: Lexicographical Ranking

"Given a number, find its lexicographical rank among [1, n]."

Question 4: Pattern Matching

"Find k-th number with specific digit patterns in lexicographical order."

Advanced Optimization Techniques

Technique 1: Precomputed Powers

// Cache powers of 10 for faster calculations
vector<long> powers = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000};

Technique 2: Iterative Deepening

// Optimize for cases where k is small
if (k <= 9) {
    return k;  // Direct return for single digits
}

Prerequisite Problems

Skills Development Path

  1. Master basic lexicographical concepts (LeetCode 386)

  2. Understand prefix trees and counting (LeetCode 440)

  3. Apply to permutation problems (LeetCode 60)

  4. Extend to complex combinatorics (Advanced problems)

Production Code Considerations

Error Handling and Validation

class Solution {
public:
    int findKthNumber(int n, int k) {
        // Input validation
        if (k < 1 || k > n || n < 1) {
            throw invalid_argument("Invalid input parameters");
        }
        
        // Handle edge cases
        if (n == 1) return 1;
        if (k == 1) return 1;
        
        // Main algorithm
        return findKthNumberInternal(n, k);
    }
    
private:
    int findKthNumberInternal(int n, int k) {
        // Implementation as shown above
    }
};

Performance Monitoring

#include <chrono>

int findKthNumberWithTiming(int n, int k) {
    auto start = chrono::high_resolution_clock::now();
    
    int result = findKthNumber(n, k);
    
    auto end = chrono::high_resolution_clock::now();
    auto duration = chrono::duration_cast<chrono::microseconds>(end - start);
    
    cout << "Execution time: " << duration.count() << " microseconds" << endl;
    return result;
}

Conclusion

LeetCode 440 represents a sophisticated application of mathematical optimization and algorithmic efficiency in competitive programming. The solution demonstrates several key principles:

Core Takeaways

  1. Efficient counting can replace brute force generation

  2. Tree visualization helps understand complex traversal patterns

  3. Greedy decisions based on mathematical analysis optimize performance

  4. Careful implementation prevents common pitfalls like integer overflow

Algorithm Design Principles

  • Divide and conquer through prefix tree partitioning

  • Mathematical optimization via gap counting

  • Iterative refinement avoiding recursion overhead

  • Boundary handling for edge cases and constraints

Real-World Applications

  • Database indexing with lexicographical ordering

  • File system navigation with dictionary-sorted paths

  • Search engine optimization with keyword ranking

  • Distributed systems with consistent hashing

The journey from understanding lexicographical order to implementing efficient tree traversal showcases the evolution from basic algorithms to advanced optimization techniques essential for senior software engineering roles.

If you learned something new today, consider following for more in-depth problem breakdowns and sharing this with your fellow devs. 👨‍💻

Posted on: 09/6/2025

om_shree_0709

🚀 Front-End Developer | UI/UX Enthusiast | EdTech Innovator

Posted by





Subscribe to our newsletter

Join 2,000+ subscribers

Stay in the loop with everything you need to know.

We care about your data in our privacy policy

Background shadow leftBackground shadow right

Have something to share?

Write on the platform and dummy copy content

Be Part of Something Big

Shifters, a developer-first community platform, is launching soon with all the features. Don't miss out on day one access. Join the waitlist: