Facebook Pixel

386. Lexicographical Numbers

Problem Description

You are given an integer n and need to return all numbers from 1 to n sorted in lexicographical order.

Lexicographical order means the numbers are sorted as if they were strings. For example, if n = 13, the lexicographical order would be: [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9]. Notice how 10, 11, 12, and 13 come before 2 because when comparing as strings, "1" comes before "2".

The challenge is to generate this sequence efficiently with strict constraints:

  • Time complexity must be O(n) - you can only visit each number once
  • Space complexity must be O(1) extra space - you cannot use additional data structures beyond the output array

The solution uses a clever iteration approach. Starting with v = 1, it follows these rules to generate the next number in lexicographical order:

  1. If multiplying the current number by 10 doesn't exceed n, do that (e.g., go from 1 to 10)
  2. Otherwise, if the current number ends in 9 or incrementing it would exceed n, divide by 10 repeatedly until you can increment
  3. Then increment by 1

This process naturally generates numbers in lexicographical order by exploring deeper levels (multiplying by 10) when possible, and backtracking (dividing by 10) when necessary.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: While this problem doesn't explicitly mention graphs, we can model it as a graph problem. Each number from 1 to n can be thought of as a node, and there are implicit edges between numbers based on lexicographical ordering rules (e.g., from 1 to 10, from 10 to 100, etc.).

Is it a tree?

  • Yes: The lexicographical ordering creates a tree structure. Consider how numbers are organized:
    • Root: implicitly starts before 1
    • 1 has children: 10, 11, 12, ..., 19
    • 2 has children: 20, 21, 22, ..., 29
    • 10 has children: 100, 101, 102, ..., 109
    • This forms a 10-ary tree where each node can have up to 10 children (digits 0-9)

DFS

  • Yes: We arrive at DFS as our algorithm choice.

Conclusion: The flowchart suggests using a Depth-First Search (DFS) approach for this problem.

The DFS pattern fits perfectly because:

  1. We traverse the implicit tree of numbers in lexicographical order
  2. DFS naturally visits nodes in the correct order: going deep first (1 โ†’ 10 โ†’ 100) before backtracking
  3. The iterative solution simulates DFS traversal without recursion, achieving O(1) space
  4. Each number is visited exactly once, giving us O(n) time complexity

The solution implements DFS by:

  • Going deeper when possible (multiply by 10)
  • Backtracking when we can't go deeper (divide by 10)
  • Moving to the next sibling (increment by 1)
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 read numbers in a dictionary or phone book - we go digit by digit from left to right.

Think about how numbers are organized lexicographically: 1, 10, 100, 101, 102, ..., 109, 11, 110, .... Notice that after 1, we immediately jump to all numbers starting with 1 before moving to 2. This reveals a tree-like structure where:

  • 1 is the parent of 10, 11, 12, ..., 19
  • 10 is the parent of 100, 101, 102, ..., 109
  • And so on...

This naturally suggests a depth-first traversal approach. When we're at a number v, we have two choices:

  1. Go deeper: If we can append a 0 to the current number (multiply by 10) without exceeding n, we should do that first. This ensures we explore all numbers with the current prefix before moving on.
  2. Go to the next sibling or backtrack: If we can't go deeper, we need to move to the next number at the same level (increment by 1). But if we're at a number ending in 9 or incrementing would exceed n, we need to backtrack (divide by 10) until we find a valid position to increment.

The beauty of this approach is that it mimics how we would manually write numbers in lexicographical order - always trying to extend the current number first (going from 1 to 10), and only moving to the next prefix when we've exhausted all possibilities with the current one.

By simulating this DFS traversal iteratively rather than recursively, we avoid the call stack overhead and achieve the required O(1) extra space complexity. Each number is generated exactly once in the correct order, giving us O(n) time complexity.

Solution Approach

The implementation uses an iterative approach to simulate DFS traversal without recursion. Let's walk through the algorithm step by step:

