Contents

【队列和栈】单调队列解决滑动窗口问题

【单调队列】主要是为了解决以下场景:

给你一个数组 window,已知其最值为 A,如果给 window 中添加⼀个数 B,那么比较一下 A 和 B 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。

可以使用 优先级队列 来动态寻找最值,通过创建一个大(小)顶堆,可以很快拿到最大(小)值。

但优先级队列无法满足标准队列结构【先进先出】的时间顺序,因为优先级队列底层利用二叉堆对元素进行动态排序,元素的出队顺序是元素的大小顺序,和入队的先后顺序完全没有关系。

而【单调队列】结构,既能够维护队列元素【先进先出】的时间顺序,又能够正确维护队列中所有元素的最值。

1 题目

力扣 239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

2 解析

2.1 搭建解题框架

🟧 普通队列的标准 API:

1
2
3
4
5
6
class Queue{
	// enqueue 操作,在队尾加⼊元素 n
	void push(int n);
	// dequeue 操作,删除队头元素
	void pop();
}

🟨 【单调队列】的 API:

1
2
3
4
5
6
7
8
class MonotonicQueue{
	// 在队尾添加元素 n
	void push(int n);
	// 返回当前队列中的最⼤值
	int max();
	// 队头元素如果是 n,删除它
	void pop(int n);
}

🟩 【滑动窗口】问题的解答框架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int[] maxSlidingWindow(int[] nums, int k) {
	MonotonicQueue window = new MonotonicQueue();
	List<Integer> res = new ArrayList<>();
	for (int i = 0; i < nums.length; i++) {
		if (i < k - 1) {
			//先把窗⼝的前 k - 1 填满
			window.push(nums[i]);
		} else {
			// 窗⼝开始向前滑动
			// 移⼊新元素
			window.push(nums[i]);
			// 将当前窗⼝中的最⼤元素记⼊结果
			res.add(window.max());
			// 移出最后的元素
			window.pop(nums[i - k + 1]);
		}
	}
	// 将 List 类型转化成 int[] 数组作为返回值
	int[] arr = new int[res.size()];
	for (int i = 0; i < res.size(); i++) {
		arr[i] = res.get(i);
	}
	return arr;
}

2.2 实现单调队列数据结构

🟠 实现「单调队列」必须使用一种数据结构支持在头部和尾部进行插入和删 除,很明显双链表满足这个条件。

🟡 由于只输出每个窗口内的最大元素,所以实现 push 方法时依然在队尾添加元素,但要把前面比自己小的元素都删掉。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class MonotonicQueue {
	// 双链表,⽀持头部和尾部增删元素
	// 维护其中的元素⾃尾部到头部单调递增
	private LinkedList<Integer> maxq = new LinkedList<>();
	// 在尾部添加⼀个元素 n,维护 maxq 的单调性质
	public void push(int n) {
		// 将前⾯⼩于⾃⼰的元素都删除
		while (!maxq.isEmpty() && maxq.getLast() < n) {
			maxq.pollLast();
		}
		maxq.addLast(n);
	}
}

🟢 最终单调队列的元素会保持 单调递减 的顺序,因此 max 方法只用返回队首元素:

1
2
3
4
public int max() {
	// 队头的元素肯定是最⼤的
	return maxq.getFirst();
}

🔵 pop 方法在队头删除元素 n :

1
2
3
4
5
public void pop(int n) {
	if (n == maxq.getFirst()) {
		maxq.pollFirst();
	}
}

完整代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* 单调队列的实现 */
class MonotonicQueue {
	LinkedList<Integer> maxq = new LinkedList<>();
	public void push(int n) {
		// 将⼩于 n 的元素全部删除
		while (!maxq.isEmpty() && maxq.getLast() < n) {
			maxq.pollLast();
		}
		// 然后将 n 加⼊尾部
		maxq.addLast(n);
	}
	public int max() {
		return maxq.getFirst();
	}
	public void pop(int n) {
		if (n == maxq.getFirst()) {
			maxq.pollFirst();
		}
	}
}

/* 解题函数的实现 */
int[] maxSlidingWindow(int[] nums, int k) {
	MonotonicQueue window = new MonotonicQueue();
	List<Integer> res = new ArrayList<>();
	for (int i = 0; i < nums.length; i++) {
		if (i < k - 1) {
			//先填满窗⼝的前 k - 1
			window.push(nums[i]);
		} else {
			// 窗⼝向前滑动,加⼊新数字
			window.push(nums[i]);
			// 记录当前窗⼝的最⼤值
			res.add(window.max());
			// 移出旧数字
			window.pop(nums[i - k + 1]);
		}
	}
	// 需要转成 int[] 数组再返回
	int[] arr = new int[res.size()];
	for (int i = 0; i < res.size(); i++) {
		arr[i] = res.get(i);
	}
	return arr;
}

🟣 nums 中的每个元素最多被 push 和 pop ⼀次,没有任何多余操作,所以整体的复杂度是 O(N)。空间复杂度就是窗口的大小 O(k)

参考资料:

https://labuladong.github.io/algo/