leetcode:378. 有序矩阵中第 K 小的元素

题目来源

题目描述

在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {

    }
};

题目解析

分析数据

(1)数据量。从下面可以看出:

  • 这是一个正方形。
  • 最大数据量: 300 ∗ 300 = 6 ∗ 1 0 4 300 * 300 = 6 * 10^4 300300=6104
    • 算法复杂度不能是 O ( N 2 ) O(N^2) O(N2),因为会超过 1 0 8 10^8 108
    • 至少应该是 O ( N ∗ l o g N ) O(N * logN) O(NlogN)
      在这里插入图片描述

(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;
    }
};

类似题目

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
给定一个整数数组 nums 和一个目标值 target,要求在数组找出两个数的和等于目标值,并返回这两个数的索引。 思路1:暴力法 最简单的思路是使用两层循环遍历数组的所有组合,判断两个数的和是否等于目标值。如果等于目标值,则返回这两个数的索引。 此方法的时间复杂度为O(n^2),空间复杂度为O(1)。 思路2:哈希表 为了优化时间复杂度,可以使用哈希表来存储数组元素和对应的索引。遍历数组,对于每个元素nums[i],我们可以通过计算target - nums[i]的值,查找哈希表是否存在这个差值。 如果存在,则说明找到了两个数的和等于目标值,返回它们的索引。如果不存在,将当前元素nums[i]和它的索引存入哈希表。 此方法的时间复杂度为O(n),空间复杂度为O(n)。 思路3:双指针 如果数组已经排序,可以使用双指针的方法来求解。假设数组从小到大排序,定义左指针left指向数组的第一个元素,右指针right指向数组的最后一个元素。 如果当前两个指针指向的数的和等于目标值,则返回它们的索引。如果和小于目标值,则将左指针右移一位,使得和增大;如果和大于目标值,则将右指针左移一位,使得和减小。 继续移动指针,直到找到两个数的和等于目标值或者左指针超过了右指针。 此方法的时间复杂度为O(nlogn),空间复杂度为O(1)。 以上三种方法都可以解决问题,选择合适的方法取决于具体的应用场景和要求。如果数组规模较小并且不需要考虑额外的空间使用,则暴力法是最简单的方法。如果数组较大或者需要优化时间复杂度,则哈希表或双指针方法更合适。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值