题目描述

✅ 386. 字典序排数

image-20260928221407677

题意分析

将 1 到 n 的所有整数,按照它们的十进制字符串字典序排列并返回,每个整数恰好出现一次。字典序逐位比较,公共前缀完全相同时,较短的字符串排在较长字符串之前,与按数值大小排序不同。

题目要求线性时间、常数额外空间,不计必须返回的结果列表。因此不能先生成全部字符串再排序,也不能用随数字位数增长的递归栈保存遍历过程。

解法:十叉树先序迭代

核心思路

[!blue]

把整数的十进制前缀看作一棵不需要实际创建的树:最上层是首位 1 到 9,前缀 cur 的孩子依次为 cur * 10 到 cur * 10 + 9,只保留不超过 n 的节点。字典序要求先输出前缀本身,再按末位大小访问它的各个子树,正好是这棵前缀树的先序遍历。

输出 cur 后,如果 cur * 10 <= n,还有合法的第一个孩子,就应该先沿当前前缀向下进入它,因为整棵当前前缀子树都排在下一个兄弟之前。代码用等价条件 cur <= n / 10 判断,再做乘法。

没有孩子时,先尝试同层的下一个兄弟。只有末位不是 9,且 cur + 1 <= n 时,加一才会得到合法兄弟;末位为 9 时直接加一会进位到另一个前缀,跳过先序遍历需要先处理的祖先兄弟,不能直接这样移动。

如果当前层没有合法兄弟,就用整数除以十返回父前缀,再检查父层是否有下一位兄弟。这个过程可能需要连续退回多层,所以必须使用循环。找到可右移的层后再加一,就得到当前子树之后的第一个合法节点。数字本身携带全部前缀信息,因此向上返回不需要额外的栈。

外层固定输出 n 次,每次按上述规则得到下一个字典序节点。最后一个输出完成后,代码仍可能计算一个已无须使用的后继,但外层计数已经结束,不会再次把它加入答案;不能只靠 cur <= n 控制整次遍历,否则回退后的值可能再次落入已访问范围。

解题步骤

  1. 创建结果列表,令 cur = 1,外层循环共执行 n 次。
  2. 每轮先把当前数字加入结果。
  3. 若 cur <= n / 10,令 cur *= 10,进入当前前缀的第一个孩子。
  4. 否则,只要末位为 9 或下一兄弟会超过 n,就连续执行 cur /= 10 回退到上一层。
  5. 找到存在合法兄弟的层后执行 cur++,下一轮继续输出,直到累计输出 n 个数。

代码实现

class Solution {
    public List<Integer> lexicalOrder(int n) {
        List<Integer> res = new ArrayList<>(n);
        int cur = 1;

        for (int i = 0; i < n; i++) {
            res.add(cur);

            // 前缀的孩子比下一个兄弟更早出现,有合法孩子就先下探。
            if (cur <= n / 10) {
                cur *= 10;
            } else {
                // 当前分支结束后可能要连续退出多层,直到找到合法兄弟。
                while (cur % 10 == 9 || cur + 1 > n) {
                    cur /= 10;
                }

                cur++;
            }
        }

        return res;
    }
}
func lexicalOrder(n int) []int {
    res := make([]int, 0, n)
    cur := 1
    for i := 0; i < n; i++ {
        res = append(res, cur)
        // 前缀的孩子比下一个兄弟更早出现,有合法孩子就先下探。
        if cur <= n/10 {
            cur *= 10
        } else {
            // 当前分支结束后可能要连续退出多层,直到找到合法兄弟。
            for cur%10 == 9 || cur+1 > n {
                cur /= 10
            }
            cur++
        }
    }
    return res
}

复杂度分析

  • 时间复杂度:$O(n)$。每个整数输出一次,下探和回退对应前缀树边的访问;整次遍历中每条相关边只需常数次移动,连续回退的累计代价仍为线性。
  • 空间复杂度:$O(1)$,不计返回结果,只保留当前数字和循环下标,没有创建前缀树、字符串数组或递归栈。

关键点总结

[!green]

  • 字典序等价于数字前缀树的先序:先当前前缀,再它的孩子,最后才是兄弟。
  • 乘十进入第一个孩子,除十返回父前缀,确认合法后加一进入兄弟。
  • 末位为九与超过上界都会使本层没有下一兄弟,需要继续向上寻找。
  • 固定输出数量保证最后一棵前缀子树处理完后正常结束。

易错点总结

[!yellow]

  • 输出当前前缀后直接加一,会跳过所有以它开头的更长数字;应优先检查孩子。
  • 只检查末位是否为九,可能生成超过上界的兄弟;只检查上界,又可能跨前缀错误进位。
  • 用一次 if 代替连续回退,可能在父层仍没有兄弟时错误停下。
  • 下探判断写成严格小于,会漏掉第一个孩子恰好等于上界的情况。
  • 使用当前值是否小于上界作为总循环条件,不能保证所有数字恰好输出一次;应以输出数量控制终止。

相似题目

题目 难度 关联与区别
440. 字典序的第K小数字 困难 数字前缀形成隐式十叉树,本题按先序枚举全部节点,原题按前缀子树大小跳过整块找到第k个。
208. 实现 Trie (前缀树) 中等 同样按前缀组织字典序,本题节点由数值规则隐式生成,不需要显式插入字符串。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/39470775
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!