《算法导论》第 6 章
(二叉)堆是一种数据结构,它可以看成是一个近似于完全二叉树(除了最后一层外,其他层都被填满)的数组。
大根堆(max-heap):根节点大于等于左右节点。常用于堆排序。
小根堆(min-heap):根节点小于等于左右节点。常用于构建优先队列。
性质
堆结构上的基本操作的运行时间大多与树的高度成正比,即 logN
max-heapify: 时间复杂度 O(logN)
Build-max-heap: 从无序的数组中构造一个最大堆
heapsort: 堆排序
Max-heap-insert, heap-extract-max, heap-increase-key, heap-maximum 时间复杂度是 log(N), 功能是利用堆实现一个优先队列
def heapify(nums: List[int], length: int, i: int):
"""
以 i 为根节点构造 max-heap
时间复杂度:O(logN) N 是 length 大小
:param nums: 原始数组
:param length: 树的大小,或者说数组的长度
:param i:
:return:
"""
largest = i
l = 2 * i + 1
r = 2 * i + 2
# Tip: 有可能有存在左节点没有右节点的场景,所以不要提前判断 l r 的大小
if l < length and nums[largest] < nums[l]:
largest = l
if r < length and nums[largest] < nums[r]:
largest = r
if largest != i:
nums[i], nums[largest] = nums[largest], nums[i]
# Tip:此时 largest 所在的位置可能又不符合 max-heap 了所以要递归
heapify(nums, length, largest)
def build_max_heap(nums: List[int]):
"""
根据《算法导论》中的推算,时间复杂度是 O(N)
:param nums:
:return:
"""
length = len(nums)
# Tip: 叶子节点已经符合大根堆的性质,所以无需 heapify
for i in range(length // 2 - 1, -1, -1):
heapify(nums, length, i)
def heap_sort(nums: List[int]):
"""
时间复杂度:O(NlogN) O(N+NlogN) 即为 O(NlogN)
空间复杂度:O(1)
:param nums:
:return:
"""
length = len(nums)
build_max_heap(nums)
for i in range(length - 1, 0, -1):
nums[i], nums[0] = nums[0], nums[i]
heapify(nums, i, 0)
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。
在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。
提取最大元素
def heap_extract_max(nums: List[int]) -> int:
"""
此时 nums 是一个大根堆,提取最大元素
时间复杂度:O(logN)
:param nums:
:return:
"""
length = len(nums)
if length < 1:
return None
max_val = nums[0]
nums[0] = nums[length - 1]
# 重新构建大根堆
heapify(nums, length-1, 0)
return max_val
增大堆中某个位置的值
def heap_extract_max(nums: List[int]) -> int:
"""
此时 nums 是一个大根堆
时间复杂度:O(logN)
:param nums:
:return:
"""
length = len(nums)
if length < 1:
return None
max_val = nums[0]
nums[0] = nums[length - 1]
# 重新构建大根堆
heapify(nums, length - 1, 0)
return max_val
新增一个元素
def heap_insert(nums: List[int], key: int):
"""
新增一个元素
时间复杂度:O(logN)
:param nums:
:param key:
:return:
"""
length = len(nums)
nums.append(key)
heap_increase_key(nums, length, key)
if __name__ == "__main__":
nums2 = [5, 7, 6, 4, 1, 3, 6]
build_max_heap(nums2)
heap_insert(nums2, 10)
print(nums2) # [10, 7, 6, 5, 1, 3, 6, 4]
经典的使用 priority queue 解决的问题,其中也要发现一些技巧,比如向堆中 push 的时候避免重复采取的小技巧
class Solution:
def heapify(self, nums: List[List[int]], length: int, i: int):
"""
从 i 开始构建最小堆
:param i:
:param length:
:param nums:
:return:
"""
l, r = i * 2 + 1, i * 2 + 2
min_idx = i
if l < length and nums[l][2] < nums[min_idx][2]:
min_idx = l
if r < length and nums[r][2] < nums[min_idx][2]:
min_idx = r
if min_idx != i:
nums[i], nums[min_idx] = nums[min_idx], nums[i]
self.heapify(nums, length, min_idx)
def build_min_heap(self, nums: List[List[int]]):
length = len(nums)
# 最后一个非叶子节点所在索引
idx = length // 2 - 1
for i in range(idx, -1, -1):
self.heapify(nums, length, i)
def heap_extract_min(self, nums: List[List[int]]) -> List[int]:
min_val = nums[0]
length = len(nums)
nums[0], nums[length - 1] = nums[length - 1], nums[0]
nums.pop()
self.heapify(nums, length - 1, 0)
return min_val
def heap_decrease(self, nums: List[List[int]], i: int, val: List[int]):
"""
索引 i 处 修改为 val
:param nums:
:param i:
:param val:
:return:
"""
# 根节点
def get_root(idx: int) -> int:
return (idx + 1) // 2 - 1
nums[i] = val
while get_root(i) >= 0 and nums[get_root(i)][2] > nums[i][2]:
nums[get_root(i)], nums[i] = nums[i], nums[get_root(i)]
i = get_root(i)
def heap_push(self, nums: List[List[int]], val: List[int]):
nums.append(val)
length = len(nums)
self.heap_decrease(nums, length - 1, val)
def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
"""
已知最小的是 (0,0), 下一个就是待比较的就是 (0,1) 和 (1,0)
假如下一个是 (0,1) 那么下一个要比较的是 (1,0) (1,1) (0,2) => (1,0) (0,2)
假如下一个是 (1,0) 那么下一个要比较的是 (0,1) (1,1) (2,0) => (0,1) (2,0)
假如下一个是 (1,0) 那么下一个要比较的是 (0,2) (1,1) (2,0)
其实就是上次比较的数,加上新的 (a+1,b) (a,b+1),但是每次都这么增加其实带有重复的情况,比如
(0,0)
(0,1) (1,0)
选 (0,1) 则 (1,0) 再加入 (1,1) (0,2)
选 (1,0) 则 (1,1) (0,2) 再加入 (1,1) (2,0) 此时重复了 (1,1)
如果一开始我们就有 (0,0) (1,0) (2,0)...(k-1,0), 找到最小值 (a,b) 之后每次都添加 (a,b+1) 则变成了
(0,0)
(1,0)... 再加入 (0,1)
选 (0,1) 则 (1,0) (2,0) ... 再加入 (0,2)
选 (1,0) 则 (2,0) (0,2) ... 再加入 (1,1) 满足需求
建堆:O(N)
extract_min: O(logN)
heap_push: O(logN)
时间复杂度:O(klogk)
空间复杂度:O(k)
:param nums1:
:param nums2:
:param k:
:return:
"""
m, n = len(nums1), len(nums2)
nums = [[i, 0, nums1[i]+nums2[0]] for i in range(min(k, m))]
self.build_min_heap(nums)
res = []
while nums and len(res) < k:
[i, j, _] = self.heap_extract_min(nums)
res.append([nums1[i], nums2[j]])
if j+1 < n:
self.heap_push(nums, [i, j + 1, nums1[i]+nums2[j+1]])
return res
如果解决 topk 问题可以选 k 个数构建 Min-heap,之后遍历剩下的元素与 heap[0] 对比,如果大于其则加入堆中,直到最后构建出来由 top k 个数组成的堆,heap[0] 即为 topk。
如果解决 lowk 问题可以选 k 个数构建 Max-heap, 之后遍历剩下的元素与 heap[0] 对比,如果小于其则加入堆中,直到最后构建出来由 low k 个数组成的堆, heap[0] 即为 lowk。
TopK问题三种方法总结
263. Ugly Number
264. Ugly Number II
313. Super Ugly Number
方法 1:通过构建 min-heap,n 次向堆中加入质数的倍数,并在每一次提取最小值
class Solution:
def heapify(self, nums: List[int], length: int, idx: int):
if length == 0:
return
min_idx = idx
l = idx * 2 + 1
r = idx * 2 + 2
if l < length and nums[l] < nums[min_idx]:
min_idx = l
if r < length and nums[r] < nums[min_idx]:
min_idx = r
if min_idx != idx:
nums[min_idx], nums[idx] = nums[idx], nums[min_idx]
self.heapify(nums, length, min_idx)
def extract_min(self, nums: List[int]) -> int:
res = nums[0]
length = len(nums)
nums[0] = nums[length - 1]
nums.pop()
self.heapify(nums, length - 1, 0)
return res
def change_val(self, nums: List[int], idx: int, val: int):
nums[idx] = val
def root(index: int) -> int:
return (index + 1) // 2 - 1
while root(idx) >= 0 and nums[root(idx)] > nums[idx]:
nums[idx], nums[root(idx)] = nums[root(idx)], nums[idx]
idx = root(idx)
def insert_val(self, nums: List[int], val: int):
nums.append(val)
length = len(nums)
self.change_val(nums, length - 1, val)
def nthUglyNumber_0(self, n: int) -> int:
"""
通过构建 min-heap,每次选择 heap[0],并推进 heap[0]*[2,3,5] 下次继续选择 heap[0] 直到选到第 n 个值即可
时间复杂度:O(NlogN)
空间复杂度:O(N)
1
push 2 3 5
select 2
push 4 6 10
select 3
push 6 9 15 因此要用一个哈希表暂存加入过 heap 的值
:param n:
:return:
"""
nums = [1]
prime = [2, 3, 5]
hash_table = {1: True}
res = 0
for i in range(n):
res = self.extract_min(nums)
for val in prime:
if val * res not in hash_table:
self.insert_val(nums, val * res)
hash_table[val * res] = True
return res
方法 2:dp 获取,这种思路挺难想到的,利用了三个指针
class Solution:
def nthUglyNumber(self, n: int) -> int:
"""
dp 实现求第 n 个元素
我们知道第一个数是 1
第二批是 2 3 5 从中选一个小的 2
第三批就是 4 3 5 选一个小的 3
第四批就是 4 5 6 选 4
第五批就是 5 6 6 选 5
由此可见每一批数字都跟上一次选出来的结果有关,即问题的解是由子问题的最优解构成的,即符合最优子结构性质
状态转移方程:F(n) = min(F(f2)(n-1)*2, F(f3)(n-1)*3, F(f5)(n-1)*5)
边界条件:初值 f2=f3=f5=1, f2 指的是还没有使用乘 2 机会的已有结果的索引,它的前一位已经使用过了。
在每次循环中通过 min 函数对比选择了哪一个则哪一个 f指针 加 1,而且为了保证去重:相同的值则都加一
时间复杂度:O(n)
空间复杂度:O(n)
1
2 3 5 选 2 2 1 1
4 3 5 选 3 2 2 1
4 6 5 选 4 3 2 1
6 6 5 选 5 3 2 2
6 6 10 选 6 4 3 2
8 9 10 选 8 5 3 2
10 9 10 选 9 5 4 2
10 12 10 选 10 6 4 3
12 12 15 选 12 7 5 3
:param n:
:return:
"""
dp = [0] * (n + 1)
dp[1] = 1
idx2, idx3, idx5 = 1, 1, 1
for i in range(2, n + 1):
num2 = 2 * dp[idx2]
num3 = 3 * dp[idx3]
num5 = 5 * dp[idx5]
dp[i] = min(num2, num3, num5)
# Tip 不能用 else 语句,可以用 10 来验证下
if dp[i] == num2:
idx2 += 1
if dp[i] == num3:
idx3 += 1
if dp[i] == num5:
idx5 += 1
return dp[n]
很有意思的一道题,hard,因此不需要完全掌握,可以了解下思路。解决问题的根本思路在于求「包含边缘点」的元素的纵坐标最大值,「包含边缘点」定义为 start<=boundary
class Solution:
def getSkyline_0(self, buildings: List[List[int]]) -> List[List[int]]:
"""
以 [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]] 为例
如果使用合并数组的方式,元素 0 与 元素 1 合并得到结果若干个再与元素 2 合并,时间复杂度其实在 O(N^2) 左右。
但实际上答案总是在所有建筑物的边缘点上,只不过纵坐标有所差异
根据题解,其实是观察规律得到我们总是要寻找每个建筑物或者说区间内「包含边缘点」的纵坐标的最大值,比如 [2,9,10] 它其实包含 2<=x<=8
2 的值是 10, 9 并不包含其中,其值为 0, 我们遍历剩下的所有区间,可以得到「包含 2」的纵坐标最大值是 10,「包含 9」的纵坐标最大值是
12,其他类推,最终将得到的边缘点从小到大排序,剔除重复纵坐标的点则得到答案
时间复杂度:O(N^2)
空间复杂度:O(N)
2 10
9 0 12
3 15 10
7 0 12
5 12 15
12 0
15 10
20 0 8
19 8 10
24 0
排序后为 (2,10) (3,15) (5,15) (7,12) (9,12) (12,0) (15,10) (19,10) (20,8) (24,0)
剔除 (5,15) (9,12) (19,10) 变成
(2,10) (3,15) (7,12) (12,0) (15,10) (20,8) (24,0)
:param buildings:
:return:
"""
length = len(buildings)
boundaries = []
for i in range(length):
boundaries.append(buildings[i][0])
boundaries.append(buildings[i][1])
boundaries.sort()
res = []
for boundary in boundaries:
max_val = 0
for val in buildings:
if val[0] <= boundary < val[1]:
max_val = max(max_val, val[2])
if (res and res[-1][1] != max_val) or (not res):
res.append([boundary, max_val])
return res
但其实我们可以用优先队列来解决,我们以
buildings[x][2]
为对比值构建堆。我们向堆中插入 buildings 元素,直到 b u i l d i n g s [ x ] [ 0 ] < = b o u n d a r y buildings[x][0]<=boundary buildings[x][0]<=boundary,buildings 是从左向右排布的所以后面的肯定不包含 boundary,然后我们 pop 出队列里 b u i l d i n g s [ x ] [ 1 ] < = b o u n d a r y buildings[x][1]<=boundary buildings[x][1]<=boundary 的值,因为它们肯定不包含 boundary, 注意 boundary 是有序的,所以后面的 boundary 不受影响。
此时堆的根就是包含 boundary 的最高的建筑,以此向后类推直到得到答案,注意去除重复高度的点即可。
class Solution:
def heapify(self, nums: List[List[int]], length: int, idx: int):
l = idx * 2 + 1
r = idx * 2 + 2
max_idx = idx
if l < length and nums[l][1] > nums[max_idx][1]:
max_idx = l
if r < length and nums[r][1] > nums[max_idx][1]:
max_idx = r
if max_idx != idx:
nums[max_idx], nums[idx] = nums[idx], nums[max_idx]
self.heapify(nums, length, max_idx)
def extract_max(self, nums: List[List[int]]):
res = nums[0]
length = len(nums)
nums[0] = nums[length - 1]
nums.pop()
self.heapify(nums, length - 1, 0)
return res
def change_val(self, nums: List[List[int]], idx: int, val: List[int]):
nums[idx] = val
def root(idx: int) -> int:
return (idx + 1) // 2 - 1
while root(idx) >= 0 and nums[idx][1] > nums[root(idx)][1]:
nums[idx], nums[root(idx)] = nums[root(idx)], nums[idx]
idx = root(idx)
def insert_val(self, nums: List[List[int]], val: List[int]):
nums.append(val)
length = len(nums)
self.change_val(nums, length - 1, val)
def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
"""
使用优先队列解题,时间复杂度:O(NlogN) 对于边界 2 而言可能是 x1logN, 对于边界 3 而言是 x2logN,累加起来就是 NlogN
空间复杂度:O(N) 构建出来的堆 O(N), 边界数组 O(2N) N 是建筑数量
:param buildings:
:return:
"""
length = len(buildings)
boundaries = []
for i in range(length):
boundaries.append(buildings[i][0])
boundaries.append(buildings[i][1])
boundaries.sort()
# 使用 max-heap 构建优先队列
idx = 0
my_heap = []
res = []
for boundary in boundaries:
# tip: 为了寻找「包含 boundary」的建筑,将所有左边界小于等于 boundary 的建筑加到堆里去
while idx < length and buildings[idx][0] <= boundary:
self.insert_val(my_heap, [buildings[idx][1], buildings[idx][2]])
idx += 1
# tip: 此时构建了一个 max-heap 堆顶是高度最高的建筑,从堆中向外移除那些右边界小于等于 boundary 的建筑,因为这些建筑一定不会
# 「包含 boundary」,同时由于 boundary 是递增的,所以小于当前 boundary 的一定也小于之后的 boundary,将其剔除掉不会影响
# 后续 boundary 的高度的获取
while my_heap and my_heap[0][0] <= boundary:
self.extract_max(my_heap)
if my_heap:
# tip: 如果结果中上一位的高度等于当前得到的建筑高度,那么没必要加入
if not res or (res and res[-1][1] != my_heap[0][1]):
res.append([boundary, my_heap[0][1]])
else:
if not res or (res and res[-1][1] != 0):
res.append([boundary, 0])
return res