Medium
题目描述
给你一个 n x n 矩阵 matrix,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
你必须找到一种内存复杂度优于 O(n²) 的解法。
示例 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 == matrix[i].length1 <= n <= 300-10⁹ <= matrix[i][j] <= 10⁹- 题目数据 保证
matrix中的所有行和列都按 非递减顺序 排列 1 <= k <= n²
进阶:
- 你能否用一个恒定的内存(即
O(1)内存复杂度)来解决这个问题? - 你能在
O(n)的时间复杂度下解决这个问题吗?这个方法对于面试来说可能太超前了,但是你会发现阅读这篇文章很有趣。
解题思路
本题有两种常见解法:
解法一:最小堆(优先队列)
利用矩阵行列有序的特性,用最小堆维护当前最小的k个候选元素。初始时将第一行元素加入堆,每次取出堆顶最小元素,同时将其下方元素(如果存在且未访问过)加入堆。重复k次后得到答案。
解法二:二分搜索(推荐)
关键观察:对于矩阵中任意值mid,我们可以在O(n)时间内统计出小于等于mid的元素个数。利用这个性质,在[matrix[0][0], matrix[n-1][n-1]]范围内二分搜索第k小的值。
对于统计小于等于mid的元素个数,从矩阵右上角开始遍历:
- 如果当前元素 <= mid,说明该列从当前位置往下的所有元素都 <= mid,计数后向下移动
- 如果当前元素 > mid,向左移动
这种方法的优势是空间复杂度仅O(1),且时间复杂度为O(n*log(max-min)),在大多数情况下比堆方法更优。
二分搜索法还有一个细节:最终答案必须是矩阵中实际存在的元素,这可以通过精确的二分边界控制来保证。
代码实现
class Solution {
public:
int kthSmallest(vector<vector<int>>& matrix, int k) {
int n = matrix.size();
int left = matrix[0][0];
int right = matrix[n-1][n-1];
while (left < right) {
int mid = left + (right - left) / 2;
int count = countLessEqual(matrix, mid);
if (count < k) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
private:
int countLessEqual(vector<vector<int>>& matrix, int target) {
int n = matrix.size();
int count = 0;
int row = 0, col = n - 1;
while (row < n && col >= 0) {
if (matrix[row][col] <= target) {
count += col + 1;
row++;
} else {
col--;
}
}
return count;
}
};
class Solution:
def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
n = len(matrix)
left, right = matrix[0][0], matrix[n-1][n-1]
while left < right:
mid = (left + right) // 2
count = self.countLessEqual(matrix, mid)
if count < k:
left = mid + 1
else:
right = mid
return left
def countLessEqual(self, matrix: List[List[int]], target: int) -> int:
n = len(matrix)
count = 0
row, col = 0, n - 1
while row < n and col >= 0:
if matrix[row][col] <= target:
count += col + 1
row += 1
else:
col -= 1
return count
public class Solution {
public int KthSmallest(int[][] matrix, int k) {
int n = matrix.Length;
int left = matrix[0][0];
int right = matrix[n-1][n-1];
while (left < right) {
int mid = left + (right - left) / 2;
int count = CountLessEqual(matrix, mid);
if (count < k) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
private int CountLessEqual(int[][] matrix, int target) {
int n = matrix.Length;
int count = 0;
int row = 0, col = n - 1;
while (row < n && col >= 0) {
if (matrix[row][col] <= target) {
count += col + 1;
row++;
} else {
col--;
}
}
return count;
}
}
var kthSmallest = function(matrix, k) {
const n = matrix.length;
let left = matrix[0][0];
let right = matrix[n-1][n-1];
while (left < right) {
const mid = Math.floor((left + right) / 2);
const count = countLessEqual(matrix, mid);
if (count < k) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
};
function countLessEqual(matrix, target) {
const n = matrix.length;
let count = 0;
let row = 0, col = n - 1;
while (row < n && col >= 0) {
if (matrix[row][col] <= target) {
count += col + 1;
row++;
} else {
col--;
}
}
return count;
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 最小堆 | O(k log n) | O(n) |
| 二分搜索 | O(n log(max-min)) | O(1) |
推荐解法: 二分搜索法,因为空间复杂度更优,且在大多数情况下时间复杂度也更好。