LeetCode 386 - Lexicographical Numbers
The problem asks us to generate all integers from 1 to n, but not in normal numerical order. Instead, the numbers must appear in lexicographical order, also called dictionary order. Lexicographical order compares numbers as strings rather than as numeric values.
Difficulty: 🟡 Medium
Topics: Depth-First Search, Trie
Solution
Problem Understanding
The problem asks us to generate all integers from 1 to n, but not in normal numerical order. Instead, the numbers must appear in lexicographical order, also called dictionary order.
Lexicographical order compares numbers as strings rather than as numeric values. For example:
"1"comes before"10""10"comes before"11""2"comes after"13"
If n = 13, the lexicographical ordering becomes:
1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9
The input is a single integer n, representing the upper bound of the range [1, n].
The output must contain every integer in that range exactly once, sorted lexicographically.
The important part of this problem is not simply producing the correct ordering, but doing so under strict complexity requirements:
- Time complexity must be
O(n) - Extra space complexity must be
O(1)
These constraints immediately eliminate solutions that convert numbers to strings and sort them directly, because sorting requires O(n log n) time and additional memory.
The constraint 1 <= n <= 5 * 10^4 is not extremely large, but the problem explicitly demands an optimal traversal strategy rather than relying on built in sorting.
Several edge cases are important:
n = 1, the smallest possible input- Numbers ending in
9, where traversal must backtrack correctly - Numbers where multiplying by
10exceedsn - Transitions like
19 -> 2, which are not numerically adjacent but are lexicographically adjacent - Inputs like
100, where deep lexicographical branches exist
A naive implementation often fails during these transitions between prefixes.
Approaches
Brute Force Approach
The simplest approach is:
- Generate all integers from
1ton - Convert each integer to a string
- Sort the strings lexicographically
- Convert them back to integers
For example:
numbers = [1, 2, 3, ..., n]
numbers.sort(key=str)
This works because string comparison naturally follows lexicographical ordering.
However, sorting requires O(n log n) comparisons. Additionally, converting numbers to strings and storing them introduces extra memory overhead.
Although this solution is straightforward and correct, it violates the required O(n) time constraint.
Optimal Approach
The key insight is that lexicographical order forms a virtual tree structure.
For example, if n = 13:
1
├── 10
├── 11
├── 12
└── 13
2
3
4
...
9
Each number acts like a prefix.
From a current number:
- Going deeper in the tree means multiplying by
10 - Moving to the next sibling means adding
1 - Backtracking means dividing by
10
This structure allows us to simulate a depth first traversal without explicitly building a trie or recursion stack.
The traversal rules are:
- Try to go deeper by multiplying by
10 - If not possible, move to the next sibling by adding
1 - If the sibling is invalid or ends in
9, backtrack until a valid sibling exists
This generates numbers directly in lexicographical order in exactly O(n) time and O(1) extra space.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n log n) | O(n) | Generate all numbers and sort them as strings |
| Optimal | O(n) | O(1) | Simulates DFS traversal of a lexicographical tree |
Algorithm Walkthrough
Step 1: Start From 1
Lexicographical order always begins with 1, so initialize:
current = 1
We will generate exactly n numbers.
Step 2: Add Current Number to Result
At every iteration, append the current number to the answer list.
This guarantees that numbers are recorded in traversal order.
Step 3: Try Going Deeper
The lexicographically smallest child of a number is obtained by appending a digit 0.
Mathematically:
current * 10
Example:
1 -> 10
10 -> 100
If current * 10 <= n, we move deeper:
current *= 10
This mirrors DFS traversal into the subtree.
Step 4: Move to Next Sibling
If going deeper is impossible, try moving horizontally.
Example:
10 -> 11
11 -> 12
This is done with:
current += 1
However, this only works if:
- The next number does not exceed
n - The current number does not end in
9
Step 5: Backtrack When Necessary
If:
currentends with9, orcurrent + 1 > n
then we must move upward in the tree.
We repeatedly divide by 10:
current //= 10
until a valid sibling exists.
Example:
19 -> 1 -> 2
After backtracking, increment:
current += 1
Step 6: Repeat Until n Numbers Are Generated
We continue this traversal exactly n times.
Because each number is visited once, total complexity remains linear.
Why It Works
The algorithm performs a preorder DFS traversal of the implicit lexicographical tree.
At every step:
- Multiplying by
10visits the leftmost child - Incrementing visits the next sibling
- Dividing by
10backtracks to the parent
These operations exactly reproduce lexicographical ordering without explicitly storing the tree structure.
Since each number from 1 to n is generated once and only once, the algorithm is correct and optimal.
Python Solution
from typing import List
class Solution:
def lexicalOrder(self, n: int) -> List[int]:
result = []
current = 1
for _ in range(n):
result.append(current)
if current * 10 <= n:
current *= 10
else:
while current % 10 == 9 or current + 1 > n:
current //= 10
current += 1
return result
The implementation starts with current = 1, which is always the first lexicographical number.
The loop runs exactly n times, ensuring every number is generated once.
Inside the loop:
- The current number is appended to the result.
- The algorithm first attempts to go deeper into the lexicographical tree by multiplying by
10. - If deeper traversal is impossible, the code backtracks until a valid sibling exists.
- Finally, it moves to the next sibling using
current += 1.
The while loop handles all cases where traversal must move upward in the tree, such as transitions from 19 -> 2 or 199 -> 2.
No recursion, sorting, or auxiliary data structures are used, which satisfies the required O(1) extra space constraint.
Go Solution
func lexicalOrder(n int) []int {
result := make([]int, 0, n)
current := 1
for i := 0; i < n; i++ {
result = append(result, current)
if current*10 <= n {
current *= 10
} else {
for current%10 == 9 || current+1 > n {
current /= 10
}
current++
}
}
return result
}
The Go implementation follows the same traversal logic as the Python version.
A slice with capacity n is preallocated for efficiency:
result := make([]int, 0, n)
Integer arithmetic in Go naturally handles all required operations safely because the constraint n <= 5 * 10^4 is far below overflow limits for int.
Unlike Python, Go does not have dynamic list resizing semantics identical to Python lists, so preallocating capacity avoids unnecessary reallocations.
Worked Examples
Example 1
Input:
n = 13
Expected output:
[1,10,11,12,13,2,3,4,5,6,7,8,9]
Step by Step Trace
| Iteration | Current | Action | Result |
|---|---|---|---|
| 1 | 1 | append 1, go deeper to 10 | [1] |
| 2 | 10 | append 10, cannot go deeper, move to 11 | [1,10] |
| 3 | 11 | append 11, move to 12 | [1,10,11] |
| 4 | 12 | append 12, move to 13 | [1,10,11,12] |
| 5 | 13 | append 13, backtrack to 1, move to 2 | [1,10,11,12,13] |
| 6 | 2 | append 2, move to 3 | [1,10,11,12,13,2] |
| 7 | 3 | append 3, move to 4 | [1,10,11,12,13,2,3] |
| 8 | 4 | append 4, move to 5 | [1,10,11,12,13,2,3,4] |
| 9 | 5 | append 5, move to 6 | [1,10,11,12,13,2,3,4,5] |
| 10 | 6 | append 6, move to 7 | [1,10,11,12,13,2,3,4,5,6] |
| 11 | 7 | append 7, move to 8 | [1,10,11,12,13,2,3,4,5,6,7] |
| 12 | 8 | append 8, move to 9 | [1,10,11,12,13,2,3,4,5,6,7,8] |
| 13 | 9 | append 9 | [1,10,11,12,13,2,3,4,5,6,7,8,9] |
Example 2
Input:
n = 2
Expected output:
[1,2]
Step by Step Trace
| Iteration | Current | Action | Result |
|---|---|---|---|
| 1 | 1 | append 1, cannot go deeper, move to 2 | [1] |
| 2 | 2 | append 2 | [1,2] |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each number from 1 to n is visited exactly once |
| Space | O(1) | Only a few integer variables are used beyond the output list |
The traversal never revisits numbers or performs sorting operations.
Although the algorithm occasionally backtracks by dividing by 10, the total number of such operations across the entire execution is still linear relative to n.
The output list itself is not counted toward auxiliary space complexity because it is required by the problem.
Test Cases
from typing import List
class Solution:
def lexicalOrder(self, n: int) -> List[int]:
result = []
current = 1
for _ in range(n):
result.append(current)
if current * 10 <= n:
current *= 10
else:
while current % 10 == 9 or current + 1 > n:
current //= 10
current += 1
return result
sol = Solution()
assert sol.lexicalOrder(1) == [1] # minimum input
assert sol.lexicalOrder(2) == [1, 2] # small sequential case
assert sol.lexicalOrder(9) == [1,2,3,4,5,6,7,8,9] # single digit range
assert sol.lexicalOrder(10) == [1,10,2,3,4,5,6,7,8,9] # first deep branch
assert sol.lexicalOrder(13) == [1,10,11,12,13,2,3,4,5,6,7,8,9] # provided example
assert sol.lexicalOrder(19) == [1,10,11,12,13,14,15,16,17,18,19,2,3,4,5,6,7,8,9] # backtracking from 19
assert sol.lexicalOrder(20) == [1,10,11,12,13,14,15,16,17,18,19,2,20,3,4,5,6,7,8,9] # branch under 2
assert sol.lexicalOrder(99)[0] == 1 # starts correctly
assert sol.lexicalOrder(99)[-1] == 99 # ends correctly
assert len(sol.lexicalOrder(50000)) == 50000 # upper constraint stress test
| Test | Why |
|---|---|
n = 1 |
Validates minimum boundary |
n = 2 |
Validates small input behavior |
n = 9 |
Ensures simple single digit ordering |
n = 10 |
Tests transition into deeper lexicographical branch |
n = 13 |
Validates provided example |
n = 19 |
Tests backtracking after reaching 19 |
n = 20 |
Tests subtree traversal under prefix 2 |
n = 99 |
Verifies ordering across many branches |
n = 50000 |
Stress test for upper constraint |
Edge Cases
Edge Case 1: Smallest Possible Input
When n = 1, the answer contains only one number.
A buggy implementation might incorrectly attempt deeper traversal with 10 or perform unnecessary backtracking.
This implementation handles the case naturally because the loop executes once and appends only 1.
Edge Case 2: Transition From 19 to 2
This is one of the most important lexicographical transitions.
Numerically, 20 follows 19, but lexicographically 2 comes next.
A naive increment based solution would fail here.
The implementation correctly backtracks:
19 -> 1 -> 2
using repeated division by 10.
Edge Case 3: Numbers Ending in 9
Whenever the current number ends in 9, there are no more siblings at that level.
For example:
9
19
29
199
The algorithm detects this using:
current % 10 == 9
and backtracks until a valid next sibling exists.
Edge Case 4: Deep Branches Exceeding n
Suppose n = 13.
From 13, multiplying by 10 gives 130, which exceeds n.
The algorithm avoids invalid traversal with:
if current * 10 <= n:
This guarantees that only valid numbers are generated.