堆(Heap)是一种特殊的完全二叉树结构,具有以下核心特性:
这种数据结构之所以在算法领域广泛应用,主要得益于其高效的插入和删除操作时间复杂度(O(log n))以及获取极值的O(1)时间复杂度。在实际工程中,堆常用于解决以下几类问题:
关键理解:堆的物理存储通常使用数组实现,对于索引为i的节点,其左子节点索引为2i+1,右子节点为2i+2,父节点为⌊(i-1)/2⌋。这种紧凑的存储方式避免了指针开销,同时保持了高效的随机访问能力。
给定一个无序整数数组nums和整数k,需要返回数组中第k个最大的元素。最直观的解法是:
这种解法的时间复杂度为O(n log n),空间复杂度取决于排序算法,通常为O(1)或O(n)。
python复制def findKthLargest(nums, k):
nums.sort()
return nums[-k]
快速选择算法(Quickselect)是快速排序的变种,平均时间复杂度为O(n),最坏情况下为O(n²)。其核心思想是:
python复制import random
def findKthLargest(nums, k):
def partition(left, right, pivot_index):
pivot = nums[pivot_index]
# 将pivot移到末尾
nums[pivot_index], nums[right] = nums[right], nums[pivot_index]
store_index = left
for i in range(left, right):
if nums[i] > pivot: # 注意这里是大于,因为我们找第k大
nums[store_index], nums[i] = nums[i], nums[store_index]
store_index += 1
# 将pivot移到最终位置
nums[right], nums[store_index] = nums[store_index], nums[right]
return store_index
def select(left, right, k_smallest):
if left == right:
return nums[left]
# 随机选择pivot
pivot_index = random.randint(left, right)
# 执行分区操作
pivot_index = partition(left, right, pivot_index)
if k_smallest == pivot_index:
return nums[k_smallest]
elif k_smallest < pivot_index:
return select(left, pivot_index - 1, k_smallest)
else:
return select(pivot_index + 1, right, k_smallest)
return select(0, len(nums) - 1, k - 1)
使用最小堆可以在O(n log k)时间内解决问题,特别适合处理海量数据(无法一次性装入内存的情况):
python复制import heapq
def findKthLargest(nums, k):
heap = []
for num in nums:
if len(heap) < k:
heapq.heappush(heap, num)
else:
if num > heap[0]:
heapq.heappop(heap)
heapq.heappush(heap, num)
return heap[0]
性能对比:当k远小于n时,堆解法更优;当k接近n时,快速选择通常更快。在实际工程中,可以根据数据特征选择合适的方法。
最直接的解法分为三步:
python复制from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
return [item[0] for item in count.most_common(k)]
使用最小堆维护前k个高频元素,可以将时间复杂度优化到O(n log k):
python复制import heapq
from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
heap = []
for num, freq in count.items():
if len(heap) < k:
heapq.heappush(heap, (freq, num))
else:
if freq > heap[0][0]:
heapq.heappop(heap)
heapq.heappush(heap, (freq, num))
return [item[1] for item in heap]
当元素频率范围已知且不大时,可以使用桶排序将时间复杂度降到O(n):
python复制from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
max_freq = max(count.values())
# 创建频率桶
buckets = [[] for _ in range(max_freq + 1)]
for num, freq in count.items():
buckets[freq].append(num)
# 从高到低收集元素
res = []
for i in range(max_freq, -1, -1):
if buckets[i]:
res.extend(buckets[i])
if len(res) >= k:
break
return res[:k]
实际应用:在日志分析系统中,这种算法常用于统计高频错误码;在推荐系统中用于识别热门商品。
中位数问题要求能够:
最简单的解法是维护一个有序数组:
这种方法在小数据量时可行,但无法应对高频插入的场景。
使用最大堆和最小堆的组合可以将插入复杂度降到O(log n),查询保持O(1):
python复制import heapq
class MedianFinder:
def __init__(self):
self.max_heap = [] # 存储较小的一半(Python中通过存储负数模拟最大堆)
self.min_heap = [] # 存储较大的一半
def addNum(self, num):
if not self.max_heap or num <= -self.max_heap[0]:
heapq.heappush(self.max_heap, -num)
else:
heapq.heappush(self.min_heap, num)
# 平衡两个堆的大小
if len(self.max_heap) > len(self.min_heap) + 1:
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def findMedian(self):
if len(self.max_heap) == len(self.min_heap):
return (-self.max_heap[0] + self.min_heap[0]) / 2
else:
return -self.max_heap[0]
以插入序列[1,2,3]为例:
插入1:
插入2:
插入3:
此时中位数:
工程实践:这种双堆结构在金融交易系统中非常有用,可以实时计算价格中位数;在性能监控系统中用于分析请求延迟的中间值。
常规的建堆方法是逐个插入,时间复杂度为O(n log n)。实际上可以使用Floyd算法在O(n)时间内完成建堆:
python复制def heapify(arr):
n = len(arr)
for i in range(n//2 - 1, -1, -1):
sift_down(arr, i, n)
def sift_down(arr, i, n):
while True:
left = 2 * i + 1
right = 2 * i + 2
largest = i
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest == i:
break
arr[i], arr[largest] = arr[largest], arr[i]
i = largest
在处理超大规模数据时,可以考虑:
对于Top K问题,可以:
python复制from multiprocessing import Pool
def parallel_top_k(nums, k, workers=4):
chunk_size = len(nums) // workers
chunks = [nums[i:i+chunk_size] for i in range(0, len(nums), chunk_size)]
with Pool(workers) as p:
local_tops = p.map(lambda x: top_k_chunk(x, k), chunks)
merged = []
for top in local_tops:
merged.extend(top)
return top_k_chunk(merged, k)
def top_k_chunk(chunk, k):
count = Counter(chunk)
return heapq.nlargest(k, count.items(), key=lambda x: x[1])
常见错误包括:
调试建议:
浮点数计算可能导致精度问题,解决方案:
当处理海量数据时:
某电商平台的实时推荐系统需求:
解决方案:
高频交易系统需求:
解决方案:
日志分析系统需求:
解决方案: