LeetCode 378. 有序矩阵中第 K 小的元素
问题描述
给你一个 n x n 矩阵 matrix,其中每行和每列元素均按升序排序,找出矩阵中第 k 小的元素。
请注意,这是排序后的第 k 小元素,而不是第 k 个不同的元素。
你必须解决这个问题,且必须使用 O(k log n) 或 O(n log n) 的算法。
示例:
输入: matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出: 13
解释: 矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第8小元素是13
输入: matrix = [[-5]], k = 1
输出: -5
约束条件:
n == matrix.length == matrix[i].length1 <= n <= 300-10⁹ <= matrix[i][j] <= 10⁹- 题目数据保证
matrix中的所有行和列都按非递减顺序排列 1 <= k <= n²
算法思路
方法一:最小堆
核心思想:利用矩阵的有序,使用最小堆来逐步获取第 k 小的元素。
关键:
- 最小元素:
matrix[0][0]一定是最小的 - 候选元素:对于任意元素
matrix[i][j],下一个可能的候选是matrix[i+1][j]和matrix[i][j+1] - 避免重复:记录已经访问过的坐标,防止重复添加
步骤:
- 创建最小堆,存储
[value, row, col] - 初始化:将
(0, 0)加入堆和访问集合 - 循环 k 次:
- 弹出堆顶元素
- 如果是第 k 次弹出,返回该元素值
- 尝试添加右边
(row, col+1)和下边(row+1, col)的元素到堆中(如果未访问过且坐标有效)
方法二:二分查找
核心思想:在值域 [matrix[0][0], matrix[n-1][n-1]] 中进行二分查找,对于每个候选值 mid,计算矩阵中小于等于 mid 的元素个数。
关键:
- 有序矩阵的计数:从右上角开始,利用有序在 O(n) 时间内计算 ≤ target 的元素个数
- 二分条件:如果 ≤ mid 的元素个数 ≥ k,说明第 k 小元素 ≤ mid
步骤:
- 设置二分边界:
left = matrix[0][0],right = matrix[n-1][n-1] - 当
left < right时:- 计算
mid = left + (right - left) / 2 - 计算矩阵中 ≤ mid 的元素个数
count - 如果
count >= k,则right = mid - 否则
left = mid + 1
- 计算
- 返回
left
方法三:归并排序思想
核心思想:矩阵的每一行是一个有序数组,使用归并排序的思想找到第 k 小元素。
步骤:
- 创建最小堆,存储
[value, row, col] - 初始化:将每行的第一个元素加入堆
- 弹出 k 次,每次弹出后将对应行的下一个元素加入堆
- 第 k 次弹出的元素即为答案
代码实现
方法一:最小堆 + 访问集合
import java.util.*;
class Solution {
/**
* 有序矩阵中第K小的元素
*
* @param matrix n x n 有序矩阵
* @param k 第k小的元素
* @return 第k小的元素值
*/
public int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
// 最小堆:存储 [value, row, col]
PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[0] - b[0]);
// 访问集合:避免重复添加相同坐标
boolean[][] visited = new boolean[n][n];
// 初始化:添加左上角元素
minHeap.offer(new int[]{matrix[0][0], 0, 0});
visited[0][0] = true;
// BFS搜索k次
for (int i = 0; i < k; i++) {
int[] current = minHeap.poll();
int value = current[0];
int row = current[1];
int col = current[2];
// 如果是第k次弹出,返回结果
if (i == k - 1) {
return value;
}
// 尝试添加右边的元素
if (col + 1 < n && !visited[row][col + 1]) {
minHeap.offer(new int[]{matrix[row][col + 1], row, col + 1});
visited[row][col + 1] = true;
}
// 尝试添加下边的元素
if (row + 1 < n && !visited[row + 1][col]) {
minHeap.offer(new int[]{matrix[row + 1][col], row + 1, col});
visited[row + 1][col] = true;
}
}
return -1;
}
}
方法二:二分查找
class Solution {
/**
* 有序矩阵中第K小的元素
* 时间复杂度: O(n log(max-min))
*
* @param matrix n x n 有序矩阵
* @param k 第k小的元素
* @return 第k小的元素值
*/
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;
// 计算矩阵中小于等于mid的元素个数
int count = countLessEqual(matrix, mid, n);
if (count >= k) {
// 第k小元素在左半部分(包括mid)
right = mid;
} else {
// 第k小元素在右半部分
left = mid + 1;
}
}
return left;
}
/**
* 计算有序矩阵中小于等于target的元素个数
* 从右上角开始遍历,时间复杂度O(n)
*
* @param matrix 有序矩阵
* @param target 目标值
* @param n 矩阵维度
* @return 小于等于target的元素个数
*/
private int countLessEqual(int[][] matrix, int target, int n) {
int count = 0;
int row = 0;
int col = n - 1; // 从右上角开始
while (row < n && col >= 0) {
if (matrix[row][col] <= target) {
// 当前元素及该行左边所有元素都 <= target
count += col + 1;
row++; // 移动到下一行
} else {
// 当前元素 > target,向左移动
col--;
}
}
return count;
}
}
方法三:归并排序
import java.util.*;
class Solution {
/**
* 有序矩阵中第K小的元素
*
* @param matrix n x n 有序矩阵
* @param k 第k小的元素
* @return 第k小的元素值
*/
public int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
// 最小堆:存储 [value, row, col]
PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[0] - b[0]);
// 初始化:将每行的第一个元素加入堆
for (int i = 0; i < n; i++) {
minHeap.offer(new int[]{matrix[i][0], i, 0});
}
// 弹出k次
for (int i = 0; i < k - 1; i++) {
int[] current = minHeap.poll();
int row = current[1];
int col = current[2];
// 如果该行还有下一个元素,加入堆
if (col + 1 < n) {
minHeap.offer(new int[]{matrix[row][col + 1], row, col + 1});
}
}
// 第k次弹出的元素
return minHeap.poll()[0];
}
}
算法分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 最小堆 | O(k log k) | O(k) |
| 二分查找 | O(n log(max-min)) | O(1) |
| 多路归并 | O(k log n) | O(n) |
算法过程
matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8 :
二分查找:
- 初始边界:
left = 1, right = 15 - 第1轮:
mid = 8- 计算 ≤ 8 的元素个数:4 (1,5,9中1,5;10,11,13中无;12,13,15中无)
count = 4 < 8→left = 9
- 第2轮:
mid = 12- 计算 ≤ 12 的元素个数:6 (1,5,9;10,11;12)
count = 6 < 8→left = 13
- 第3轮:
mid = 13- 计算 ≤ 13 的元素个数:8 (1,5,9;10,11,13;12,13)
count = 8 >= 8→right = 13
- 终止:
left = right = 13,返回 13
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[][] matrix1 = {{1, 5, 9}, {10, 11, 13}, {12, 13, 15}};
System.out.println("Test 1: " + solution.kthSmallest(matrix1, 8)); // 13
// 测试用例2:单元素矩阵
int[][] matrix2 = {{-5}};
System.out.println("Test 2: " + solution.kthSmallest(matrix2, 1)); // -5
// 测试用例3:2x2矩阵
int[][] matrix3 = {{1, 2}, {3, 4}};
System.out.println("Test 3: " + solution.kthSmallest(matrix3, 3)); // 3
// 测试用例4:包含重复元素
int[][] matrix4 = {{1, 2, 2}, {2, 3, 3}, {3, 4, 4}};
System.out.println("Test 4: " + solution.kthSmallest(matrix4, 5)); // 3
// 测试用例5:k=1(最小元素)
int[][] matrix5 = {{1, 5, 9}, {10, 11, 13}, {12, 13, 15}};
System.out.println("Test 5: " + solution.kthSmallest(matrix5, 1)); // 1
// 测试用例6:k=n²(最大元素)
int[][] matrix6 = {{1, 5, 9}, {10, 11, 13}, {12, 13, 15}};
System.out.println("Test 6: " + solution.kthSmallest(matrix6, 9)); // 15
// 测试用例7:负数矩阵
int[][] matrix7 = {{-5, -4}, {-3, -2}};
System.out.println("Test 7: " + solution.kthSmallest(matrix7, 3)); // -3
// 测试用例8:3x3全相同
int[][] matrix8 = {{5, 5, 5}, {5, 5, 5}, {5, 5, 5}};
System.out.println("Test 8: " + solution.kthSmallest(matrix8, 5)); // 5
// 测试用例9:严格递增矩阵
int[][] matrix9 = {{1, 4, 7}, {2, 5, 8}, {3, 6, 9}};
System.out.println("Test 9: " + solution.kthSmallest(matrix9, 5)); // 5
}
关键点
-
有序矩阵:
- 每行从左到右升序
- 每列从上到下升序
matrix[i][j] <= matrix[i][j+1]且matrix[i][j] <= matrix[i+1][j]
-
countLessEqual:
- 从右上角开始,利用有序
- 时间复杂度 O(n),而不是 O(n²)
-
二分查找的边界:
- 使用
left < right而不是left <= right - 当
count >= k时,right = mid(包含 mid) - 当
count < k时,left = mid + 1(排除 mid)
- 使用
-
重复元素:
- 要求第 k 小元素,不是第 k 个不同元素
- 重复元素需要全部计数
常见问题
-
为什么二分查找能找到确切的矩阵元素?
- 最终的
left一定是矩阵中存在的元素 - 当
count >= k时保留mid,确保不会跳过真实存在的元素
- 最终的
-
countLessEqual 为什么从右上角开始?
- 右上角是每行的最大值,判断整行是否都 ≤ target
- 如果
matrix[row][col] <= target,则该行左边所有元素都 ≤ target
815

被折叠的 条评论
为什么被折叠?