Initialization:

  • Start with v = 1 as our current number
  • Create an empty answer array to store results

Main Loop (repeat n times):

For each iteration, we perform three key operations:

  1. Add current number to result: Append v to the answer array.

  2. Try to go deeper (multiply by 10):

    • If v * 10 <= n, we can extend the current number by appending a 0
    • Update v to v * 10
    • This moves us from numbers like 1 โ†’ 10 or 12 โ†’ 120
  3. Backtrack and increment when necessary:

    • If we can't go deeper, we need to find the next valid number
    • Use a while loop to handle two cases:
      • If v % 10 == 9: The current number ends in 9, so we can't simply increment (e.g., from 19 we can't go to 20 by adding 1)
      • If v + 1 > n: Incrementing would exceed our limit
    • In either case, divide v by 10 to backtrack (e.g., 19 โ†’ 1 or 139 โ†’ 13)
    • After the loop, increment v by 1 to move to the next sibling

Example walkthrough for n = 13:

  • Start: v = 1, add 1
  • 1 * 10 = 10 <= 13, so v = 10, add 10
  • 10 * 10 = 100 > 13, and 10 % 10 != 9, and 10 + 1 <= 13, so v = 11, add 11
  • 11 * 10 = 110 > 13, and 11 % 10 != 9, and 11 + 1 <= 13, so v = 12, add 12
  • 12 * 10 = 120 > 13, and 12 % 10 != 9, and 12 + 1 <= 13, so v = 13, add 13
  • 13 * 10 = 130 > 13, and 13 + 1 > 13, so divide: v = 1, then v = 2, add 2
  • Continue this pattern...

The algorithm maintains the lexicographical order by always trying to go deeper first (smaller lexicographical values) and only moving to siblings or backtracking when necessary. This ensures we generate exactly n numbers in O(n) time using only O(1) extra space beyond the output array.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with n = 15 to see how it generates numbers in lexicographical order.

Initial State: v = 1, answer = []

Iteration 1:

  • Add 1 to answer: answer = [1]
  • Can we go deeper? 1 * 10 = 10 โ‰ค 15 โœ“
  • Update: v = 10

Iteration 2:

  • Add 10 to answer: answer = [1, 10]
  • Can we go deeper? 10 * 10 = 100 > 15 โœ—
  • Need to backtrack? 10 % 10 = 0 โ‰  9 and 10 + 1 = 11 โ‰ค 15 โœ“
  • Increment: v = 11

Iteration 3:

  • Add 11 to answer: answer = [1, 10, 11]
  • Can we go deeper? 11 * 10 = 110 > 15 โœ—
  • Need to backtrack? 11 % 10 = 1 โ‰  9 and 11 + 1 = 12 โ‰ค 15 โœ“
  • Increment: v = 12

Iterations 4-6: Similar pattern continues

  • Add 12, then v = 13
  • Add 13, then v = 14
  • Add 14, then v = 15
  • Answer so far: [1, 10, 11, 12, 13, 14, 15]

Iteration 7:

  • Add 15 to answer: answer = [1, 10, 11, 12, 13, 14, 15]
  • Can we go deeper? 15 * 10 = 150 > 15 โœ—
  • Need to backtrack? 15 + 1 = 16 > 15 โœ—
  • Backtrack: v = 15 / 10 = 1
  • Increment: v = 2

Iteration 8:

  • Add 2 to answer: answer = [1, 10, 11, 12, 13, 14, 15, 2]
  • Can we go deeper? 2 * 10 = 20 > 15 โœ—
  • Need to backtrack? 2 % 10 = 2 โ‰  9 and 2 + 1 = 3 โ‰ค 15 โœ“
  • Increment: v = 3

Continue this pattern through iterations 9-15, adding 3, 4, 5, 6, 7, 8, 9

Final Result: [1, 10, 11, 12, 13, 14, 15, 2, 3, 4, 5, 6, 7, 8, 9]

