POTD: 386. Lexicographical Numbers

Understanding the Problem: Lexicographical Numbers

The 386th problem on LeetCode challenges us to generate numbers in lexicographical order. Given an integer n, the task is to return all numbers from 1 to n sorted as they would appear in a dictionary (lexicographically).

For instance, for n = 13, the numbers in lexicographical order would be:

1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9

Lexicographical Order Explained

Lexicographical (or dictionary) order is the way words are ordered in a dictionary, where digits are compared character by character. For example, the number 2 comes after 13 in lexicographical order because "1" comes before "2", and 13 has already taken all possible numbers starting with 1.

The key insight here is that lexicographically ordered numbers resemble a tree-like structure where each number can branch into subsequent digits.

Approach

The goal is to simulate this tree structure in code. Here’s how we can visualize the solution:

  • Tree Representation:
  • Starting at 1, lexicographically the next numbers would be 10, 100, and so on. After reaching the leaf node of this path (i.e., the largest number with this prefix), the next smallest lexicographical number would be 2, followed by 20, 200, and so on. This kind of traversal can be efficiently achieved using Depth-First Search (DFS).

DFS Approach: Step-by-Step

Instead of creating all the numbers first and sorting them, we directly simulate this lexicographical order using DFS. The idea is to “visit” numbers starting from 1, then explore all numbers prefixed with 1 before moving to numbers starting with 2, and so on.

Here’s how we can implement the solution:

var lexicalOrder = function(n) {
    const result = [];

    // DFS to generate numbers
    const dfs = (cur) => {
        if (cur > n) return;
        result.push(cur);
        for (let i = 0; i <= 9; i++) {
            const next = cur * 10 + i;
            if (next > n) return;
            dfs(next);
        }
    };

    // Start DFS for numbers 1 to 9
    for (let i = 1; i <= 9; i++) {
        dfs(i);
    }

    return result;
};

Explanation of the Code

  1. DFS Traversal: The dfs function explores each lexicographical “branch” starting from cur. It first pushes cur to the result and then recursively explores numbers formed by appending digits from 0 to 9 to cur.
  2. Base Case: If the next number generated (cur * 10 + i) exceeds n, we stop exploring further.
  3. Main Loop: The DFS is initiated for each number from 1 to 9 (since 0 cannot be a prefix of any valid number).

Complexity Analysis

  • Time Complexity: The time complexity is O(n) because we perform a DFS-like traversal and visit each valid number exactly once.
  • Space Complexity: The space complexity is O(n) due to the storage needed for the result array and the call stack used in DFS.

Key Insights

  1. Efficiency with DFS: The DFS approach ensures that we don’t waste memory or time generating numbers unnecessarily. Instead, we explore only those numbers that are required.
  2. Avoiding Sorting: Rather than generating all numbers and then sorting them lexicographically (which would take O(n log n) time), we directly simulate the lexicographical ordering, making the solution much faster and more memory efficient.

Edge Cases

  • For n = 1, the output should simply be [1].
  • For n = 0, no numbers should be output.
  • For large values of n, the DFS efficiently traverses the number space without memory overload or timeouts.

Conclusion

By utilizing DFS, we are able to solve the problem of generating numbers in lexicographical order in an optimal way, avoiding unnecessary computations and memory consumption. This problem teaches us to think of number generation in terms of prefix trees and use efficient traversal techniques to achieve the desired ordering.

This approach can be extended to solve many other problems involving lexicographical orders and tree-like structures.

Leave a Reply