
滑动窗口中位数问题要求在一个固定大小k的滑动窗口在数组上移动时,计算每个窗口内的中位数。双堆法是解决此问题的常用且高效策略:
通过维护这两个堆,使得大顶堆的堆顶元素小于或等于小顶堆的堆顶元素,并且两个堆的大小差不超过1。这样,中位数可以直接从堆顶元素中获取。
在原始的双堆实现中,当窗口滑动时,需要移除窗口最左侧的元素,并添加最右侧的新元素。问题通常出现在移除旧元素的操作上:
def popNum(self, num):
if num > (self.small[0] * -1):
self.large.remove(num) # O(K) 查找和移除
heapq.heapify(self.large) # O(K) 重新堆化
else:
self.small.remove(num * -1) # O(K) 查找和移除
heapq.heapify(self.small) # O(K) 重新堆化
self.balance()list.remove(num) 操作在Python中需要遍历列表来查找元素,其时间复杂度为O(K)(K为堆的大小)。移除元素后,堆的结构被破坏,需要调用heapq.heapify()来重新构建堆,这同样是O(K)的操作。因此,每次移除元素的总时间复杂度为O(K)。
对于包含N个元素的数组和大小为K的窗口,总时间复杂度将是O(N * K),当N和K都很大时(例如N=100000, K=50000),这将导致严重的时间限制超出(TLE)。
为了将移除操作的复杂度降低到O(log K),我们采用以下策略:
与其在堆中物理移除元素,不如采用“惰性删除”策略:
由于数组中可能存在重复值,仅仅通过值来判断元素是否在窗口外是不够的。例如,窗口中可能有多个5,当一个5离开窗口时,我们不能错误地删除另一个仍在窗口中的5。
解决方案是,将每个元素存储为(值, 原始索引)的元组。这样,每个元素在堆中都有一个唯一的标识符。
为了实现惰性删除,我们可以利用滑动窗口的特性。当窗口从[start, end]移动到[start+1, end+1]时,元素nums[start]离开窗口。我们可以设置一个lowindex,表示当前窗口的起始索引。任何索引小于lowindex的元素都应被视为已删除。
我们将构建两个自定义的堆类MinWindowHeap和MaxWindowHeap,它们基于Python的heapq模块,并加入了惰性删除逻辑。
import heapq
class MinWindowHeap(object):
def __init__(self, conv=lambda x: x):
self.heap = [] # 存储 (值, 索引) 元组的堆
self.conv = conv # 转换函数,用于MaxHeap将值取负
self.lowindex = 0 # 当前窗口的起始索引,小于此索引的元素被视为已删除
def peek(self):
"""返回堆顶的有效元素,跳过所有已删除的元素。"""
while self.heap:
item = self.conv(self.heap[0]) # 获取堆顶元素(可能经过转换)
if item[1] >= self.lowindex: # 如果元素的索引大于等于lowindex,则为有效元素
return item
heapq.heappop(self.heap) # 否则,该元素已过期,将其弹出并继续查找
def push(self, item):
"""将 (值, 索引) 元组推入堆中。"""
heapq.heappush(self.heap, self.conv(item))
def pop(self):
"""弹出并返回堆顶的有效元素。"""
item = self.peek() # 先通过peek找到并移除所有无效元素
heapq.heappop(self.heap) # 弹出有效的堆顶元素
return itemMaxWindowHeap通过对值取负来实现大顶堆的功能,其余逻辑与MinWindowHeap相同。
def negate(item): # 辅助函数,用于将 (值, 索引) 中的值取负
return -item[0], item[1]
class MaxWindowHeap(MinWindowHeap):
def __init__(self):
super(MaxWindowHeap, self).__init__(negate) # 传入negate函数Solution类将协调两个自定义堆的操作,实现滑动窗口中位数的计算。
class Solution(object):
def rebalance(self, add):
"""
重新平衡两个堆的大小。
balance > 0 表示 large 堆元素多,balance < 0 表示 small 堆元素多。
"""
self.balance += add
if abs(self.balance) < 2: # 堆大小差在1以内,无需平衡
return
if self.balance > 1: # large 堆元素过多,将 large 堆顶移到 small 堆
self.small.push(self.large.pop())
elif self.balance < -1: # small 堆元素过多,将 small 堆顶移到 large 堆
self.large.push(self.small.pop())
self.balance = 0 # 平衡后重置 balance
def insert(self, item):
"""
将新元素 (值, 索引) 插入到合适的堆中,并进行平衡。
"""
pivot = self.large.peek() or self.small.peek() # 找到一个参考点来决定插入哪个堆
islarge = not pivot or item[0] > pivot[0] # 如果新元素大于参考点,则属于large堆
heap = self.large if islarge else self.small
heap.push(item)
self.rebalance(1 if islarge else -1) # 更新 balance 并平衡
def remove(self, item):
"""
处理旧元素 (值, 索引) 离开窗口。
注意:这里不实际从堆中移除,而是更新 lowindex 来标记其为无效。
"""
pivot = self.large.peek()
islarge = pivot and item[0] >= pivot[0] # 判断旧元素原来属于哪个堆
# 关键步骤:更新 lowindex。所有索引小于 item[1]+1 的元素都将视为已删除。
self.large.lowindex = self.small.lowindex = item[1] + 1
self.rebalance(-1 if islarge else 1) # 更新 balance 并平衡
def getMedian(self):
"""
获取当前窗口的中位数。
"""
if self.balance == 0: # 两个堆大小相等,中位数是两者堆顶的平均值
return (self.large.peek()[0] + self.small.peek()[0]) * 0.5
return self.large.peek()[0] if self.balance > 0 else self.small.peek()[0] # 堆大小不等,中位数是较大堆的堆顶
def medianSlidingWindow(self, nums, k):
"""
主函数:计算滑动窗口中位数。
"""
self.small = MaxWindowHeap()
self.large = MinWindowHeap()
self.balance = 0 # 用于跟踪两个堆的有效元素数量差
# 将原始数组转换为 (值, 索引) 元组列表
items = [(val, i) for i, val in enumerate(nums)]
# 初始化第一个窗口
for item in items[:k]:
self.insert(item)
result = [self.getMedian()]
# 滑动窗口
for olditem, item in zip(items, items[k:]):
self.remove(olditem) # 移除旧元素(惰性删除)
self.insert(item) # 插入新元素
result.append(self.getMedian()) # 获取当前窗口中位数
return result相比于原始的O(N * K)复杂度,O(N log K)在处理大数据集时具有显著的性能优势,能够有效避免TLE。
通过上述优化,我们成功解决了滑动窗口中位数问题中因低效移除操作导致的TLE问题,提供了一个既高效又健壮的解决方案。
以上就是优化滑动窗口中位数:使用惰性删除与双堆策略解决TLE问题的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号