Notice how the algorithm naturally produces lexicographical order:

  • It explores all numbers starting with "1" (1, 10-15) before moving to "2"
  • When it can't go deeper (multiply by 10), it increments
  • When it can't increment (would exceed n or ends in 9), it backtracks by dividing by 10

Solution Implementation

1class Solution:
2    def lexicalOrder(self, n: int) -> List[int]:
3        """
4        Generate numbers from 1 to n in lexicographical order.
5      
6        The algorithm simulates a depth-first traversal of a trie structure
7        where each path represents a number. It generates numbers in the
8        same order as they would appear in a dictionary.
9      
10        Args:
11            n: The upper bound (inclusive) for the range of numbers
12          
13        Returns:
14            A list of integers from 1 to n in lexicographical order
15        """
16        result = []
17        current_number = 1
18      
19        # Generate exactly n numbers
20        for _ in range(n):
21            # Add the current number to the result
22            result.append(current_number)
23          
24            # Try to go deeper in the trie (multiply by 10)
25            # This explores numbers like 1 -> 10 -> 100, etc.
26            if current_number * 10 <= n:
27                current_number *= 10
28            else:
29                # Cannot go deeper, need to move to the next sibling or backtrack
30              
31                # Backtrack if we've reached a number ending in 9
32                # or if incrementing would exceed n
33                while current_number % 10 == 9 or current_number + 1 > n:
34                    current_number //= 10  # Move up one level in the trie
35              
36                # Move to the next sibling
37                current_number += 1
38      
39        return result
40
1class Solution {
2    /**
3     * Generate numbers from 1 to n in lexicographical order.
4     * Uses an iterative approach to traverse numbers as if they were in a trie structure.
5     * 
6     * @param n The upper bound of numbers to generate (inclusive)
7     * @return List of integers from 1 to n in lexicographical order
8     */
9    public List<Integer> lexicalOrder(int n) {
10        // Initialize result list with capacity n for efficiency
11        List<Integer> result = new ArrayList<>(n);
12      
13        // Start with the current number as 1
14        int currentNumber = 1;
15      
16        // Generate exactly n numbers
17        for (int i = 0; i < n; i++) {
18            // Add current number to the result
19            result.add(currentNumber);
20          
21            // Try to go deeper in the trie (multiply by 10)
22            // This moves from a number like 1 to 10, or 12 to 120
23            if (currentNumber * 10 <= n) {
24                currentNumber *= 10;
25            } else {
26                // Cannot go deeper, need to move to the next sibling or backtrack
27              
28                // Backtrack if we're at a number ending in 9 (no next sibling)
29                // or if incrementing would exceed n
30                while (currentNumber % 10 == 9 || currentNumber + 1 > n) {
31                    // Move up one level in the trie (divide by 10)
32                    currentNumber /= 10;
33                }
34              
35                // Move to the next sibling at the current level
36                currentNumber++;
37            }
38        }
39      
40        return result;
41    }
42}
43
1class Solution {
2public:
3    vector<int> lexicalOrder(int n) {
4        vector<int> result;
5        int currentNumber = 1;
6      
7        // Generate n numbers in lexicographical order
8        for (int i = 0; i < n; ++i) {
9            // Add current number to result
10            result.push_back(currentNumber);
11          
12            // Try to go deeper in the tree (multiply by 10)
13            if (currentNumber * 10 <= n) {
14                currentNumber *= 10;
15            } else {
16                // Can't go deeper, need to move to next sibling or backtrack
17              
18                // Backtrack if we're at a leaf (ends with 9) or next number exceeds n
19                while (currentNumber % 10 == 9 || currentNumber + 1 > n) {
20                    currentNumber /= 10;
21                }
22              
23                // Move to next sibling
24                ++currentNumber;
25            }
26        }
27      
28        return result;
29    }
30};
31
1/**
2 * Generates numbers from 1 to n in lexicographical order
3 * @param n - The upper bound of numbers to generate
4 * @returns An array of numbers in lexicographical order
5 */
6function lexicalOrder(n: number): number[] {
7    const result: number[] = [];
8    let currentNumber: number = 1;
9  
10    // Generate exactly n numbers
11    for (let i = 0; i < n; i++) {
12        // Add current number to result
13        result.push(currentNumber);
14      
15        // Try to go deeper in the tree (multiply by 10)
16        if (currentNumber * 10 <= n) {
17            currentNumber *= 10;
18        } else {
19            // Can't go deeper, need to move to next sibling or backtrack
20          
21            // Backtrack if we're at a leaf node (ends with 9) or reached the boundary (n)
22            while (currentNumber % 10 === 9 || currentNumber === n) {
23                currentNumber = Math.floor(currentNumber / 10);
24            }
25          
26            // Move to next sibling
27            currentNumber++;
28        }
29    }
30  
31    return result;
32}
33

