题目来源
题目描述
class Solution {
public:
int kthSmallest(vector<vector<int>>& matrix, int k) {
}
};
题目解析
分析数据
(1)数据量。从下面可以看出:
- 这是一个正方形。
- 最大数据量:
300
∗
300
=
6
∗
1
0
4
300 * 300 = 6 * 10^4
300∗300=6∗104
- 算法复杂度不能是 O ( N 2 ) O(N^2) O(N2),因为会超过 1 0 8 10^8 108
- 至少应该是
O
(
N
∗
l
o
g
N
)
O(N * logN)
O(N∗logN)
(2)数据特性
- 值域比较大,所以填表法pass
- 从左到右递增,从上往下递增,
- 也就是它们具有单调性, 因此,思考:如何利用它呢?可不可以二分或者利用这个特性淘汰某些数据呢?
- 另外,matrix[0][0]是最小值,matrix[n-1][n-1]是最大值。
- 从k的取值范围可以看出k可以找到矩阵中的任意一个数
二分(最优解)
思路
- 整个矩阵中最小的数是左上角的数(假设为1),最大的数是右下角的数(假设为1000)。
- 第k小的数(假设k=100)一定在[1, 1000]之间。
- 怎么找这个k小的数呢?
- 先找<= 500的数有几个? 如果有200个,那么目标大了,因此
- 再找<= 250的数有几个?如果有50个,那么目标小了,因此
- 再找<=375的数有几个?如果刚好有100个,但是数组中并不一定有这个数,那么应该是<=375(>375的数一定会被淘汰)并且离它最近的那个数
- 因此,每一次二分都应该返回两个信息:
- <= 某一个值的个数有几个?
- 最接近它的是谁?
问题
思路:要求小于等于target的
- 尽量淘汰比target大的(列)
- 尽量保留小于等于target的(行)
从这里我们可以发现一个性质,
- 任取一个数x满足 l < = x < = r l <= x <= r l<=x<=r,那么矩阵中不大于 midx 的数,肯定全部分布在矩阵的左上角。
- 例如下图,取 x=8:
问题
代码
class Solution {
struct Info{
int near;
int num;
Info(int num, int near) : num(num), near(near){
}
};
static Info noMoreNum(vector<vector<int>>& matrix, int value){
int near = INT16_MIN;
int num = 0;
int N = matrix.size(), M = matrix[0].size();
int row = 0, col = M - 1;
while (row < N && col >= 0){
if(matrix[row][col] <= value){
near = std::max(near, matrix[row][col]);
num += col + 1;
row++;
}else{
col--;
}
}
return { num, near};
}
public:
int kthSmallest(vector<vector<int>>& matrix, int k) {
int N = matrix.size(), M = matrix[0].size();
int left = matrix[0][0], right = matrix[N - 1][M - 1];
int ans = 0;
while (left <= right){
int mid = left + ((right - left) >> 1);
// <=mid 有几个 <= mid 在矩阵中真实出现的数,谁最接近mid
Info info = noMoreNum(matrix, mid);
if(info.num < k){
left = mid + 1;
}else{
ans = info.near;
right = mid - 1;
}
}
return ans;
}
};
复杂度: O((N + M) * log(max - min))
归并排序(使用堆)
思路
在整个矩阵中,每次弹出矩阵中最小的值,第k个被弹出的就是我们想要的数据
那么,每次弹出矩阵中最小的值呢?
当我们看到下面这个有序矩阵时,我们知道左上角的数字是整个矩阵最小的。
但是弹出它之后,如何能够保证接下来每一次都还能找到全矩阵中最小的值呢?
解决这个问题的关键,在于维护一组“最小值候选人”。
需要保证的是最小值必然从这组候选人中产生,于是每次只要从候选人中弹出一个最小的就可以了。
我们来选择第一组候选人,在这里可以选择第一列,因为每一个数字都是其对应行的最小值,全局最小必然在其中
第一次弹出很简单,将左上角的1弹出即可。
1弹出之后,我们如何找到下一个候选人呢?
很简单,刚才弹出的位置右移一格就行了,这样不是还能保证候选人链表中每一个数字都是每一行的最小值吗,那全局最小值必然在其中!
我们每次弹出候选人当中的最小值,然后把上次弹出候选人的右边一个补进来,就能一直保证全局最小值在候选人列表中产生,
说白了就是每行一路,五路归并,用堆比较最小值。所以上下的顺序是没有用到,只用到左右的顺序
class Solution {
struct Element {
int val;
int x;
int y;
Element(int val, int x, int y) : val(val), x(x), y(y) {
}
// 方法二定义pq 需要重载 operator>
bool operator> (const Element &other) const {
return this->val > other.val;
}
};
public:
int kthSmallest(vector<vector<int>>& matrix, int k) {
int N = matrix.size(), M = matrix[0].size();
// 定义pq方法一:用自定义的比较
// auto comp = [](Element e1, Element e2) { return e1.val > e2.val; };
// priority_queue<Element, vector<Element>, decltype(comp)> pq (comp); // 用优先队列表示最小堆
// 定义pq方法二:在 Element 中重载 operator>,然后用内置的std::greater
priority_queue<Element, vector<Element>, std::greater<Element>> pq;
// 初始化:将 matrix 的第一列加入 pq 作为初始的「最小候选值」列表
for (int r = 0; r < N; ++r) {
Element e(matrix[r][0], r, 0);
pq.push(e);
}
// 弹出前 k-1 小的值
for (int i = 0; i < k - 1; ++i) {
Element top = pq.top(); pq.pop();
if (top.y != N - 1) { // 当前 (top.x, top.y) 的右边还有数字,将它右边的数 push 到优先队列中
Element e(matrix[top.x][top.y + 1], top.x, top.y + 1);
pq.push(e);
}
}
return pq.top().val;
}
};