单调队列结构解决滑动窗口问题 :: labuladong的算法小抄 #1062
Replies: 14 comments 6 replies
-
這邊提醒一下,如果在push element 進入 Monotonic Queue 時,把條件設做 q.peekLast() <= ele 在這個test case - [7,5,7,1,6,0] 3 會出錯。 |
Beta Was this translation helpful? Give feedback.
-
插入节点可以使用新结构体 Node{val int ,index int},来排除val相同,但是应该删除的index不匹配问题 |
Beta Was this translation helpful? Give feedback.
-
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length, left;
if (n == 0) {
return nums;
}
int[] res = new int[n - k + 1];
Deque<Integer> dq = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
left = i - k + 1;
while (!dq.isEmpty() && nums[i] > dq.peekLast()) {
dq.pollLast();
}
dq.offerLast(nums[i]);
if (left >= 0) {
res[left] = dq.peekFirst();
if (!dq.isEmpty() && dq.peekFirst() == nums[left]) {
dq.pollFirst();
}
}
}
return res;
} |
Beta Was this translation helpful? Give feedback.
-
CPP version class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> res;
for(int i=0; i<k; i++)
que.push(nums[i]);
res.push_back(que.front());
for(int i=k; i<nums.size(); i++){
que.pop(nums[i-k]);
que.push(nums[i]);
res.push_back(que.front());
}
return res;
}
private:
class MyQueue{
public:
deque<int> que;
void pop(int val){
if(!que.empty() && que.front()==val)
que.pop_front();
}
void push(int val){
while(!que.empty() && val>que.back())
que.pop_back();
que.push_back(val);
}
int front(){
return que.front();
}
};
}; |
Beta Was this translation helpful? Give feedback.
-
javascript version: var maxSlidingWindow = function(nums, k) {
let window = new monotonicQueue();
let res = [];
for (let i=0; i<nums.length; i++) {
// 先把窗口的前k-1个元素填满
if (i < k - 1) {
window.push(nums[i]);
} else {
// 窗口开始向前滑动
// 移入新元素
window.push(nums[i]);
// 将当前窗口中的最大元素计入结果
res.push(window.max());
// 移出最后的元素
window.pop(nums[i - k + 1]);
}
}
return res;
};
var monotonicQueue = function () {
let queue = [];
// 在队尾添加元素n
this.push = function (n) {
while (queue.length && n > queue[queue.length - 1]) {
queue.pop();
}
queue.push(n);
};
// 返回当前队列中的最大值
this.max = function () {
// 队头元素是最最大值
return queue[0];
};
// 队头元素如果是n,删除它
this.pop = function (n) {
if (n == queue[0]) {
queue.shift();
}
};
}; |
Beta Was this translation helpful? Give feedback.
-
1楼说的有道理, |
Beta Was this translation helpful? Give feedback.
-
请教各位一个问题哎,我使用python手动定义了双链表,虽然可以通过所有用例,但为什么最后运行时间和使用内存都很多呢?按说应该很快的呀。代码如下 class MonotonicQueue:
def __init__(self):
self.mq = DLList()
def push_in(self, n):
while self.mq.get_size() and self.mq.peek_last() < n:
self.mq.pop_last()
self.mq.insert(n)
def peek(self):
return self.mq.peek_first()
def pop_out(self, n):
if self.mq.peek_first() == n:
self.mq.pop_first()
# Doubly Linked List Node class
class DLLNode:
def __init__(self, val):
self.val = val
self.prev = None
self.next = None
class DLList:
def __init__(self):
self.head = DLLNode(-1)
self.tail = DLLNode(-1)
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def insert(self, n):
# insert to the last position
new_node = DLLNode(n)
self.tail.prev.next = new_node
new_node.prev = self.tail.prev
new_node.next = self.tail
self.tail.prev = new_node
self.size += 1
def pop_last(self):
self.pop_node(self.tail.prev)
def pop_first(self):
self.pop_node(self.head.next)
def pop_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
node.next = None
node.prev = None
self.size -= 1
def peek_last(self):
return self.tail.prev.val
def peek_first(self):
return self.head.next.val
def get_size(self):
return self.size
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
mq = MonotonicQueue()
ans = []
for i in range(len(nums)):
if i < k-1:
mq.push_in(nums[i])
else:
mq.push_in(nums[i])
ans.append(mq.peek())
mq.pop_out(nums[i+1-k])
return ans |
Beta Was this translation helpful? Give feedback.
-
单调队列的思路很棒,但是不熟悉的话有点不太好想,关于这道题,题意很清晰,就是如何动态地维护集合并高效地返回最大值。 动态集合底层常见的就是哈希表和 rb tree。维护最大值最常见的就是 heap。 我分别从 rb tree 和 heap 的角度给出几个比较直接但是讨论不多的解决方案(我大概看了一下相关题解,大多数都是讲单调队列,以及堆的延迟删除(我的实现方案和大多数题解略有不同)): RB-TREE 方案:我们可以动态地维护一个容量为 K 的平衡树,那么最大值就是平衡树的最右节点,同时当窗口向右滑动时,删除左边界元素,并添加右边界元素即可。 C++ STL 已经实现了 RB-TREE,我的代码实现中采用的是 multiset(因为窗口中的元素是有可能重复的),代码如下(LeetCode 执行通过): 这种方案实现起来是这几种方案中最简单快速的。 class Solution
{
public:
vector<int> maxSlidingWindow(vector<int> &nums, int k)
{
typedef pair<multiset<int>::iterator, multiset<int>::iterator> srange;
multiset<int> s;
if (nums.size() <= k)
return {*max_element(nums.begin(), nums.end())};
for (int i = 0; i < k; i++)
s.insert(nums[i]);
vector<int> ans;
int j = k, size = nums.size();
while (j < size)
{
ans.push_back(*(--s.end()));
s.insert(nums[j]);
srange range = s.equal_range(nums[j - k]);
s.erase(range.first);
j++;
}
ans.push_back(*(--s.end()));
return ans;
}
}; HEAP && HASH-TABLE && 延迟删除我们可以维护一个大根堆来快速寻找最大值,但是在窗口滑动过程中需要删除左边界的元素,对于堆来讲,删除堆顶元素很容易,但是删除队中的任意元素不太容易(因为标准的heap接口并不支持随机删除heap中元素的功能,我在第三种解决方案中手动实现了这一API),所以可以考虑采用变相删除。 大多数题解都是通过比较索引大小关系来实现的,我的实现方案是定义一个node结构体,node包含数据本身以及一个名称为del的bool变量,del默认为false表示该元素没有被删除。如果标记为true表示已经被删除,那么在弹出元素的时候就会忽略del为true的元素。 为了能够快速获取到heap中元素的地址,还需要一个hash-table存储数据到node地址的映射关系,采用unordered_multimap实现,原因还是因为窗口中数据可能会重复。 代码如下(LeetCode 执行通过): 这种方案略微复杂。 struct node
{
int val;
bool del;
node(int __val) : val(__val), del(false) {}
};
class Solution
{
public:
vector<int> maxSlidingWindow(vector<int> &nums, int k)
{
if (nums.size() <= k)
return {*max_element(nums.begin(), nums.end())};
typedef pair<unordered_multimap<int, node *>::iterator, unordered_multimap<int, node *>::iterator> prange;
auto comp = [](node *a, node *b)
{
return a->val < b->val;
};
priority_queue<node *, vector<node *>, decltype(comp)> q(comp);
unordered_multimap<int, node *> val2addr;
vector<int> ans;
for (int i = 0; i < k; i++)
{
node *addr = new node(nums[i]);
q.push(addr);
val2addr.insert({nums[i], addr});
}
int size = nums.size(), j = k;
while (j < size)
{
while (q.top()->del == true)
q.pop();
ans.push_back(q.top()->val);
node *addr = new node(nums[j]);
q.push(addr);
val2addr.insert({nums[j], addr});
prange range = val2addr.equal_range(nums[j - k]);
range.first->second->del = true;
val2addr.erase(range.first);
j++;
}
while (q.top()->del == true)
q.pop();
ans.push_back(q.top()->val);
return ans;
}
}; HEAP && HASH-TABLE && 立即删除第一种以及第二种解决方案已经足够好了,这种解决方案只是作为一种补充,要想实现立即删除heap中任意元素的功能,我们需要做两件事情。 第一件事情就是存储数据到数据在heap中索引的映射,这部分采用unordered_multimap实现。 第二件事情是实现一个随机删除的接口,我们最常见的是删除堆顶元素,很少见随机删除堆内部元素,但是这个实现方案很简单。 堆是一棵完全二叉树,假设从1开始编号,一直到n,那么对于编号为i的元素,我们只需要将其与编号为n的元素互换并且删除堆尾部的元素,然后重新对堆进行调整即可。 交换元素后,编号为i的元素可能破坏堆的性质,调整方案如下:连续调用上浮和下沉这两个接口即可。编号为i的元素与其父亲节点(如果有的话)以及子女节点(如果有的话)的关系有且只有三种关系: 1、堆的性质继续保持,无需做任何调整。 绝对不会出现别的情况,所以连续调用上浮与下沉就可以覆盖所有情况,实际上要么这两个操作一个都不会执行,要么只执行一个,绝不会既上浮又下沉。(ps:随机删除heap中的元素是《算法导论》中的一道练习题) 代码如下(LeetCode执行通过): 这种方案最复杂,因为需要自己实现一个堆,只是代码量略大而已,并不难。 struct heap
{
typedef pair<unordered_multimap<int, int>::iterator, unordered_multimap<int, int>::iterator> prange;
vector<int> __heap;
unordered_multimap<int, int> elm2ind;
void __swap(int __x, int __y)
{
prange x_range = elm2ind.equal_range(__heap[__x]);
prange y_range = elm2ind.equal_range(__heap[__y]);
for (auto it = x_range.first; it != x_range.second; it++)
if (it->second == __x)
{
it->second = __y;
break;
}
for (auto it = y_range.first; it != y_range.second; it++)
if (it->second == __y)
{
it->second = __x;
break;
}
swap(__heap[__x], __heap[__y]);
}
void up(int pos)
{
int parent = (pos - 1) >> 1;
while (parent >= 0 && __heap[parent] < __heap[pos])
{
__swap(parent, pos);
pos = parent;
parent = (pos - 1) >> 1;
}
}
void down(int pos)
{
int left = (pos << 1) + 1, right = (pos << 1) + 2;
int size = __heap.size();
while (left < size)
{
int y = pos;
if (__heap[left] > __heap[y])
y = left;
if (right < size && __heap[right] > __heap[y])
y = right;
if (y != pos)
{
__swap(pos, y);
pos = y;
left = (pos << 1) + 1, right = (pos << 1) + 2;
}
else
break;
}
}
void random_delete(int data)
{
prange range = elm2ind.equal_range(data);
int pos = range.first->second;
int size = __heap.size();
__swap(pos, size - 1);
__heap.pop_back();
elm2ind.erase(range.first);
//关键调整操作
up(pos);
down(pos);
}
int &top()
{
return __heap[0];
}
void push(int data)
{
__heap.push_back(data);
elm2ind.insert({data, __heap.size() - 1});
up(__heap.size() - 1);
}
};
class Solution
{
public:
vector<int> maxSlidingWindow(vector<int> &nums, int k)
{
heap h;
if (nums.size() <= k)
return {*max_element(nums.begin(), nums.end())};
for (int i = 0; i < k; i++)
h.push(nums[i]);
vector<int> ans;
int j = k;
while (j < nums.size())
{
ans.push_back(h.top());
h.random_delete(nums[j - k]);
h.push(nums[j]);
j++;
}
ans.push_back(h.top());
return ans;
}
}; 时间复杂度三种方案中,第一种方案执行568ms,第二种执行692ms,第三种1132ms。执行时间虽然有差别,但只是常数差别而已,不存在阶的差异,基本操作要么是常量要么就是对数级别。不严格的分析,理论上来讲,第一种时间复杂度是 O(N LOG(K)),第二种时间复杂度是O(N LOG(N))(因为最坏情况下堆中的元素数量是N所以真数大一些,但是没什么大的影响,因为LOG在实际表现中近乎常量阶),第三种时间复杂度是O(N LOG(K))。 |
Beta Was this translation helpful? Give feedback.
-
打卡,点赞 |
Beta Was this translation helpful? Give feedback.
-
python如何使用list作为linkedlist的实现会超时 |
Beta Was this translation helpful? Give feedback.
-
py3已ac,请各位指正 class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
res = []
n = len(nums)
stack = collections.deque()
w = collections.deque()
for i in range(k):
w.append(nums[i])
if not stack or stack[-1] >= nums[i]:
stack.append(nums[i])
continue
while stack and stack[-1] < nums[i]:
stack.pop()
stack.append(nums[i])
cur_max = stack[0]
res.append(cur_max)
idx = k-1
while idx < n-1:
if w.popleft() == stack[0]:
stack.popleft()
idx += 1
w.append(nums[idx])
while stack and stack[-1] < nums[idx]:
stack.pop()
stack.append(nums[idx])
res.append(stack[0])
return res |
Beta Was this translation helpful? Give feedback.
-
扩展延伸:额外用一个LinkedList保存当前单调队列中的实际元素,这样poll()方法就不用传递元素了,单调队列的size()方法则调用这个队列的size()方法;获取最小值的min()方法与max()方法类似,申请一个LindkedList实现从小到大存储元素 |
Beta Was this translation helpful? Give feedback.
-
接 @weixueman 的思路,实现了一下拓展,放到sliding window题里通过了。思路就是多维护一个存全部元素的queue以及minq /* 单调队列的通用实现,可以高效维护最大值和最小值 */
class MonotonicQueue<E extends Comparable<E>> {
LinkedList<E> queue;
LinkedList<E> maxq;
LinkedList<E> minq;
public MonotonicQueue() {
queue = new LinkedList<>();
maxq = new LinkedList<>();
minq = new LinkedList<>();
}
// 标准队列 API,向队尾加入元素
public void push(E elem) {
queue.addLast(elem);
while (!maxq.isEmpty() && maxq.getLast().compareTo(elem) < 0) {
maxq.pollLast();
}
maxq.addLast(elem);
while (!minq.isEmpty() && minq.getLast().compareTo(elem) > 0) {
minq.pollLast();
}
minq.addLast(elem);
}
// 标准队列 API,从队头弹出元素,符合先进先出的顺序
public E pop() {
if (size() == 0) return null;
E first = queue.pollFirst();
if (maxq.getFirst().equals(first)) {
maxq.pollFirst();
}
if (minq.getFirst().equals(first)) {
minq.pollFirst();
}
return first;
}
// 标准队列 API,返回队列中的元素个数
public int size() {
return queue.size();
}
// 单调队列特有 API,O(1) 时间计算队列中元素的最大值
public E max() {
return maxq.getFirst();
}
// 单调队列特有 API,O(1) 时间计算队列中元素的最小值
public E min() {
return minq.getFirst();
}
} |
Beta Was this translation helpful? Give feedback.
-
请教: |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
文章链接点这里:单调队列结构解决滑动窗口问题
评论礼仪 见这里,违者直接拉黑。
Beta Was this translation helpful? Give feedback.
All reactions