单调队列及应用
2019年9月11日单调队列
单调队列是一种优先队列,要求元素是可排序的,或传入比较器。单调队列继承队列的以下性质:
- 可从队尾插入元素。
- 可从队头弹出元素。
- 在队列中的所有元素里,队头元素一定是最早插入的。在队列中的所有元素里,队尾元素一定是最晚插入的。(FIFO)
单调队列有以下特殊性质:
- 一个元素插入队列后,即使没有手动弹出,也不一定存在于该队列。
var queue=new MonotoneQueue();
queue.Enqueue(a);
queue.Enqueue(b);
Debug.Assert(queue.Contains(a) || queue.Contains(a) == false);单调队列是个很小众的数据结构。
应用与实现
单调队列的一个应用是LeetCode的一道题
Given an array nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.
Example:
Input: nums = [1,3,-1,-3,5,3,6,7], and k = 3 Output: [3,3,5,5,6,7] Explanation: Window position Max --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7Note:
You may assume k is always valid, 1 ≤ k ≤ input array’s size for non-empty array.
作为单调队列的初学者,我们直接看答案。
这里使用一个单调递减队列,队头是最大的元素,队尾(即队列入口)为最小的元素。
对[1, 3, -1]依次入队
| Queue | 注释 | 
|---|---|
| 1 | 1入队 | 
| 3 | 3入队,但1<3,所以从右端弹出1,再插入3 | 
| 3 -1 | -1入队 | 
这是算法的第一步,比较简单。在继续之前,回忆单调队列的定义:单调队列是一种优先队列,要求元素是可排序的,或传入比较器。从定义中发现,1、元素携带一个Priority属性,该属性用于排序;2、元素允许携带其他数据。
题目要求超过窗口的元素要删掉,所以单调队列还必须记录元素索引。于是使用一个类保存索引和排序用的Priority。
[DebuggerDisplay("Index={Index}, Value={Value}")]
struct Data
{
	public Data(int index, int value)
	{
		Index = index;
		Value = value;
	}
	public int Index { get; }
	public int Value { get; }
}下面是完整解答。单调队列没有完全封装起来。队列会从两头增减元素,使用数组实现不好,因为从队头删除会导致元素移动,有O(n)复杂度。如果用两个指针,实现起来比较麻烦,所以我这里用链表实现。
public int[] MaxSlidingWindow(int[] nums, int k)
{
	if (nums.Length == 0)
		return new int[0];
	int[] result = new int[nums.Length - k + 1];
	//用链表实现单调队列,因为需要从两端移除元素。
	//设尾端为peek。
	//设该队列为单调递减队列,First为最大值,Last为最大值。
	var monotoneQueue = new LinkedList<Data>();
	//初始化单调队列,同时也是处理初始窗口
	for (int i = 0; i < k - 1; i++)
		Enqueue(monotoneQueue, new Data(i, nums[i]));
	for (int i = 0; i < result.Length; i++)
	{
		Enqueue(monotoneQueue, new Data(i + k - 1, nums[i + k - 1]));
		result[i] = monotoneQueue.First.Value.Value;
		Debug.Assert(monotoneQueue.First.Value.Index >= i);
		if (monotoneQueue.First.Value.Index == i)
			monotoneQueue.RemoveFirst();
	}
	return result;
}
/// <summary>
/// Add an item to monotonic decreasing queue. If the item is larger than <see cref="LinkedList{T}.Last"/>,
/// <see cref="LinkedList{T}.Last"/> will be removed.
/// </summary>
/// <param name="monotonicDecreasingQueue"></param>
/// <param name="item"></param>
void Enqueue(LinkedList<Data> monotonicDecreasingQueue, Data item)
{
	while (monotonicDecreasingQueue.Last?.Value.Value < item.Value)
		monotonicDecreasingQueue.RemoveLast();
	monotonicDecreasingQueue.AddLast(item);
}
[DebuggerDisplay("Index={Index}, Value={Value}")]
struct Data
{
	public Data(int index, int value)
	{
		Index = index;
		Value = value;
	}
	public int Index { get; }
	public int Value { get; }
}下面是封装后的单调队列
public class MonotoneQueue<T> : IEnumerable<T>
{
	private readonly IComparer<T> comparer;
	private readonly LinkedList<T> list = new LinkedList<T>();
	/// <summary>
	/// 
	/// </summary>
	/// <param name="comparer">用comparer的方法来决定单调递增还是单调递减</param>
	public MonotoneQueue(IComparer<T> comparer)
	{
		this.comparer = comparer;
	}
	public void Enqueue(T item)
	{
		while (list.Count > 0 && comparer.Compare(list.Last.Value, item) > 0)
			list.RemoveLast();
		list.AddLast(item);
	}
	public void Dequeue()
	{
		list.RemoveFirst();
	}
	public T Head => list.First.Value;
	public IEnumerator<T> GetEnumerator()
	{
		return list.GetEnumerator();
	}
	IEnumerator IEnumerable.GetEnumerator()
	{
		return list.GetEnumerator();
	}
}如何在MaxSlidingWindow()方法里使用MonotoneQueue类,作为课后习题留个大家。
- ENDLESSLETHE. 单调队列和单调栈详解. . 2017-12-14 [2019-09-10].
- . 239. Sliding Window Maximum. LeetCode. [2019-09-11].
[…] 上一篇文章上一篇 单调队列及应用 搜索: […]