LeetCode每日一题2025-06-09
440. 字典序的第K小数字 H
给定整数 n 和 k,返回 [1, n] 中字典序第 k 小的数字。
示例 1:
输入: n = 13, k = 2
输出: 10
解释: 字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。
示例 2:
输入: n = 1, k = 1
输出: 1
提示:
1 <= k <= n <= 10⁹
问题分析
题目要求在区间 中,找到按字典序排序后第 小的数字。若将所有数字看作字符串并按字典序排列,实际等价于在一个“字典树”(Trie)结构上做先序遍历,找到第 个节点对应的数字。具体分析如下:
-
字典序与Trie 树
- 数字 作为第一层节点;以“前缀”为思路,若当前节点是 ,则其子节点为 (只要 )。
- 字典序遍历等价于对这棵树做“先序遍历”(先访问节点本身,再遍历其子树),返回顺序即为字典序。
-
前缀下节点计数
- 对于任意前缀 ,需要统计以 为前缀的所有数字在 范围内的个数。记为 。
- 计算思路:令
在每一轮,将当前层中所有以 开头且不超过 的数字数量累加到 :
然后向下一层扩展:
直至 为止。
-
贪心移动思路
- 从当前前缀 出发,假设已访问了 (即将 )。
- 在循环中,每步计算 :
- 若 ,说明第 小不在以 为前缀的子树中,可“跳过”整棵子树:
- 否则,第 小就在该子树中,此时先访问 本身(),再进入下一层:
- 若 ,说明第 小不在以 为前缀的子树中,可“跳过”整棵子树:
- 重复上述过程,直到 ,此时 即为答案。
算法思路
-
初始化
1
2curr = 1
k = k - 1 // 因为我们把数字 1 当作第一个访问 -
定义前缀计数函数
1
2
3
4
5
6
7
8
9count(prefix, n):
cnt = 0
cur = prefix
nxt = prefix + 1
while cur <= n:
cnt += min(nxt, n+1) - cur
cur *= 10
nxt *= 10
return cnt -
主循环
1
2
3
4
5
6
7
8
9
10
11while k > 0:
nodes = count(curr, n)
if nodes <= k:
// 第 k 小不在以 curr 为前缀的子树
k = k - nodes
curr = curr + 1
else:
// 第 k 小在以 curr 为前缀的子树
k = k - 1 // 访问 curr 本身
curr = curr * 10
return curr -
最终,
curr即为区间 中按字典序排列后的第 小数字。
时间复杂度
-
计算 需要不断把前缀乘以 10,层数最多为 轮,故单次 调用开销为 。
-
在主循环中,每次循环要么做一次 操作(兄弟前缀),要么做一次 操作(进入子树)。最坏情况下循环次数为 。虽然 ,但当 很大时,一次可跳过大量节点,因此实际循环次数远小于 。
-
综合来看,主循环中每次都要调用一次 ,复杂度近似为
代码分解
- 主函数 findKthNumber(n, k)
- 作用:初始化参数,循环寻址第 小数字。
- 核心变量:
curr:当前前缀(已定位到树中的某个节点)。k:剩余需要定位的偏移量。
- 辅助函数 count(prefix, n)
- 作用:计算当前前缀
prefix下,在 范围内共有多少个数字。 - 逻辑:
cur = prefix, nxt = prefix + 1, cnt = 0- 每轮累加 ,然后
cur *= 10, nxt *= 10,直到cur > n。 - 返回累计的
cnt。
- 跳过与进入逻辑
- 若
count(curr,n) <= k:k -= count(curr,n),跳过整个子树;curr += 1,移动到下一个兄弟前缀。
- 否则:
k -= 1,访问当前前缀节点本身;curr *= 10,进入该前缀的第一个子节点。
- 循环终止
- 当
k == 0时,curr即为答案,直接返回。
代码实现
1 | class Solution: |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Stay hungry. Stay foolish.!
评论
