目录
一、概述
该博客结合leetcode原题介绍了“堆”的建立和操作,以及其常见题目。
主要要了解的堆的实现有两种:二叉树堆和Fibonacci堆(前者效率最低,后者效率最高)
1.1 二叉树堆
1.1.1 堆的定义
(1)首先,堆一定是一个完全二叉树
(2)最大堆:每个节点都比其子树所有节点大
(3)最小堆:每个节点都比其子树所有节点小
1.1.2 堆的插入
如下图,当要对堆插入元素时,将从完全二叉树的最后一个位置进行插入,即6号位置。
如下图,当插入58这个元素后,破坏了之前堆的有序性。
则将该元素与父节点进行替换,还不平衡的话再重复此操作,直至该元素小于父节点,此时,也一定恢复了堆的有序性。
由于堆的深度是logN,N是元素个数,所以插入的最坏情况时间复杂度是O(logN)。
1.1.3 堆的删除
如下图,对堆的删除操作只能在堆顶进行。可以想想是为什么,因为堆只满足自上而下的顺序,自下而上是没有顺序规律的。因此对下图的堆,我们的删除操作只能删除值为58的元素。
删除元素之后,将堆中最后一个元素,31放到堆顶(根)。此时发现堆顶元素不满足比所有子节点大的有序性。
因此我们将其和子节点交换位置,交换后如果依然比子节点大,则重复前一步,如下图所示。
1.1.4 堆的建立
堆的建立时间复杂度O(N)(具体证明方法参考【1】【2】)。方法是按照下图中的数字顺序,依次将对应圆圈中的二叉树通过和堆的插入一样的方法变成一个个小堆,最后整个二叉树都变成堆。
1.2 Fibonacci堆
二、例题
2.1 第K大的元素
#leetcode 703 第K大的元素
(1)暴力解法
*时间复杂度O(NlogN)
*空间复杂度O(K)
class KthLargest(object):
def __init__(self, k, nums):
"""
:type k: int
:type nums: List[int]
"""
self.nums = sorted(nums, reverse=True)
self.sort_k = self.nums[:k]
self.k = k
def add(self, val):
"""
:type val: int
:rtype: int
"""
self.sort_k = sorted(self.sort_k + [val], reverse=True)[:self.k]
return self.sort_k[-1]
(2)初步优化
*时间复杂度O(NKlogK)
*空间复杂度O(K)
class KthLargest(object):
def __init__(self, k, nums):
"""
:type k: int
:type nums: List[int]
"""
self.k = k
self.sort_k = []
for val in nums:
self.add(val)
def add(self, val):
"""
:type val: int
:rtype: int
"""
self.sort_k = sorted(self.sort_k + [val], reverse=True)[:self.k]
return self.sort_k[-1]
(3)使用优先队列
*时间复杂度O(NlogK)
*空间复杂度O(K)
import heapq
class KthLargest(object):
def __init__(self, k, nums):
"""
:type k: int
:type nums: List[int]
"""
self.heap = [] # 创建小顶堆(根是最小值)
self.k = k
self.nums = nums
for num in nums:
self.add(num)
def add(self, val):
"""
:type val: int
:rtype: int
"""
if len(self.heap) < self.k:
heapq.heappush(self.heap, val)
else:
if val > self.heap[0]:
heapq.heapreplace(self.heap, val) # 替换堆顶元素,并重新排序
return self.heap[0]
2.2 前K个高频元素
#Leetcode 347 前K个高频元素
(1)暴力解法
*时间复杂度O(NlogN)
*空间复杂度O(N)
class Solution:
def topKFrequent(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
freq_dict = {}
for v in nums:
freq_dict[v] = freq_dict.get(v, 0) + 1
res = freq_dict.items()
res.sort(key=lambda x:x[1], reverse=True)
return [res[i][0] for i in range(k)]
(2)使用优先队列
*时间复杂度O(N)
*空间复杂度O(N)
class Solution:
def topKFrequent(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
freq_dict = {} # 创建计数的存储空间
for v in nums:
freq_dict[v] = freq_dict.get(v, 0) + 1
heap = [] # 创建堆
for key, value in freq_dict.items():
if len(heap)<k: # 当堆还没满时,插入元素
heapq.heappush(heap, (value, key))
continue
if value>heap[0][0]: # 堆满时,若当前元素大于堆顶,则删除堆顶并插入
heapq.heapreplace(heap, (value, key))
res = []
while heap:
res.insert(0, heapq.heappop(heap)[1])
return res
2.3 返回滑动窗口中的最大值
#leetcode 239 返回滑动窗口中的最大值
1、利用优先队列
*时间复杂度:O(NlogN)
简述:维护一个大顶堆,滑动时做两个操作:顶是否过时(若是则删除)、插入新元素
def maxSlidingWindow(self, nums, k):
ret = []
# 初始化heap
heap = []
for idx, v in enumerate(nums[:k]):
heapq.heappush(heap, (-v, idx))
ret.append(-heapq.nsmallest(1, heap)[0][0])
# 开始滑动窗口
for idx, v in enumerate(nums[k:]):
idx_pop = idx
idx_insert = idx + k
while len(heap)>0 and heapq.nsmallest(1, heap)[0][1] <= idx_pop: # 先保证堆顶元素的坐标在滑动窗口内
heapq.heappop(heap)
heapq.heappush(heap, (-v, idx_insert))
ret.append(-heapq.nsmallest(1, heap)[0][0])
return ret
2、仅利用队列
*时间复杂度:O(N)
def maxSlidingWindow(self, nums, k):
ret, window = [], []
for i, v in enumerate(nums):
if i>=k and window[0]<=i-k: # 需要尾部删除
window.pop(0)
while window and nums[window[-1]]<=v: # 是否需要删除前朝老臣保证递减
window.pop()
window.append(i)
if i>=k-1 and window: # 需要输出结果
ret.append(nums[window[0]])
return ret
参考:
【1】堆和二叉查找树的建立的时间复杂度
【2】构建二叉堆时间复杂度的证明。