持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情
题目链接:668. 乘法表中第k小的数
题目描述
几乎每一个人都用 乘法表。但是你能在乘法表中快速找到第 k 小的数字吗?
给定高度 m 、宽度 n 的一张 m * n 的乘法表,以及正整数 k,你需要返回表中第 k 小的数字。
提示:
m和n的范围在[1, 30000]之间。k的范围在[1, m * n]之间。
示例 1:
输入: m = 3, n = 3, k = 5
输出: 3
解释:
乘法表:
1 2 3
2 4 6
3 6 9
第5小的数字是 3 (1, 2, 2, 3, 3).
示例 2:
输入: m = 2, n = 3, k = 6
输出: 6
解释:
乘法表:
1 2 3
2 4 6
第6小的数字是 6 (1, 2, 2, 3, 4, 6).
整理题意
题目给定一张 m * n 的乘法表,以及一个整数 k,要求我们找到乘法表中第 k 小的数字。
解题思路分析
首先观察题目数据范围:
- 乘法表的
m和n数据范围在30000以内,乘法表个数最大可达 个,而k就在这个数据范围内。 这个数据范围是不允许我们暴力遍历整个乘法表的,会超时(TLE)。
- 我们可以将问题转换为:给定一个数
x,求矩阵中有多少个数比x小,这样同样能够求得当前x是乘法表中的第几小的数; - 通过与
k进行比较来确定x是取大了还是取小了,由于x越大那么x在乘法表中的排位就越大,x越小在乘法表中的排位就越小,因此我们可以 二分x找到第一个排位等于k的x作为答案,二分的初始边界为乘法表的元素范围,即[1, m * n]。
- 这里需要注意有些数字不在乘法表中,所以二分的
x需要是第一个排位等于k的数,也就是排位等于k的最小x,所以在二分的时候要使得满足排位等于k的x尽可能小。- 那么问题又来了,为什么满足排位等于
k的最小x一定在乘法表中呢?这是因为我们在计算排位的时候count(x - 1) < k而count(x) == k,那么count(x) - count(x-1) > 0,也就是等于x的个数不为0,也就是说x在乘法表中。
- 问题转换到如何求
x在乘法表中是第几小的数呢,我们可以遍历乘法表的每一行或者每一列,此时可以直接计算得知当前行或者列有多少个数小于x,比如当前为第i行,所有数字都是i的倍数,因此不超过x的数字有 个(这里的n也可以是m,具体看遍历的是行还是列)。
注意一个优化,对于每个乘法表来说,我们每次选择
n和m中最小的那一个进行遍历,可以使得时间复杂度更优一点。
具体实现
- 选择
n和m中较小的一个作为二分时遍历的行或者列; - 初始化二分的区间范围
[l, r]; - 二分查找排位等于
k的最小x; - 遍历乘法表中的行或者列,统计乘法表中小于等于
mid的个数,那么mid在表格中就是第cnt小的数字;
需要注意的是:我们不能在
count = k时直接返回结果,而应该继续进行二分查找,最终返回能满足count = k的x最小值。
复杂度分析
- 时间复杂度:。二分的次数为 ,每次二分需要 的时间计算。
- 空间复杂度:。仅需要常数空间存储。
代码实现
class Solution {
public:
int findKthNumber(int m, int n, int k) {
//优化二分中遍历的m为较小一个
if(m > n) swap(n, m);
//答案在[1, m * n]中
int l = 0, r = m * n + 1;
while(l + 1 != r){
int mid = (l + r) >> 1;
//统计表格中小于等于mid的个数,那么mid在表格中就是第cnt小的数字
int cnt = (mid / n) * n;
for(int i = (mid / n) + 1; i <= m; i++){
cnt += mid / i;
}
//判断cnt与k的大小关系从而二分查找第一个大于等于k的cnt所对应的mid
if(cnt < k){
l = mid;
}
else r = mid;
}
return r;
}
};
总结
- 套路总结: 当元素过多,无法遍历求解二维矩阵里第
k小的数时,都可以用二分猜答案的 套路 进行解题,将题目转化为 “给定一个数,求矩阵中有多少个数比这个数小”,进而实现二分查找。 - 该题需要注意很多细节和优化,二分返回满足
count = k的x最小值,选取m和n中较小的作为遍历对象。 - 测试结果:
结束语
对于一艘没有目标的帆船来说,任何方向的风都可能时逆风。人生也是如此,浑浑噩噩没有目标,只会让时光蹉跎。想让现状有所改变,就要明确前进的方向。当你有了想要追逐的东西,旅途就会变得充实而又希望。