O(n)时间找到栈的全局最小和队列的局部最小

栈和队列一直都是一对好基友,它们的出点点其实都是从它们最基本的特性开始:先进后出还是先进先出。

在CareerCup上有一道题目(3.1):问的是对于栈的元素,能够用O(1)的时间复杂度返回其栈中的最大元素。
思路有两种:
(1)是在栈中的每个元素中保存一个本身的值外,在保存一个它以及在栈中它以下的全部元素的最大值。
维护这个值很简单,每次栈push元素T的时候,先比较T和栈顶元素中保存的最大值,如果大于,则T中的最大元素为自己的值,否则为之前元素的最大值。
取最大值则只要把栈顶元素的最大值取出即可。

#include<cstdio>
#include<stack>
#include<iostream>
#include<vector>
using namespace std;
struct Element{//一个表示当前元素,一个表示当前的最大值
 int value;
 int max;
};
stack<Element*>S;
void push(int value){
 Element *tmp = new Element();
 tmp->value = value;
 if (S.empty() || S.top()->max < tmp->value)//如果S为空或者S当前的最大值小于新元素的值
  tmp->max = value;	 //则令其最大值为新元素的值
 else
  tmp->max = S.top()->max;//否则最大值还是原来栈的最大值
 S.push(tmp);
}
void pop(){
 S.pop();
}
int findMax(){
 return S.top()->max;
}
int main(){
 vector<int>input = {4,2,5,3,6,1};
 cout << "原数组排列如下:" << endl;
 for (vector<int>::iterator itr = input.begin(); itr != input.end(); ++itr){
  if (itr == input.begin())
   cout << *itr;
  else
   cout << " " << *itr;
  push(*itr);
 }
 cout << endl;
 cout << "从栈顶到栈底的最大值如下:" << endl;
 while (!S.empty()){
  Element *top = S.top();
  S.pop();
  static bool first = true;
  if (first){
   cout << top->max;
   first = false;
  }
  else
   cout << " " << top->max;
 }
 cout << endl;
 return 0;
}

运行结果如下:


(2)第二种方法
参考了:面试题的最大最小:http://blog.csdn.net/littledouble/article/details/46375107
类似于第一种,不过不是把最大值添加到原有栈的元素中,而是另外创建一个栈专门存放最大值,当原栈每push一个元素的时候,最大栈就同步存储原栈中的最大值;当原栈pop一个元素时,最大栈也同步pop一个值。
同理,也可以维护一个最小栈。这样可以在O(1)时间内同时知道当前栈的最大和最小值。如下图


代码如下:

#include<cstdio>
#include<stack>
#include<iostream>
#include<vector>
using namespace std;
stack<int>S;//原栈
stack<int>S_Max;//存放最大值的栈
stack<int>S_Min;//存放最小值的栈
void push(int value){//三个栈同步push
 S.push(value);
 if (S_Max.empty() || S_Max.top() < value)//S_Max为空或者小于当前值则更新最大值
  S_Max.push(value);
 else
  S_Max.push(S_Max.top());//否则还是保持原来的最大值
 if (S_Min.empty() || S_Min.top() > value)//S_Min为空或者大于当前值则更新最小值
  S_Min.push(value);
 else
  S_Min.push(S_Min.top());//否则还是保持原来的最小值
}
void pop(){//三个栈同步pop
 S.pop();
 S_Max.pop();
 S_Min.pop();
}
int main(){
 vector<int>input = {4,2,5,3,6,1};
 cout << "原数组排列如下:" << endl;
 for (vector<int>::iterator itr = input.begin(); itr != input.end(); ++itr){
  if (itr == input.begin())
   cout << *itr;
  else
   cout << " " << *itr;
  push(*itr);
 }
 cout << endl;
 cout << "从栈顶到栈底的最大值如下:" << endl;
 while (!S_Max.empty()){
  static bool first = true;
  if (first){
   cout << S_Max.top();
   first = false;
  }
  else
   cout << " " << S_Max.top();
  //pop();  因为等一下还要求最小值,所以这里采用S_Max.pop()而不是pop();
  S_Max.pop();
 }
 cout << endl;
 cout << "从栈顶到栈底的最小值如下:" << endl;
 while (!S_Min.empty()){
  static bool first = true;
  if (first){
   cout << S_Min.top();
   first = false;
  }
  else
   cout << " " << S_Min.top();
  S_Min.pop();
 }
 cout << endl;
 return 0;
}