Time and Space Complexity

Time Complexity: O(n)

The algorithm iterates exactly n times through the main loop, appending one number to the result in each iteration. Within each iteration, the operations performed are:

  • Appending to the list: O(1)
  • Multiplying or dividing by 10: O(1)
  • The while loop for backtracking: Although there's a nested while loop, each digit position can only be visited a constant number of times throughout the entire algorithm. Specifically, we can backtrack from a number at most O(logโ‚โ‚€ n) times (the maximum depth), but since we only generate n numbers total, the amortized cost of backtracking is O(1) per number generated.

Therefore, the overall time complexity is O(n).

Space Complexity: O(1) (excluding the output array)

The algorithm uses only a constant amount of extra space:

  • Variable v to track the current number: O(1)
  • Loop counter: O(1)

If we include the output array ans that stores n integers, the space complexity would be O(n). However, as specified in the reference answer, we ignore the space consumption of the answer array, resulting in O(1) space complexity.

Common Pitfalls

1. Incorrect Backtracking Condition

A common mistake is using an if statement instead of a while loop for the backtracking logic:

Incorrect Implementation:

if current_number % 10 == 9 or current_number + 1 > n:
    current_number //= 10
current_number += 1

Why it fails: When we need to backtrack multiple levels (e.g., from 199 when n = 200), a single division isn't enough. We need to keep dividing until we find a valid position to increment.

Correct Implementation:

while current_number % 10 == 9 or current_number + 1 > n:
    current_number //= 10
current_number += 1

2. Off-by-One Error in Loop Count

Running the loop n-1 times or n+1 times instead of exactly n times:

Incorrect:

for i in range(1, n+1):  # or range(n-1)
    result.append(current_number)
    # ...

Correct:

for _ in range(n):  # Exactly n iterations
    result.append(current_number)
    # ...

3. Missing Edge Case for n = 1

While the algorithm handles n = 1 correctly, some might try to add special case handling unnecessarily, which can introduce bugs:

Unnecessary and error-prone:

if n == 1:
    return [1]
# regular algorithm...

The standard algorithm already handles this case perfectly without special treatment.

4. Integer Division Confusion in Python 2 vs Python 3

In Python 2, / performs integer division for integers, but in Python 3, you must use //:

Python 2 style (won't work correctly in Python 3):

current_number = current_number / 10  # Returns float in Python 3

Correct for Python 3:

current_number //= 10  # Integer division

5. Forgetting to Handle the Boundary After Backtracking

Some implementations forget that after backtracking (dividing by 10), we still need to check if incrementing is valid:

Incorrect logic:

if current_number * 10 <= n:
    current_number *= 10
elif current_number % 10 == 9:
    current_number //= 10
    current_number += 1  # Might exceed n!
else:
    current_number += 1

Correct approach: The while loop ensures we backtrack enough so that current_number + 1 is always valid when we increment.

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

Which type of traversal does breadth first search do?


Recommended Readings

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

Load More