相似题目:
- 【常考排序算法】堆排序.
- 【LeetCode】347. 前 K 个高频元素 (全手写).
- 【LeetCode】215. 数组中的第K个最大元素.
1.1 直接 sort()函数
但是面试这样写肯定不行。
补充 sort 与 sorted 区别:
- sort 是应用在 list 上的方法,sorted 可以对所有可迭代的对象进行排序操作。
- list 的sort方法返回的是对已经存在的列表进行操作,无返回值,而内建函数sorted方法返回的是一个新的 list,而不是在原来的基础上进行的操作。
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
arr.sort()
return arr[:k]
1.2 快速排序(排序完再取前 k 个) (时间:
)
直接对数组排序,排序后前k个数就是答案,排序一般较快的是,显然这并不是时间复杂度最优解。
1.3 最大堆(排序完再取前 k 个) (时间:
)
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
n = len(arr)
self.buildHeap(arr, n)
for i in range(n-1, -1, -1):
self.swap(arr, i, 0)
self.heapify(arr, i, 0)
return arr[:k]
def swap(self, arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def buildHeap(self, arr, n):
for i in range((n-2)//2, -1, -1):
self.heapify(arr, n, i)
def heapify(self, arr, n, i):
c1 = 2 * i + 1
c2 = 2 * i + 2
max = i
if c1 < n and arr[c1] > arr[max]:
max = c1
if c2 < n and arr[c2] > arr[max]:
max = c2
if max != i:
self.swap(arr, max, i)
self.heapify(arr, n, max)
1.4 最大堆 (时间:
; 空间: %3C%2Ftitle%3E%0A%3Cdefs%20aria-hidden%3D%22true%22%3E%0A%3Cpath%20stroke-width%3D%221%22%20id%3D%22E1-MJMATHI-4F%22%20d%3D%22M740%20435Q740%20320%20676%20213T511%2042T304%20-22Q207%20-22%20138%2035T51%20201Q50%20209%2050%20244Q50%20346%2098%20438T227%20601Q351%20704%20476%20704Q514%20704%20524%20703Q621%20689%20680%20617T740%20435ZM637%20476Q637%20565%20591%20615T476%20665Q396%20665%20322%20605Q242%20542%20200%20428T157%20216Q157%20126%20200%2073T314%2019Q404%2019%20485%2098T608%20313Q637%20408%20637%20476Z%22%3E%3C%2Fpath%3E%0A%3Cpath%20stroke-width%3D%221%22%20id%3D%22E1-MJMAIN-28%22%20d%3D%22M94%20250Q94%20319%20104%20381T127%20488T164%20576T202%20643T244%20695T277%20729T302%20750H315H319Q333%20750%20333%20741Q333%20738%20316%20720T275%20667T226%20581T184%20443T167%20250T184%2058T225%20-81T274%20-167T316%20-220T333%20-241Q333%20-250%20318%20-250H315H302L274%20-226Q180%20-141%20137%20-14T94%20250Z%22%3E%3C%2Fpath%3E%0A%3Cpath%20stroke-width%3D%221%22%20id%3D%22E1-MJMATHI-4E%22%20d%3D%22M234%20637Q231%20637%20226%20637Q201%20637%20196%20638T191%20649Q191%20676%20202%20682Q204%20683%20299%20683Q376%20683%20387%20683T401%20677Q612%20181%20616%20168L670%20381Q723%20592%20723%20606Q723%20633%20659%20637Q635%20637%20635%20648Q635%20650%20637%20660Q641%20676%20643%20679T653%20683Q656%20683%20684%20682T767%20680Q817%20680%20843%20681T873%20682Q888%20682%20888%20672Q888%20650%20880%20642Q878%20637%20858%20637Q787%20633%20769%20597L620%207Q618%200%20599%200Q585%200%20582%202Q579%205%20453%20305L326%20604L261%20344Q196%2088%20196%2079Q201%2046%20268%2046H278Q284%2041%20284%2038T282%2019Q278%206%20272%200H259Q228%202%20151%202Q123%202%20100%202T63%202T46%201Q31%201%2031%2010Q31%2014%2034%2026T39%2040Q41%2046%2062%2046Q130%2049%20150%2085Q154%2091%20221%20362L289%20634Q287%20635%20234%20637Z%22%3E%3C%2Fpath%3E%0A%3Cpath%20stroke-width%3D%221%22%20id%3D%22E1-MJMAIN-29%22%20d%3D%22M60%20749L64%20750Q69%20750%2074%20750H86L114%20726Q208%20641%20251%20514T294%20250Q294%20182%20284%20119T261%2012T224%20-76T186%20-143T145%20-194T113%20-227T90%20-246Q87%20-249%2086%20-250H74Q66%20-250%2063%20-250T58%20-247T55%20-238Q56%20-237%2066%20-225Q221%20-64%20221%20250T66%20725Q56%20737%2055%20738Q55%20746%2060%20749Z%22%3E%3C%2Fpath%3E%0A%3C%2Fdefs%3E%0A%3Cg%20stroke%3D%22currentColor%22%20fill%3D%22currentColor%22%20stroke-width%3D%220%22%20transform%3D%22matrix(1%200%200%20-1%200%200)%22%20aria-hidden%3D%22true%22%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMATHI-4F%22%20x%3D%220%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-28%22%20x%3D%22763%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMATHI-4E%22%20x%3D%221153%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%20%3Cuse%20xlink%3Ahref%3D%22%23E1-MJMAIN-29%22%20x%3D%222041%22%20y%3D%220%22%3E%3C%2Fuse%3E%0A%3C%2Fg%3E%0A%3C%2Fsvg%3E)
【求最大 top k 问题用最小堆,求最小 top k 问题用最大堆】
思路:用一个大顶堆实时维护数组的前
- 首先 build 一个长度为
- 随后从第
- 最后将大根堆里的数存入数组返回即可。(由于 C++ 语言中的堆(即优先队列)为大根堆,我们可以直接操作)。而 Python 语言中库中自带的为小顶堆,因此我们要对数组中所有的数取其相反数,才能使用小顶堆维护前 k 小值。
优点: 适合海量数据求k个最小。因为k个数的堆,空间是固定的,当数组超级大,那么全存入内存都变得不可行的时候,就需要从外存中慢慢读取数字,然后和这个堆进行比较。
而先排序再取前 k 个数的方法就必须吧整个数组放入内存中,才能运行,所以不适合海量数据。
法一:利用函数库 (推荐!!!):
import heapq
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k == 0:
return list()
hp = [-x for x in arr[:k]]
heapq.heapify(hp)
for i in range(k, len(arr)):
if -hp[0] > arr[i]:
heapq.heappop(hp)
heapq.heappush(hp, -arr[i])
ans = [-x for x in hp]
return
法二:手写大顶堆:
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k==0 or len(arr) == 0:
return []
n = len(arr)
nums = arr[:k] # 需要额外的 O(k) 的空间
self.buildHeap(nums, k) # 1. 建立长度为 k 的大顶堆
for i in range(k, n): # 2. 从 k+1 开始遍历,如果小于堆顶元素,则插入堆顶
if nums[0] > arr[i]:
nums[0] = arr[i] # 插入堆顶
self.heapify(nums,k, 0) # 插完以后,恢复大顶堆结构
return nums # 3. 返回这个长度为 k 的最大堆
def swap(self, arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def buildHeap(self, arr, k):
for i in range((k-2) // 2, -1, -1):
self.heapify(arr, k, i)
def heapify(self, arr, k, i):
c1 = 2 * i + 1
c2 = 2 * i + 2
max = i
if c1 < k and arr[c1] > arr[max]:
max = c1
if c2 < k and arr[c2] > arr[max]:
max = c2
if max != i:
self.swap(arr, max, i)
self.heapify(arr, k, max)
如果不做
nums = arr[:k]
这一步操作的话,而是直接将 arr[:k] 带入 buildHeap()函数中,那么buildHeap()需要返回值,否则,arr[:k] 并没有被改变,具体看以下代码
法三:
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k==0 or len(arr) == 0:
return []
n = len(arr)
# nums = arr[:k] # 需要额外的 O(k) 的空间
# self.buildHeap(nums, k) # 1. 建立长度为 k 的大顶堆
arr[:k] = self.buildHeap(arr[:k], k)
for i in range(k, n): # 2. 从 k+1 开始遍历,如果小于堆顶元素,则插入堆顶
if arr[0] > arr[i]:
arr[0] = arr[i] # 插入堆顶
self.heapify(arr,k, 0) # 插完以后,恢复大顶堆结构
return arr[:k] # 3. 返回这个长度为 k 的最大堆
def swap(self, arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def buildHeap(self, arr, k):
for i in range((k-2) // 2, -1, -1):
self.heapify(arr, k, i)
return arr
def heapify(self, arr, k, i):
c1 = 2 * i + 1
c2 = 2 * i + 2
max = i
if c1 < k and arr[c1] > arr[max]:
max = c1
if c2 < k and arr[c2] > arr[max]:
max = c2
if max != i:
self.swap(arr, max, i)
self.heapify(arr, k, max)
上述提到的问题,下面用简单代码说明:
def fun_1(arr):
arr[0], arr[1] = arr[1], arr[0]
def fun_2(arr):
arr[0], arr[1] = arr[1], arr[0]
return arr
a = [1,2,3,4]
fun_1(a[:2]) # a[:2] 没有被真正操作,是生成了一个副本进行操作
print('a:',a)
b = [5,6,7,8]
b[:2] = fun_2(b[:2]) # 函数返回值,再赋给 b[:2]
print('b:',b)
输出:
a: [1, 2, 3, 4]
b: [6, 5, 7, 8]
法四:嵌套函数(推荐)
采用嵌套函数,可以免去考虑上面提到的 arr[:k] 传参过程中的问题
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
def heapify(k, i):
c1 = 2 * i + 1
c2 = 2 * i + 2
max = i
if c1 < k and arr[c1] > arr[max]:
max = c1
if c2 < k and arr[c2] > arr[max]:
max = c2
if max != i:
arr[i], arr[max] = arr[max], arr[i]
heapify(k, max)
if k==0 or len(arr) == 0:
return []
n = len(arr)
# 建立最大堆
for i in range((k-2) // 2, -1, -1):
heapify(k, i)
for i in range(k, n): # 2. 从 k+1 开始遍历,如果小于堆顶元素,则插入堆顶
if arr[0] > arr[i]:
arr[0] = arr[i] # 插入堆顶
heapify(k, 0) # 插完以后,恢复大顶堆结构
return arr[:k] # 3. 返回这个长度为 k 的最大堆
复杂度:
- 时间复杂度:
(如果是排序后再选的话,复杂度最低达到 ,当
1.5 类似快速排序的算法 (期望时间:
,最坏
)
class Solution:
def partition(self, nums, l, r):
pivot = nums[r]
i = l - 1
for j in range(l, r):
if nums[j] <= pivot:
i += 1
nums[i], nums[j] = nums[j], nums[i]
nums[i + 1], nums[r] = nums[r], nums[i + 1]
return i + 1
def randomized_partition(self, nums, l, r):
i = random.randint(l, r)
nums[r], nums[i] = nums[i], nums[r]
return self.partition(nums, l, r)
def randomized_selected(self, arr, l, r, k):
pos = self.randomized_partition(arr, l, r)
num = pos - l + 1
if k < num:
self.randomized_selected(arr, l, pos - 1, k)
elif k > num:
self.randomized_selected(arr, pos + 1, r, k - num)
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k == 0:
return list()
self.randomized_selected(arr, 0, len(arr) - 1, k)
return arr[:k]
复杂度分析
- 时间复杂度:期望为 O(n) ,由于证明过程很繁琐,所以不再这里展开讲。具体证明可以参考《算法导论》第 9 章第 2 小节。
最坏情况下的时间复杂度为。情况最差时,每次的划分点都是最大值或最小值,一共需要划分 n−1 次,而一次划分需要线性的时间复杂度,所以最坏情况下时间复杂度为
- 空间复杂度:O(logn),递归调用的期望深度为 O(logn),每层需要的空间为 O(1),只有常数个变量。
最坏情况下的空间复杂度为 O(n)。最坏情况下需要划分 n 次,即 randomized_selected 函数递归调用最深 n−1 层,而每层由于需要 O(1) 的空间,所以一共需要 O(n) 的空间复杂度。
参考: LeetCode官方题解
大顶堆和这种方法的优劣性比较
在面试中,另一个常常问的问题就是这两种方法有何优劣。看起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,但是要注意到快速选择算法的几点局限性:
第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。
第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好。
参考:
- 【LeetCode】215. 数组中的第K个最大元素.
- 【常考排序算法】快速排序.
- 【常考排序算法】堆排序.
题主本硕机械专业,自学转互联网 算法岗成功,获得阿里、字节、美团、华为等 15+ offer
公众号 「苏学算法」