[前端]_一起刷leetcode 378. 有序矩阵中第 K 小的元素

232 阅读5分钟

这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

题目

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

给你一个 n x n **矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。

 

示例 1:

输入: matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出: 13
解释: 矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13

示例 2:

输入: matrix = [[-5]], k = 1
输出: -5

 

提示:

  • n == matrix.length
  • n == matrix[i].length
  • 1 <= n <= 300
  • -109 <= matrix[i][j] <= 109
  • 题目数据 保证 matrix 中的所有行和列都按 非递减顺序 排列
  • 1 <= k <= n2

每日一皮

首先把所有元素扁平化到一个数组中,然后对整个数组进行排序,最后用返回数组的第k个节点,也就是索引为k - 1的元素即可。用js暴力链式编程也就一行代码搞定,so easy。

var kthSmallest = function(matrix, k) {
    return matrix.flat().sort((a, b) => a - b)[k - 1];
};

归并排序

思路

接下来咱们正儿八经的来做一下这道题目,其实这道题目无非是合并多个有序的数组,在合并多个有序数组、有序链表等题目的时候,一般来讲都可以用归并排序,并且它的性能是比较好的。所以老规矩,咱们归并排序走一发。

  1. 使用while循环遍历数组,当数组的长度超过1个时进行两两合并的归并排序,直到合并剩下一个数组的时候,返回数组的第k个节点,也就是索引为k - 1的元素;

  2. 在归并排序中,由于两个子数组都是有序的,我们可以每轮直接比较数组的第一个元素的大小,把最小值放到新的结果数组中去,直到有一个放完了把剩余的加到最后面即可。

实现

/**
 * @param {number[][]} matrix
 * @param {number} k
 * @return {number}
 */
var kthSmallest = function(matrix, k) {
    while (matrix.length > 1) {
        const len = Math.ceil(matrix.length / 2);

        // 相邻的元素两两组合
        for (let i = 0; i < len; i++) {
            matrix[i] = mergeTwoArray(matrix[2 * i], matrix[2 * i + 1]);
        }

        matrix.length = len;
    }

    return matrix[0][k - 1];
};

// 合并两个数组
function mergeTwoArray(arr1, arr2) {
    let temp = [];
    // 如果没有第二个数组直接返回第一个
    if (!arr2 || arr2.length === 0) {
        return arr1;
    }

    // 两两比较
    while (arr1.length && arr2.length) {
        if (arr1[0] <= arr2[0]) {
            temp.push(arr1.shift());
        } else {
            temp.push(arr2.shift());
        }
    }

    // 返回当前元素加剩余部分
    return temp.concat([...arr1, ...arr2]);
}

堆排序

当然这种类型的题目我们也可以通过堆排序,把每个数组放进最小堆中,每次从堆顶取出一个元素,同时我们的k减一,如果取出了一个元素后当前数组还有剩余就把它塞回去,重复k次也可以实现同样的功能。这道题目我就提供一个思路,因为合并有序的链表和数组的通用答案还是归并排序来的更快一些。只不过归并排序的快大部分时候来源于不重新开辟数组的空间,因为数组的pushshift等操作的性能远远没有直接交换节点的值来的快。

二分查找

上述归并排序的解法是这类题型的通用解法,但是在这道题目中我们会发现我们用少了一个条件,就是它的每一列纵向也是递增的,这意味着这道题目有其他更好的解决思路。

根据题意我们能把当前的有序矩阵大概排列成下图中的模样:

image.png

根据题意可知,节点值最大值是右下角的节点,最小值是左上角的节点。那么我们只需要通过二分法去取它两的中值mid,然后每次取中值在当前矩阵中有多少个小于等于它的元素的数量,直到取到数量刚好k个元素为止即可。

当然,在找数量的时候,我们可以利用快捷找法,从最后一行开始找,如果当前列的值小于我们的mid,那么说明这一列的元素全部小于它,如果大于,我们就要往上一行查找,这样子只需要沿着边界找一圈我们就可以统计出数量。

/**
 * @param {number[][]} matrix
 * @param {number} k
 * @return {number}
 */
var kthSmallest = function(matrix, k) {
    const n = matrix.length;

    // 找到最小值和最大值,每次在它们中间二分查找直到查到刚好某个数量的值
    let left = matrix[0][0], right = matrix[n - 1][n - 1];

    while (left < right) {
        const mid = Math.floor((left + right) / 2);
        // 如果数量不够,就继续加大这个值
        if (findLECount(matrix, mid) < k) {
            left = mid + 1;
        } else {
           right = mid;
        }
    }

    return left;
};

// 查找有多少个小于等于当前元素的数量
function findLECount(matrix, target) {
    const n = matrix.length;

    // 当前行,从最后一行开始
    let row = n - 1;
    // 当前列,从第一列开始
    let col = 0;
    // 统计数量
    let count = 0;

    // 行一行行往上走,列一列列往右走,只要没超出边界就继续
    while (row >= 0 && col < n) {
        // 如果当前值小于目标值, 说明这一列都比它小
        if (matrix[row][col] <= target) {
            // 当前有多少行就加多少,索引从0开始所以要 + 1
            count += row + 1;
            // 当前列往右走
            col++;
        } else {
            // 如若不然,就往上查找
            row--;
        }
    }
    // 返回统计的数量
    return count;
}

看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。