Find the k-th integer in lexicographical order without generating all numbers, using prefix counting over a conceptual trie.
Problem Restatement
We are given two integers n and k.
We need to return the k-th smallest integer in lexicographical order among all integers from 1 to n.
Lexicographical order means dictionary order, as if every number were compared as a string.
For example, when:
n = 13the numbers in lexicographical order are:
[1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9]So when:
k = 2the answer is:
10The official problem asks for the k-th lexicographically smallest integer in [1, n], with 1 <= k <= n <= 10^9.
Input and Output
| Item | Meaning |
|---|---|
| Input | Integers n and k |
| Output | The k-th number in lexicographical order |
| Range | Numbers from 1 to n |
| Constraint | 1 <= k <= n <= 10^9 |
Example function shape:
def findKthNumber(n: int, k: int) -> int:
...Examples
Example 1:
n = 13
k = 2Lexicographical order:
[1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9]The second number is:
10Example 2:
n = 1
k = 1There is only one number:
[1]Answer:
1Example 3:
n = 100
k = 10The beginning of lexicographical order is:
1, 10, 100, 11, 12, 13, 14, 15, 16, 17So the answer is:
17First Thought: Generate and Sort
A simple solution is:
- Generate every number from
1ton. - Sort them as strings.
- Return the
k - 1index.
Code idea:
nums = list(range(1, n + 1))
nums.sort(key=str)
return nums[k - 1]This is correct for small n.
But n can be as large as 10^9, so we cannot generate all numbers.
We need to jump through lexicographical order without listing every integer.
Key Insight
Lexicographical order is the same as preorder traversal of a prefix tree.
Think of numbers as prefixes.
The root has children:
1, 2, 3, 4, 5, 6, 7, 8, 9Node 1 has children:
10, 11, 12, 13, 14, 15, 16, 17, 18, 19Node 10 has children:
100, 101, 102, ...So lexicographical order begins:
1
10
100
101
...
11
12
...
2
20
...We do not need to build this tree.
We only need to count how many valid numbers are under a prefix.
For example, with:
n = 13the prefix 1 contains:
1, 10, 11, 12, 13So the subtree rooted at prefix 1 has size 5.
If k is larger than this size, we can skip the entire prefix 1 subtree and move to prefix 2.
If k lies inside this subtree, we go deeper from 1 to 10.
Counting Numbers Under a Prefix
To count how many numbers from 1 to n start with prefix prefix, compare ranges level by level.
For prefix 1, the covered ranges are:
[1, 2)
[10, 20)
[100, 200)
[1000, 2000)
...Each range means numbers that start with that prefix at a certain digit length.
For a bound n, the count added at each level is:
min(n + 1, next_prefix) - prefixwhen this value is positive.
For example, n = 13, prefix 1.
First level:
[1, 2) gives 1Second level:
[10, 20) clipped by n + 1 = 14 gives [10, 14), count 4Total:
1 + 4 = 5So there are 5 numbers starting with 1.
Algorithm
Start at:
curr = 1This is the first number in lexicographical order.
Since we are already standing on the first number, reduce k by one:
k -= 1While k > 0:
- Count how many numbers are under prefix
curr. - If that count is less than or equal to
k, the answer is not inside this prefix subtree.- Skip the whole subtree.
- Move to the next sibling:
curr += 1. - Subtract the skipped count from
k.
- Otherwise, the answer is inside this prefix subtree.
- Move to the first child:
curr *= 10. - Subtract
1fromkbecause we visit that child prefix itself.
- Move to the first child:
When k == 0, curr is the answer.
Correctness
Lexicographical order over integers from 1 to n is equivalent to preorder traversal of the conceptual prefix tree.
At every step, curr represents the current prefix position in that traversal.
The helper count tells exactly how many valid numbers appear in the subtree rooted at curr.
If this subtree size is less than or equal to k, then the desired number occurs after every number in this subtree. Skipping the whole subtree and moving to curr + 1 preserves the target position after subtracting the skipped size.
If this subtree size is greater than k, then the desired number lies inside the current subtree. In preorder traversal, after visiting curr, the next position inside the subtree is its first child curr * 10. Moving there and subtracting one correctly accounts for visiting that child prefix.
Each loop step either skips a complete subtree or moves one level deeper inside the subtree that contains the answer. These operations exactly follow preorder lexicographical order without enumerating every node.
When k becomes zero, the current number is exactly the target position, so returning curr is correct.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(log n * log n) | Each move calls a prefix-count loop over digit levels |
| Space | O(1) | Only integer variables are used |
In practice this is very small because n <= 10^9, so there are at most 10 digit levels.
Implementation
class Solution:
def findKthNumber(self, n: int, k: int) -> int:
def count_prefix(prefix: int) -> int:
count = 0
first = prefix
next_prefix = prefix + 1
while first <= n:
count += min(n + 1, next_prefix) - first
first *= 10
next_prefix *= 10
return count
curr = 1
k -= 1
while k > 0:
steps = count_prefix(curr)
if steps <= k:
curr += 1
k -= steps
else:
curr *= 10
k -= 1
return currCode Explanation
The helper function counts how many valid numbers begin with a prefix:
def count_prefix(prefix: int) -> int:We use two boundaries:
first = prefix
next_prefix = prefix + 1For prefix 1, these start as:
first = 1
next_prefix = 2The range [first, next_prefix) contains numbers starting with this prefix at the current digit level.
At each level, we clip the range by n + 1:
count += min(n + 1, next_prefix) - firstThen we move one digit deeper:
first *= 10
next_prefix *= 10The main function starts at 1:
curr = 1
k -= 1We subtract one because 1 is already the first number.
For each prefix, we count its subtree:
steps = count_prefix(curr)If the target is outside this subtree:
if steps <= k:
curr += 1
k -= stepswe skip it.
Otherwise, the target is inside the subtree:
else:
curr *= 10
k -= 1We go deeper to the first child.
Finally, curr is the answer.
Testing
def brute(n, k):
nums = list(range(1, n + 1))
nums.sort(key=str)
return nums[k - 1]
def run_tests():
s = Solution()
assert s.findKthNumber(13, 2) == 10
assert s.findKthNumber(1, 1) == 1
assert s.findKthNumber(100, 10) == 17
assert s.findKthNumber(10, 3) == 2
assert s.findKthNumber(99, 90) == brute(99, 90)
assert s.findKthNumber(1000, 500) == brute(1000, 500)
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
n = 13, k = 2 | Checks official example |
n = 1, k = 1 | Checks smallest input |
n = 100, k = 10 | Checks deeper prefix 100 |
n = 10, k = 3 | Checks transition from 10 to 2 |
| Brute comparison | Validates against sorting for small inputs |
| Larger brute comparison | Checks many prefix skips |