239. 滑动窗口最大值
暴力:遍历整个数组,对于每个窗口,遍历找出最大值,时间复杂度为 O(n * k)。
思路:主要优化寻找窗口最大值的过程,可以实现 O(1) 的时间复杂度,整个算法的时间复杂度就降低为 O(n * 1) = O(n)。实现的方法是构造单调队列,队内元素从大到小排列,队列首位即最大值。
每次滑动窗口,我们需要考虑两点:
1. 队首元素是否需要出队?保证队内的元素都属于当前窗口,而不是上个窗口。2. 队尾元素是否需要移除?保证队列的单调性。
假设窗口长度为 k,现在要让 index 为 i 的元素入队。
让我们先考虑第一点:
新元素入队,意味着窗口右端元素的 index 为 i,那么窗口左端元素的 index 应该为 i - k + 1。如果队首元素的 index < i - k + 1,则说明其不属于当前窗口,它会被新入队元素挤走。
接下来考虑第二点:
将新元素与队尾元素对比,如果队尾元素更小,则出队。重复此过程直到新元素小于队尾元素,此时新元素应该入队,形成新的单调队列。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# 这里选择将 index 存入队列
result = []
window = deque()
# 遍历数组
for i in range(len(nums)):
# 处理窗口左端,查看是否要出队
if window and window[0] < i - k + 1:
window.popleft()
# 处理窗口右端,查看是否要弹出一些元素后入队
while window and nums[i] > nums[window[-1]]:
window.pop()
window.append(i)
# 检查完窗口两端后,考虑收集结果
# 但在数组 nums 的开始部分,窗口还未形成前,无法收集元素。
if i >= k - 1:
result.append(nums[window[0]])
return result
# 时间复杂度:
# 每个元素最多入队一次,出队一次,两个操作都为O(1)
# 总时间复杂度为O(n)
347. 前 K 个高频元素
暴力:哈希表记录所有元素的频率,然后对频率排序,时间复杂度 O(n * log n)。
思路:主要优化寻找前 k 个最高频率,不依赖排序,而是采用优先队列(堆),时间复杂度可以降为 O(n * log k) 。
堆(Heap)是一种特殊的完全二叉树,它满足每个节点的值都不大于(或不小于)其子节点的值的性质。根据这个性质,堆可以分为两类:
- 最小堆(Min Heap):每个父节点的值都小于或等于其所有子节点的值。因此,树的根节点给出了全树的最小值。
- 最大堆(Max Heap):每个父节点的值都大于或等于其所有子节点的值。因此,树的根节点给出了全树的最大值。
堆通常用于实现优先队列,因为堆能够在 O(1) 时间复杂度内访问到最小元素(在最小堆中)或最大元素(在最大堆中),并且可以在 O(log n) 时间复杂度内完成插入新元素或删除最小(或最大)元素的操作。
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 统计元素频率
freq_map = {}
for num in nums:
if num in freq_map:
freq_map[num] += 1
else:
freq_map[num] = 1
# 如何在Python中构造最小堆?
# heapq 模块提供的堆操作是在普通的列表上进行的,所以不需要特殊的数据结构来表示堆。
min_heap = []
# 遍历 freq_map 字典,对于每个 (频率, 元素) 元组,使用 heapq.heappush() 将其加入到 min_heap 中。
# 由于 Python 的堆是最小堆,元组的第一个元素(即频率)被用来确定在堆中的顺序,频率最低的元素会位于堆顶。
for num, freq in freq_map.items():
heapq.heappush(min_heap, (freq, num))
# 检查堆的大小是否超过了 k
# 使用 heapq.heappop() 函数移除堆顶元素,即频率最低的元素。
# 这样做的目的是确保堆中始终只包含频率最高的 k 个元素。
if len(min_heap) > k:
heapq.heappop(min_heap)
# 从堆中提取结果
top_k_elements = [num for freq, num in min_heap]
return top_k_elements