386. 字典序排数 
题目描述
给定一个整数 n,按照字典序返回 [1, n] 内的所有整数。
题目还要求算法的时间复杂度为 O(n),并且只使用 O(1) 额外空间。
什么是字典序?
字典序就是把数字当作字符串,按照查字典的方式比较。
例如比较 2 和 13:先比较第一个字符,"1" < "2",所以 13 排在 2 前面。
最直接的办法是把所有数字转成字符串后排序,但排序需要 O(n log n) 时间,不满足题目的 O(n) 要求。
核心思路:把数字看成一棵十叉树
把 1 到 9 看作第一层节点。对于任意数字 x,它后面追加一位 0 到 9,就得到它的子节点:
在不超过 n 的前提下,对这棵树进行先序遍历,访问顺序恰好就是字典序:先访问当前数字,再尽量访问以它为前缀的数字,最后访问下一个兄弟分支。
我们不需要真的创建这棵树,只维护当前数字 current,通过数学运算模拟移动:
如何找到下一个数字?
将当前数字加入答案后,分两种情况。
情况一:可以继续向下
如果 current * 10 <= n,说明最左侧子节点存在。下一项就是 current * 10。
情况二:不能继续向下
此时应尝试下一个兄弟 current + 1。但遇到下面两种情况时,没有合法的下一个兄弟:
- 当前数字以
9结尾,例如19后面不能直接变成20,因为20不再属于前缀1的子树。 current + 1 > n,例如13是本题范围内前缀1的最后一个节点。
这时要不断删除末尾数字,也就是回到父节点,直到可以移动到某个祖先的下一个兄弟节点。
再看一个需要连续回退的例子:
示例推演
以 n = 13 为例:
最终得到:
更容易理解的递归实现
如果优先考虑容易理解,可以直接对数字十叉树进行 DFS(深度优先搜索)。
第一层是 1 到 9。访问数字 current 后,依次尝试在它的末尾添加 0 到 9,得到它的子节点:
JavaScript 实现
这版代码可以概括成三步:
- 把当前数字加入结果。
- 在末尾依次添加
0到9,递归访问子节点。 - 子节点超过
n时停止当前分支。
以 n = 13 为例,递归调用顺序为:
递归版的时间复杂度也是 O(n),因为每个数字只加入结果一次。递归深度等于数字的位数,所以额外空间复杂度为 O(log n)。
它更适合第一次理解这道题;理解“十叉树的先序遍历”后,再学习下面通过乘十、加一和除十模拟 DFS 的迭代版。
满足 O(1) 额外空间的迭代实现
外层循环固定执行 n 次,所以即使最后一轮计算出的“下一项”不再使用,也不会影响结果。
代码与思路对照
循环开始时,current 始终是尚未加入答案的、字典序最小的数字。每轮把它加入结果后,再按照“子节点优先,其次兄弟节点,最后回退找祖先的兄弟节点”的顺序寻找下一项,这正是树的先序遍历规则。
正确性说明
数字 x 的所有后代都以 x 为前缀,因此在字典序中,它们一定排在 x 之后、x + 1 之前。
- 存在子节点时先执行
current *= 10,保证优先访问当前前缀下最小的数字。 - 不存在子节点时执行
current++,访问同层的下一个兄弟。 - 没有合法兄弟时不断除以
10,跳过已经访问完的子树,直到找到祖先的下一个兄弟。
每个合法数字都会作为一个树节点被访问一次,访问顺序符合先序遍历,因此结果既没有遗漏或重复,也严格满足字典序。
复杂度分析
- 时间复杂度:
O(n)。一共输出n个数字;每次除以10都是在回退一层,而遍历过程中向下和向上的总次数都是线性的。 - 额外空间复杂度:
O(1),只使用了少量变量,不需要递归栈或显式树结构。 - 返回数组本身需要
O(n)空间,但通常不计入额外空间。
为什么不直接排序?
这种写法直观,但排序需要 O(n log n) 次比较,每次比较还可能检查多个字符,同时会创建字符串。它适合帮助理解或快速验证答案,不是本题要求的最优解法。
易错点
- 把字典序理解成数值大小顺序。
- 只在
current % 10 === 9时回退,忘记处理current + 1 > n,例如n = 13、current = 13。 - 回退只执行一次。像
199这样的数字可能需要连续删除多个末位,因此必须使用while。 - 回退后忘记执行
current++,导致重复访问祖先节点。 - 使用递归 DFS 虽然容易理解,但会占用递归栈,不满足严格的
O(1)额外空间要求。
面试时怎么说
把整数按十进制前缀组织成一棵十叉树,字典序就是这棵树的先序遍历。维护当前数字:如果乘以
10不超过n,就进入最左侧子节点;否则不断删除末位,直到能访问下一个兄弟节点,再加一。整个过程不需要真正建树,每个数字访问一次,时间复杂度是O(n),额外空间复杂度是O(1)。
自测
- 为什么
1后面是10,而不是2? - 当
n = 13、current = 13时,如何得到下一个数字2? - 为什么
199之后寻找下一个分支时可能需要连续回退? - 如果改用递归 DFS,时间和空间复杂度分别是多少?

