接雨水、滑动窗口最大值、数据流中位数——三道经典难题的完整攻略
前三十三课我们掌握了各种单一技巧,但面试中的难题往往需要多种技巧的组合。本课选取三道经典的 Hard 题,分别代表:双指针思维、单调队列、堆的组合设计。它们是面试中的"试金石",能做出来说明你已经具备通关实力。
题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
min(max_left[i], max_right[i])。双指针从两端向中间移动,维护左最大值和右最大值。哪边小就处理哪边,因为小的一边决定了水位上限。class Solution:
def trap(self, height: List[int]) -> int:
if not height:
return 0
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
water = 0
while left < right:
if left_max < right_max:
left += 1
left_max = max(left_max, height[left])
water += left_max - height[left]
else:
right -= 1
right_max = max(right_max, height[right])
water += right_max - height[right]
return water
left_max[i](位置i及左侧的最高值),再从右到左计算 right_max[i]。然后遍历每个位置,水 = min(left_max[i], right_max[i]) - height[i]。def trap_dp(height):
n = len(height)
if n == 0: return 0
left_max = [0] * n
right_max = [0] * n
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i-1], height[i])
right_max[n-1] = height[n-1]
for i in range(n-2, -1, -1):
right_max[i] = max(right_max[i+1], height[i])
return sum(min(left_max[i], right_max[i]) - height[i] for i in range(n))
def trap_stack(height):
stack = []
water = 0
for i, h in enumerate(height):
while stack and h > height[stack[-1]]:
bottom = stack.pop()
if not stack:
break
left = stack[-1]
width = i - left - 1
depth = min(height[left], h) - height[bottom]
water += width * depth
stack.append(i)
return water
题目描述:给定一个数组和滑动窗口大小 k,返回每个窗口中的最大值。
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
dq = deque() # 存索引,对应的值递减
result = []
for i, num in enumerate(nums):
# 移除比当前元素小的(它们不会成为最大值)
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
# 移除已离开窗口的元素
if dq[0] <= i - k:
dq.popleft()
# 窗口形成后开始记录
if i >= k - 1:
result.append(nums[dq[0]])
return result
时间复杂度:O(n) — 每个元素最多入队出队各一次
空间复杂度:O(k) — 队列最多存 k 个元素
# 单调递减队列通用模板
from collections import deque
def monotonic_queue(nums, k):
dq = deque()
result = []
for i, num in enumerate(nums):
# 1. 维护单调性
while dq and nums[dq[-1]] < num: # 严格递减
dq.pop()
dq.append(i)
# 2. 移除过期元素
if dq[0] < i - k + 1:
dq.popleft()
# 3. 取结果
if i >= k - 1:
result.append(nums[dq[0]])
return result
题目描述:设计一个支持从数据流中添加整数并查找当前中位数的数据结构。
import heapq
class MedianFinder:
def __init__(self):
self.small = [] # 最大堆(存负值)
self.large = [] # 最小堆
def addNum(self, num: int) -> None:
# 先放入最大堆
heapq.heappush(self.small, -num)
# 最大堆堆顶移到最小堆
heapq.heappush(self.large, -heapq.heappop(self.small))
# 平衡:最大堆可以多一个元素
if len(self.large) > len(self.small):
heapq.heappush(self.small, -heapq.heappop(self.large))
def findMedian(self) -> float:
if len(self.small) > len(self.large):
return -self.small[0] # 奇数个
return (-self.small[0] + self.large[0]) / 2.0 # 偶数个
addNum:O(log n)
findMedian:O(1)
空间复杂度:O(n)
heappush(small, -num),取出时 -small[0]。addNum 的三步操作看似复杂,其实是为了保证两个性质:(1) small 的所有元素 ≤ large;(2) small 的元素数 ≥ large 的元素数(最多多1)。这三道题虽然属于不同领域,但解题思路有共同规律:
# LC 42 接雨水
暴力 O(n²) → DP预处理 O(n) → 双指针 O(1)空间
# LC 239 滑动窗口最大值
暴力 O(nk) → 堆 O(n log k) → 单调队列 O(n)
# LC 295 中位数
排序 O(n log n) → 插入排序 O(n) → 双堆 O(log n)
每道题的最优解都不是单一数据结构,而是巧妙组合:
34课覆盖了算法面试 95% 以上的考点。剩下的是多练、多总结、多复盘。祝面试顺利!🎉
从零开始到面试通关的学习路线:
总计约 34课 × 8题 = 272道题,覆盖面试高频题的 80% 以上。坚持 10 周,算法面试无忧。