当前位置:   article > 正文

【小白用python刷Leetcode】378. 有序矩阵中第K小的元素_矩阵第k小 python

矩阵第k小 python

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),而不像这个方法,要比这个时间复杂度大一些。

题解代码:

  1. def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
  2. # 归并排序
  3. def guibing(s1,s2):
  4. # 我知道函数内套函数很蠢,但是我觉得面试时候面试官应该是会让手撕归并的,况且也不难是吧
  5. res = []
  6. i = 0
  7. j = 0
  8. while i < len(s1) and j < len(s2):
  9. if s1[i] < s2[j]:
  10. res.append(s1[i])
  11. i += 1
  12. else:
  13. res.append(s2[j])
  14. j += 1
  15. if i == len(s1):
  16. res +=s2[j:]
  17. else:
  18. res += s1[i:]
  19. return res
  20. tmp = matrix[0]
  21. for i in range(1,len(matrix)):
  22. nums = matrix[i]
  23. tmp = guibing(tmp,nums) # 依次逐行归并
  24. p = len(tmp)
  25. if i < len(matrix)-1 and p >= k:
  26. '''保证不是最后一行防止超出index的范围;
  27. 同时保证已归并的有序数组长度大于k才会判断是否到达截止条件'''
  28. if matrix[i+1][0] > tmp[k-1]: # 到达截止条件
  29. return tmp[k-1]
  30. return tmp[k-1]

运行结果:

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

官方思路

前面有说官方思路是二分+锯齿搜索。大体来说和剑指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)。

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

  1. def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
  2. n = len(matrix)
  3. def checknum(mid):
  4. # check数组中比mid小的元素是否多于k个
  5. i = n-1 # 设定初始位置,从左下角开始
  6. j = 0
  7. counter = 0 # 记录其左上部分有几个元素,即比mid小的有几个元素
  8. while i >= 0 and j < n:
  9. if matrix[i][j] <= mid: # 未到边界且mid较大,按列记,先将这一列的元素计入
  10. counter += i+1 # +1是因为i是index,实际个数要在index上+1
  11. j += 1 # 右移
  12. else:
  13. i -= 1 # 上移
  14. return counter >= k # 比mid小的元素个数与k比较
  15. left = matrix[0][0]
  16. right = matrix[-1][-1] # 定义左右边界
  17. while left < right: # 循环内进行二分
  18. mid = (left + right) // 2
  19. if checknum(mid):
  20. right = mid
  21. else:
  22. left = mid+1 # 注意每次更新的边界,别把mid漏了
  23. return left

运行结果:

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

原题链接:

https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/982687
推荐阅读
相关标签
  

闽ICP备14008679号