后来在做LeetCode上有一题,是滑动窗口保持最大值,具体看下面的链接:
双端队列LeetCode上的:https://leetcode.com/problems/sliding-window-maximum/
它的原理和之前的线性时间取得栈的最大/小值有着异曲同工之妙。
它的题目是给你一个数列,比如4,2,5,4,6,1,以及一个窗口大小,窗口从数列的头部向尾部滑动,每次滑动一格,
要你输出窗口中最大值组成的数列,例子中的答案应该是5,5,6,6

怎么从上面的思路引导到这一题呢,也是要用其他的结构去存储当前窗口的最大值,而且因为其是队列的属性,
所以我们引入的结构也要是队列形式的,比如queue或deque之类的。
*而且我们注意到在窗口中当后者有更大的数出现的时候,前者的数都可以不用存储,即使它们还在窗口中
比如,4,2,5,我们先存储4为最大,当2来的时候,我们也应该把2保留(也存储),因为4比2先,当4出队列(出窗口)的时候2可能就是最大值。而当5再进来的时候,4和2都可以不用保留(存储)了,因为有5在它们肯定成为不了最大,而且它们是更早离开的。
所以我们采用的是deque双端列队,并且这个队列大小始终会<=窗口大小
当有元素来到的时候,如果deque为空,则push_back()。
如果deque非空,则把新元素和队尾元素进行比较,如果队尾元素小,则一直pop_back()到比新元素大或者空队列的时候,再把新元素push_back进去。
当我们需要当前窗口的最大值时,只要对deque进行front()取出对头值就好了。
当窗口滑动时,如果出窗口的值等于deque的front(),则deque进行pop_front(),否则不动。
所以取当前窗口只需要O(1),一次遍历只需要O(n)的复杂度。
注意到我们的deque肯定是递减排列的。
下面来看一个例子:
第一步:把数列中的数填满窗口,第一个数是4,所以我们存储最大值的deque:maxque进行push_back()操作


第二步:把数列中的数填满窗口,第二个数是2,小于deque的back:4,所以对maxque进行push_back()操作


第三步:把数列中的数填满窗口,第三个数是5,大于deque的back:2,所以对maxque进行pop_back()操作,
然后再进行判断,还是大于deque的back:4,所以再对maxque进行pop_back()操作,
maxque已经为空了,所以对maxque进行push_back()操作,5进入maxque。
这时,窗口已经满了,达到3了,所以对maxque取front就可以得到当前窗口的最大值:5


第四步:窗口向右滑动,
先把窗口的左边的数字去除,2离开窗口,然后把2与maxque的front进行判断,如果相等,则让maxque进行pop_front()操作,现在不相等(说明2在前面的某一时刻已经被pop_back()了,也说明2后面有更大的数,所以2提前被淘汰了)那么maxque什么也不做。
再把窗口右边的数字加入窗口,并把3和maxque的back进行比较,3小于5,那么就把maxque进行push_back把3加入maxque。
所以对maxque取front就可以得到当前窗口的最大值:5


第五步:窗口向右滑动,
先把窗口的左边的数字去除,5离开窗口,然后把5与maxque的front进行判断,相等,所以让maxque进行pop_front()操作
再把窗口右边的数字加入窗口,并把6和maxque的back进行比较,因为现在maxque为空,所以不用比较,直接加入,push_back
所以对maxque取front就可以得到当前窗口的最大值:6


第六步:窗口向右滑动,
先把窗口的左边的数字去除,3离开窗口,然后把3与maxque的front进行判断,不相等,所以maxque什么也不做
再把窗口右边的数字加入窗口,并把1和maxque的back进行比较,1小于maxque的back,所以把1加入,push_back()。
所以对maxque取front就可以得到当前窗口的最大值:6


LeetCode上的答案:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int>vec;
        int length = nums.size();
        if(length == 0 || k == 0)return vec;
        for(int i= 0;i < k - 1;i ++){
            push(nums[i]);
        }
        for(int i= k - 1;i < length; ++i){
            push(nums[i]);
            vec.push_back(findMax());
            pop(nums[i - k + 1]);
        }
        return vec;
       
    }
    deque<int>maxque;
    void push(int val){
        while(!maxque.empty() && (maxque.back() < val))
            maxque.pop_back();
        maxque.push_back(val);
    }
    void pop(int val){
        if(maxque.front() == val)
            maxque.pop_front();
    }
    int findMax(){
        return maxque.front();
    }
};

——by Apie陈小旭

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值