1. 概述
给定一个数组array,大小为n,找到其中第k大的数或者前k大的数。这是一个电面高频题,有快排,堆排等多种思路来解题,本文章将从时间复杂度和空间复杂度,来介绍这些算法以及它们所适用的场景
2. 堆排
直观上理解找到第k大或者前k大的数,用的是最大堆,但实际上最小堆也是可以的,至于具体使用哪一种,则取决于问题的场景。一般来说,面试答的是最小堆(省空间)
2.1 最大堆
要找第k大的元素,那么heapify一个max heap,然后既然是要找第k大的元素,max heap顶端是最大的,第k大的元素就是从最底层向上的第k个元素,实现方法只要heapify后pop k次,pop所得到的数便是前k大的数,最后一次pop便是第k大的数。
import heapq
class Solution(object):
def findKthLargest(self, nums, k):
nums = [-num for num in nums]
heapq.heapify(nums)
res = float('inf')
for _ in range(k):
res = heapq.heappop(nums)
return -res
首先解释下为什么要nums=[-num for num in nums].
因为Python的heapq库调用heapify的时候,用的永远是一个min heap,然后因为没有max heap的实现,便用min heap来模拟max heap的运算,最简单的就是将所有的数变成-num.
时间复杂度:
O
(
n
+
k
l
o
g
n
)
O(n+klogn)
O(n+klogn),heapify用了O(n),然后一共pop了k个元素,每个元素使用logn的时间复杂度,所以一共是O(n+klogn)
空间复杂度:
O
(
n
)
O(n)
O(n),用了整个数组空间进行堆化,因此空间复杂度是O(n)
2.2 最小堆
- 初始化一个size为k的数组,这k个数为输入数组array的前k的数,然后对这个数组进行最小堆的heapify,得到min heap
- array数组中剩下的n-k的数依次与min heap比较,只要这个数比堆顶的数要大,那么就把最小值pop出来,该值放入堆顶做siftdown;否则这个数直接作为出来的一个数,堆不变,这样保证了出来的n-k个数都是比min heap中的k的数要大的,那么min heap中的k个数便是最大的k个数
import heapq
class Solution(object):
def findKthLargest(self, nums, k):
min_heap = nums[:k]
heapq.heapify(min_heap)
for i in range(k, len(nums)):
if nums[i] > min_heap[0]:
heapq.heappop(min_heap)
heapq.heappush(min_heap, nums[i])
return min_heap[0]
时间复杂度:
O
(
k
)
+
O
(
(
n
−
k
)
∗
l
o
g
k
)
O(k)+O((n-k)*logk)
O(k)+O((n−k)∗logk)
空间复杂度:
O
(
k
)
O(k)
O(k)
2.3 最小堆VS最大堆
堆 | 时间复杂度 | 空间复杂度 |
---|---|---|
最大堆 | O ( n + k l o g n ) O(n+klogn) O(n+klogn) | O(n) |
最小堆 | O ( k ) + O ( ( n − k ) ∗ l o g k ) O(k)+O((n-k)*logk) O(k)+O((n−k)∗logk) | O ( k ) O(k) O(k) |
时间复杂度上进行对比:
- 如果考虑k无限接近于n,
最大堆: O ( n + n l o g n ) ≈ O ( n l o g n ) O(n+nlogn) \approx O(nlogn) O(n+nlogn)≈O(nlogn)
最小堆: O ( n + l o g k ) ≈ O ( n ) O(n+logk) \approx O(n) O(n+logk)≈O(n) - 如果考虑k=0.5
最大堆: O ( n + n l o g n ) O(n+nlogn) O(n+nlogn)
最小堆: O ( n + n l o g n ) O(n+nlogn) O(n+nlogn) - 如果考虑n无限大
最大堆:O(constantn)
最小堆:O(nlogk)
3. 快排
class Solution(object):
res = 0
def findKthLargest(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: int
"""
self.helper(nums, len(nums)-k+1, 0, len(nums)-1)
return self.res
def helper(self, nums, k, low, high):
pvt_pos = self.partion(nums, low, high)
if pvt_pos - low + 1 == k:
self.res = nums[pvt_pos]
return
elif pvt_pos - low + 1 > k:
self.helper(nums, k, low, pvt_pos-1)
else:
self.helper(nums, k-pvt_pos+low-1, pvt_pos+1, high)
def partion(self, nums, low, high):
pivot = nums[low]
while low < high:
while low < high and nums[high] >= pivot:
high -= 1
nums[low] = nums[high]
while low < high and nums[low] <= pivot:
low += 1
nums[high] = nums[low]
nums[low] = pivot
return low
sol = Solution()
nums = [3,2,1,5,6,4]
print(sol.findKthLargest(nums, 2))
print(nums)
时间复杂度
快排查找确定轴心来划分数组
所以时间复杂度为
O
=
s
u
m
(
N
+
N
/
2
+
N
/
4
+
N
/
8
+
.
.
.
)
O = sum(N + N/2 + N/4 + N/8 + ...)
O=sum(N+N/2+N/4+N/8+...)
等比数列求和
S
n
=
a
1
(
1
−
q
n
)
/
(
1
−
q
)
S_n = a_1(1 - q^n) / (1 - q)
Sn=a1(1−qn)/(1−q)
解得
O
=
2
N
=
O
(
n
)
O = 2N=O(n)
O=2N=O(n)
注意平均复杂度是
O
(
n
)
O(n)
O(n),最坏情况下(比如数组基本有序,找最大的数partion选的轴是第一个数)还是
O
(
n
2
)
O(n^2)
O(n2)
4.BFPRT算法
BFPRT算法解决的问题十分经典,即从某n个元素的序列中选出第k大(第k小)的元素,通过巧妙的分 析,BFPRT可以保证在最坏情况下仍为线性时间复杂度。该算法的思想与快速排序思想相似,当然,为使得算法在最坏情况下,依然能达到o(n)的时间复杂 度,五位算法作者做了精妙的处理。
算法步骤:
-
将n个元素每5个一组,分成n/5(上界)组。
-
取出每一组的中位数,任意排序方法,比如插入排序。
-
递归的调用selection算法查找上一步中所有中位数的中位数,设为x,偶数个中位数的情况下设定为选取中间小的一个。
-
用x来分割数组,设小于等于x的个数为k,大于x的个数即为n-k。
-
若i==k,返回x;若i<k,在小于x的元素中递归查找第i小的元素;若i>k,在大于x的元素中递归查找第i-k小的元素。
终止条件:n=1时,返回的即是i小元素。