[LC] 378. Kth Smallest Element in a Sorted Matrix

感觉自己在这题上面是砸了很长的一段时间啊。这题目前来说可以有很多种解法,其中前三种都是围绕着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;

这些条件都不能搞错了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值