这是我参与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.lengthn == matrix[i].length1 <= 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];
};
归并排序
思路
接下来咱们正儿八经的来做一下这道题目,其实这道题目无非是合并多个有序的数组,在合并多个有序数组、有序链表等题目的时候,一般来讲都可以用归并排序,并且它的性能是比较好的。所以老规矩,咱们归并排序走一发。
-
使用
while循环遍历数组,当数组的长度超过1个时进行两两合并的归并排序,直到合并剩下一个数组的时候,返回数组的第k个节点,也就是索引为k - 1的元素; -
在归并排序中,由于两个子数组都是有序的,我们可以每轮直接比较数组的第一个元素的大小,把最小值放到新的结果数组中去,直到有一个放完了把剩余的加到最后面即可。
实现
/**
* @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次也可以实现同样的功能。这道题目我就提供一个思路,因为合并有序的链表和数组的通用答案还是归并排序来的更快一些。只不过归并排序的快大部分时候来源于不重新开辟数组的空间,因为数组的push和shift等操作的性能远远没有直接交换节点的值来的快。
二分查找
上述归并排序的解法是这类题型的通用解法,但是在这道题目中我们会发现我们用少了一个条件,就是它的每一列纵向也是递增的,这意味着这道题目有其他更好的解决思路。
根据题意我们能把当前的有序矩阵大概排列成下图中的模样:
根据题意可知,节点值最大值是右下角的节点,最小值是左上角的节点。那么我们只需要通过二分法去取它两的中值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;
}
看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。