感觉自己在这题上面是砸了很长的一段时间啊。这题目前来说可以有很多种解法,其中前三种都是围绕着heap转的。
1. 纯heap解。就是用一个size为k的最大堆不停遍历整个matrix就可以了,复杂度是O(n^2logk)。这种解法可以用于任何matrix,也就是解法本身没有利用任何sorted matrix的特征。但神奇的是居然也能过,没有超时
public int kthSmallest(int[][] matrix, int k) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>((a, b) -> b - a);
for (int[] row : matrix) {
for (int num : row) {
maxHeap.add(num);
if (maxHeap.size() > k) {
maxHeap.poll();
}
}
}
return maxHeap.poll();
}
2. 接下来的解法,基本概念就和merge n sorted list是一样的。priorityqueue里面装的不再是一个单一的整型数,而是一个[col, row, matrix[col][row]]的结构。当然用作比较对象的还是matrix[col][row]这个值,另外用的是最小堆而不是最大堆了。上面那个解法用最大堆是因为我们要保留最小的k个,所以遍历的时候每次都要pop出来最大的那个,这种做法我们要pop掉k - 1次最小值,从而达到最后这个最小堆里的头部就是第k小的。这种做法利用了sorted matrix双向排序中的其中一个方向。也就是,如果一个matrix是按照行排序,或者列排序的,都可以用这种方法找第k小的数,而不需要行列都排序。这个做法的复杂度是O((n + k)logn)
public int kthSmallest(int[][] matrix, int k) {
PriorityQueue<int[]> maxHeap = new PriorityQueue<int[]>((a, b) -> a[2] - b[2]);
for (int i = 0; i < matrix.length; i++) {
maxHeap.add(new int[]{i, 0, matrix[i][0]});
}
for (int i = 0; i < k - 1; i++) {
int[] polled = maxHeap.poll();
if (polled[1] + 1 < matrix[polled[0]].length) {
polled[1] = polled[1] + 1;
polled[2] = matrix[polled[0]][polled[1]];
maxHeap.add(polled);
}
}
return maxHeap.poll()[2];
}
3. 实话说,接下来的解法已经开始有点超越我能够完全理解的范围了。上面的解法,都没有完全利用到sorted matrix的性质,就是从行从列来看都是排序的。这个解法,就充分利用了这行列都排序的条件,和解法二有点类似:都是通过pop掉前面k - 1小的元素然后把第k小的元素保留在一个最小堆的堆顶,而且在堆中的数据结构也是一样的,但过程不太一样。做法是这样的
1. 建立一个最小堆,并把[0, 0, matrix[0][0]]放到堆里
2. 把堆顶元素pop出来,假设pop出来的是[x, y, matrix[x][y]]。那么将[x + 1, y, matrix[x + 1][y]]push回去,如果x为0的话,那么还需要push [x, y + 1, matrix[x][y + 1]]回去。
3. 重复k - 1次步骤2。最后堆顶就是第k小的数字。
public int kthSmallest(int[][] matrix, int k) {
PriorityQueue<int[]> numPQ = new PriorityQueue<>((a, b) -> a[2] - b[2]);
numPQ.add(new int[]{0, 0, matrix[0][0]});
for (int i = 0; i < k - 1; i++) {
int[] current = numPQ.poll();
if (current[0] == 0 && current[1] != matrix[0].length - 1) {
numPQ.add(new int[]{current[0], current[1] + 1, matrix[current[0]][current[1] + 1]});
}
if (current[0] != matrix.length - 1) {
current[0]++;
current[2] = matrix[current[0]][current[1]];
numPQ.add(current);
}
}
return numPQ.poll()[2];
}
要记住步骤2这种走法,这种走法可以让你在sorted matrix里面每次都可以pop到当前最小的。用这种方法走完整个数组就可以得到一个排好序的matrix的一维表达方式。这样做的复杂度就是 O(klogk)
下面是目前已知最快的算法。二分法:
一般来说,二分法都适用于在一个数组里面搜寻特定的数字,搜的是index。但是在某些特别的情况下,我们也可以进行range based的二分搜索,一个很久以前出现过的题目求开方就利用的是range based的二分搜索法进行的。https://blog.csdn.net/chaochen1407/article/details/43308435
这题的二分算法也是基于range来进行的。range的范围就是(low, high),low最开始为matrix[0][0],也就是matrix中数字最小的,high最开始为matrix[matrix.length - 1][matrix[0].length - 1],也就是matrix中最大的, 之后根据遍历进行收束。 核心原理就是用二分不断让low和high迫近第k小的数字并最后得到第k小的数字。
1. 我们每一次首先确定一个mid,然后搜索这个mid在这个matrix大于多少个数字。搜索的方法是这样的,从matrix[0][matrix[0].length]开始作为起点往左扫,当扫到第一个不大于mid的数字的时候,假设y轴上的index是j,那么就知道matrix[0][0...j]都小于等于mid,作一个counter,累计上这j + 1个数。接着x轴的index + 1但y轴上的index不变依然为j,然后继续往左扫,一直反复。这是利用了sorted matrix在column上依旧是sorted的条件。因为你matrix[0][j + 1 ... matrix[0].length - 1]都比mid大,所以matrix[1][j + 1 ... matrix[1].length - 1]肯定也都mid大,所以在column上面的index就可以维持在原来的位置即可。也因为这样,当row上的index走到了最后,你在column上的index也只需要走一个n(也就是数组一个维度的长度)即可。
2. 通过这样遍历,我们可以得到这个sorted matrix上,不大于mid的数字有多少个。走一遍这样的遍历的复杂度是O(n)。如果上面累计的counter大于k,我们就知道mid不在前k个数字的范围内,这个时候high就应该等于mid - 1,否则就知道mid就在前k个数字范围之内,可能就是第k个数字也说不定。有一个可选的做法是:你可以通过counter知道mid是否刚好大于等于k个数字,如果是的话,你可以在上述遍历的过程里记录mid大于等于的数字中最大的那个,那个就必然是第k个数字。具体可以参考https://www.jianshu.com/p/f16928ea675b 给出来的最后一组代码。但不这么做也可以,因为最后low和high还是会收束在第k大的数字和第k大的数字减一上面。 根据二分法的原理,最终,走了log(matrix[matrix.length][matrix[0].length - 1] - matrix[0][0])次数后,low和high会收束在kth smallest number上面。最终返回low就行。
public int kthSmallest(int[][] matrix, int k) {
int lo = matrix[0][0], hi = matrix[matrix.length - 1][matrix[0].length - 1];
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int j = matrix[0].length - 1;
int counter = 0;
for (int i = 0; i < matrix.length; i++) {
while (j >= 0 && matrix[i][j] > mid) j--;
counter += j + 1;
}
if (counter < k) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return lo;
}
这里,注意三个细节:while (j >= 0 && matrix[i][j] > mid); if (counter < k) ; return lo;
这些条件都不能搞错了。