378. 有序矩阵中第K小的元素

题目描述

给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是排序后的第 k 小元素,而不是第 k 个不同的元素。

示例:


matrix = [ [ 1, 5, 9], [10, 11, 13], [12, 13, 15] ], k = 8, 返回 13。


提示:你可以假设 k 的值永远是有效的,1 ≤ k ≤ n^2 

解题思路

今天没有再遇到很熟悉的题目,不过乍一看还是很眼熟,很像剑指offer的第一题。区别还是有的,主要区别是这道题要难一些。。。因为和剑指offer第一题很像,所以第一反应就是锯齿状对角线来这个二维遍历数组,但是没想明白这个思路该怎么继续下去,遂作罢;然后看到了部分有序,很快又反映到二分上,但是仍没想明白该怎么继续,又作罢(当然最后看官方题解时用的是二分+锯齿遍历,Orz,后面会有说到)。那么既然是部分有序,所以就又想到归并利用归并将这个部分有序的二维数组打平,变成一维的有序数组,然后第K小就是数组中[k-1]位置的元素。因为不用完全打平,只要到[k-1]位置上就可以了,所以cost要比纯暴力全打平稍好一点点,不过也就好一点点。

具体来说就是先取二维数组matrix的前两行做归并成一个有序数组,然后再用这个数组和第三行继续归并,一直循环,有点像spark里rdd.reduce(lambda a,b: a+b)这种操作。截止条件当下一行第一个元素大于已排序数组中[k-1]位置元素的时候,因为此时归并插入元素的位置都在第K小元素之后,前面的顺序不会再变了,就可以停止了。时间复杂度大约是

O(klogn)还大一些吧,对不起,我时间复杂度这块确实很不好。如果一直没有触发截止条件,则需要将整个二维数组完全打平,k的最坏值为n^2,所以时间复杂度最坏O(n^2logn)。其实单就归并思路来说,这种方法也不是最好的,在归并步骤上还可以改进。在这个方法我是逐行依次归并的,其实应该可以多行两两归并,官方题解中说要用小根堆什么的,还给出了参考例题:leetccde 23. 合并K个排序链表。我看官方题解的代码里似乎调用了现有的方法,实际面试可能不让这么做,所以也没细看。具体如何更好地归并,还是等遇到题目时再说吧,毕竟还要看这道题的官方题解呢,怕一下太多吃不透,谁让我小白呢。那其实多行两两归并的时候复杂度应该就稳定在O(klogn),而不像这个方法,要比这个时间复杂度大一些。

题解代码:

def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
        # 归并排序
        def guibing(s1,s2):  
        # 我知道函数内套函数很蠢,但是我觉得面试时候面试官应该是会让手撕归并的,况且也不难是吧
            res = []
            i = 0
            j = 0
            while i < len(s1) and j < len(s2):
                if s1[i] < s2[j]:
                    res.append(s1[i])
                    i += 1
                else:
                    res.append(s2[j])
                    j += 1
            if i == len(s1):
                res +=s2[j:] 
            else:
                res += s1[i:]
            return res
        
        tmp = matrix[0]
        for i in range(1,len(matrix)):
            nums = matrix[i]
            tmp = guibing(tmp,nums)  # 依次逐行归并
            p = len(tmp)
            if i < len(matrix)-1 and p >= k:  
            '''保证不是最后一行防止超出index的范围;
                同时保证已归并的有序数组长度大于k才会判断是否到达截止条件'''
                if matrix[i+1][0] > tmp[k-1]:  # 到达截止条件
                    return tmp[k-1]
        return tmp[k-1]

运行结果:

python 提取矩阵每个元素_leetcode

不过这方法真的很烂啊,效率可太糟糕了。不过也正常,因为这个方法只利用了每行元素递增这一个性质,每列也递增这个性质并没有用到。那么官方题解就都用到了,所以效果就很好。

官方思路

前面有说官方思路是二分+锯齿搜索。大体来说和剑指offer第一题很相似,就是从右上或者左下开始锯齿状遍历数组内的元素,与所pick的数进行比较,要求数组内左边的数小于pick的数,右边的数大于pick的数,并且上边的数小于pick的数,下边的数大于pick的数。将这一系列临界点连起来就是锯齿状的边界。在遍历中每一次比较的时候,如果pick的数大于当前遍历到的数组内的数,则这个数组内的数,它的左边和上边的所有数都小于pick的数,那么所有左边和上边的数的个数如果小于k,说明我们所pick的数并不合适,不是第k小的数。那么这个pick的数怎么找呢,就要用到二分了。二分的左边界是matrix[0][0],右边界是matrix[-1][-1],因为所有数的范围都不会超过这两个边界。然后直接用二分确定pick的数mid = (left + right) // 2。这样做下来时间复杂度在

O(nlog(rightleft)),因为二分查找进行次数为O(log(rightleft)),每次操作锯齿状遍历的时间复杂度为 O(n)。

具体解释起来有点麻烦,主要是很难说清楚,官方题解里解释得非常清楚,还有图,所以我就不做搬运工了,直接贴上链接:官方题解,记得看的是方法三哈。代码是我看完题解自己写的,不过对照下来似乎一模一样,细节的解释部分我都放在注释里了,应该还算清楚。

def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
        n = len(matrix)

        def checknum(mid):
            # check数组中比mid小的元素是否多于k个
            i = n-1  # 设定初始位置,从左下角开始
            j = 0
            counter = 0  # 记录其左上部分有几个元素,即比mid小的有几个元素
            while i >= 0 and j < n:
                if matrix[i][j] <= mid:  # 未到边界且mid较大,按列记,先将这一列的元素计入
                    counter += i+1  # +1是因为i是index,实际个数要在index上+1
                    j += 1  # 右移
                else:
                    i -= 1  # 上移
            return counter >= k  # 比mid小的元素个数与k比较

        left = matrix[0][0]
        right = matrix[-1][-1]  # 定义左右边界
        while left < right:  # 循环内进行二分
            mid = (left + right) // 2
            if checknum(mid):
                right = mid
            else:
                left = mid+1  # 注意每次更新的边界,别把mid漏了
        return left

运行结果:

python 提取矩阵每个元素_算法_02

以上就是我,一个正儿八经的小白(大神们通过看代码应该也感觉出来了),对这道题的理解,欢迎诸位指正讨论,感谢阅读。