栈和队列相关算法题目

栈和队列

栈的基本性质

栈是一种操作受限的线性表,只允许在一端插入和删除数据。
先进后出,后进先出,这就是典型的栈的特点。
栈既可以用数组来实现,又可以用链表来实现,用数组实现的栈,叫做顺序栈,用链表实现的栈叫做链式栈
栈操作(出栈、入栈)的时间复杂度是O(1)
如何实现一个支持动态扩容的栈,只需要底层依赖一个支持动态扩容的数组就可以了,当栈满了以后,就申请一个更大的数组,将原来的数组搬移到新的数组中。

栈的应用

  • 栈在函数调用中的应用
    操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种数据结构,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
  • 栈在表达式求值中的应用
    编译器通过两个栈来实现,其中一个保存操作数的栈,另一个保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈,当遇到运算符,就与运算符栈的站定元素比较。
    如果闭运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶元运算符,从操作数栈的栈顶取2个操作数,然后进行计算,在把计算完的结果压入操作数栈,继续比较。
  • 栈在括号匹配中的应用
    当扫描到左括号时,则将其压入栈中,当扫描到右括号时,从栈顶取出一个左括号,如果能够匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
    当所有括号都扫描完成之后,如果栈为空,则说明字符串为合法格式,如果说明有未匹配的左括号,为非法格式。

小问题

jvm里面的栈跟我们这里说的栈是不是一回事儿呢?
如果不是,那它为什么又叫做栈呢?
内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。

几个练习题

有效的括号

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
    思路:栈最基本的操作,遇到右括号出栈判断,遇到左括号进行入栈,最主要的是基础知识点不扎实
public boolean isValid(String s) {
        boolean flag = true;
        if (s == null || s.length() ==0){
            return flag;
        }
        Stack<String> stack  = new Stack<String>();
        for(int i =0;i<s.length();i++){
            String string = s.charAt(i)+"";
            String ss = "";
            if(string.equals("(")||string.equals("{")||string.equals("[")){
                stack.push(string);
            }else if(string.equals(")")){
                if(stack.isEmpty()||!stack.pop().equals("(")){
                    flag = false;
                    break;
                }                
            }else if(string.equals("}")){
                if(stack.isEmpty()||!stack.pop().equals("{")){
                    flag = false;
                    break;
                }                
            }else if(string.equals("]")){
                if(stack.isEmpty()||!stack.pop().equals("[")){
                    flag = false;
                    break;
                }                
            }else{
                flag = false;
                break;
            }
        }
        if(!stack.isEmpty()){
            flag = false;
        }
        return flag;

    }

优化后的代码

public boolean isValid(String s) {
        boolean flag = true;
        if (s == null || s.length() ==0){
            return flag;
        }
        if(s.length()%2 == 1){
            flag = false;
            return flag;
        }
        HashMap<Character,Character> map = new HashMap<Character,Character>(){{
            put(')','(');
            put('}','{');
            put(']','[');
        }};
        Stack<Character> stack = new Stack<Character>();
        for(int i = 0;i<s.length();i++){
            char ss = s.charAt(i);
            if(map.containsKey(ss)){
                if(stack.isEmpty() || !map.get(ss).equals(stack.peek())){
                    flag = false;
                    break;
                }
                stack.pop();
            }else{
                stack.push(ss);
            }
        }
        if(!stack.isEmpty()){
            flag = false;
        }
        return flag;

    }
棒球比赛

你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。

比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:

整数 x - 表示本回合新获得分数 x
“+” - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
“D” - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
“C” - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。
请你返回记录中所有得分的总和。

public int calPoints(String[] ops) {
        Deque<Integer> stack  = new LinkedList<Integer>();
        for(int i = 0;i < ops.length;i++){
            String s = ops[i];
            if(s.equals("+")){
                int a = stack.pop();
                int b = a + stack.peek();
                stack.push(a);
                stack.push(b);

            }else if(s.equals("D")){
                int a = stack.peek();
                stack.push(2*a);

            }else if(s.equals("C")){
                stack.pop();

            }else{
                stack.push(Integer.parseInt(s));
            }
        }
        int sum = 0;
        while(!stack.isEmpty()){
            sum += stack.pop();
        }
        return sum;

    }
比较含退格的字符串

给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

此题也可以用stringBuffer来解答

public boolean backspaceCompare(String s, String t) {
        Deque<String> stack_s = new LinkedList<String>();
        Deque<String> stack_t = new LinkedList<String>();
        String s1 = rev(s,stack_s);
        String s2 = rev(t,stack_t);
        return s1.equals(s2)? true:false;
    }
    public String rev (String string,Deque<String> stack){
        for(int i =0;i<string.length();i++){
            String s = string.charAt(i)+"";
            if(s.equals("#")){
                if(!stack.isEmpty()){
                    stack.pop();
                }
            }else{
                stack.push(s);
            }
        }
        String result = "";
        while(!stack.isEmpty()){
            result+=stack.pop();
        }
        return result;
    }
滑动窗口的平均值

给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算滑动窗口里所有数字的平均值。

实现 MovingAverage 类:

MovingAverage(int size) 用窗口大小 size 初始化对象。
double next(int val) 成员函数 next 每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后 size 个值的移动平均值,即滑动窗口里所有数字的平均值。

class MovingAverage {
    Queue<Integer> queue;
    int n = 0;
    double sum = 0.0;
    public MovingAverage(int size) {
        queue = new LinkedList<Integer>();
        n = size;
    }
    
    public double next(int val) {
        if(queue.size()<n){
            sum += val;
            queue.offer(val);
        }else{
            sum +=val;
            sum -=queue.poll();
            queue.offer(val);
        }
        return sum/queue.size();     
    }
}
队列的最大值

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1

示例1
输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

思路:需要定义两个队列,一个普通队列存储数据queue,一个为双向队列deque,存储最大值,需要在插入和删除的时候将其进行修改;如果添加时,添加的数据大于deque中的getlast,则将小于val的值都删掉,添加val;如果删除的时候,queue.peek==deque.peek则queue和deque的peek都需要删除

在这里插入代码片class MaxQueue {
    Queue<Integer> queue;//用于添加入队出队信息
    Deque<Integer> deque;//用于存放最大值信息
    public MaxQueue() {
        queue = new LinkedList<Integer>();
        deque = new LinkedList<Integer>();
    }
    
    public int max_value() {//获得最大的数
        int max = -1;
        if(queue.isEmpty()){
            return max;
        }
        return deque.getFirst();
    }
    
    public void push_back(int value) {
        queue.offer(value);
        while(!deque.isEmpty() && deque.getLast()<value){
            deque.removeLast();
        }
        deque.offer(value);
    }
    
    public int pop_front() {
        if(queue.isEmpty()){
            return -1;
        }
        int result = queue.peek();
        if(deque.peek()==result){
            deque.poll();
        }
        return queue.poll();
    }
}

单调栈1 2

单调栈是一种特殊的栈,栈本身就是一种受限的数据结构,单调栈在此基础上又受限一次,单调栈要求栈中的元素是单调递减或单调递增的(按照出栈顺序是递增还是递减分为单调递增栈和单调递减栈)
单调栈适合的应用场景就是求下一个大于*或者下一个小于的题目

单调栈模板
vector<int> nextGreaterElement(vector<int>& nums) {
    vector<int> res(nums.size()); // 存放答案的数组
    stack<int> s;
    // 倒着往栈里放
    for (int i = nums.size() - 1; i >= 0; i--) {
        // 判定个子高矮
        while (!s.empty() && s.top() <= nums[i]) {
            // 矮个起开,反正也被挡着了。。。
            s.pop();
        }
        // nums[i] 身后的 next great number
        res[i] = s.empty() ? -1 : s.top();
        // 
        s.push(nums[i]);
    }
    return res;
}

//java 中顶端元素用的是peek()而不是此处的top
下一个更大元素

给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
思路一:暴力搜索两层遍历

 public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        //第一种方法:暴力搜索法
        int[] result = new int[nums1.length];
        for(int i =0;i<nums1.length;i++){
            int j = 0;
            while(nums1[i]!=nums2[j]){
                j++;
            }
            int max_num = -1;
            for(;j<nums2.length;j++){
                if(nums2[j]>nums1[i]){
                    max_num = nums2[j];
                    break;
                }
            }
            result[i] = max_num;
        }
        return result;
        //第二种方法:利用单调栈,stack、nums[],result[],同时设置nums和result两者map对应关系,然后在遍历nums1,找到对应的值即可
    }

思路二:利用单调栈,stack、nums[],result[],同时设置nums和result两者map对应关系,然后在遍历nums1,找到对应的值即可

  • 先对 nums2 中的每一个元素,求出它的右边第一个更大的元素;
  • 将上一步的对应关系放入哈希表(HashMap)中;
  • 再遍历数组 nums1,根据哈希表找出答案。
 public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        //第二种方法:利用单调栈,stack、nums[],result[],同时设置nums和result两者map对应关系,然后在遍历nums1,找到对应的值即可
        int [] finalresult = new int[nums1.length];
        int [] result = monoStack(nums2);
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        for(int i =0; i<nums2.length;i++){
            map.put(nums2[i],result[i]);
        }
        for(int i = 0;i<nums1.length;i++){
            finalresult[i]=map.get(nums1[i]);
        }
        return finalresult;
    }
    public int[] monoStack(int[] nums){
        int[] result = new int[nums.length];
        Stack<Integer> stack = new Stack<Integer>();
        for(int i = nums.length-1; i >= 0; i--){
            while(!stack.isEmpty() && nums[i]>=stack.peek()){
                stack.pop();
            }
            result[i] = stack.isEmpty()? -1:stack.peek();
            stack.push(nums[i]);
        }
        return result;
    }
每日温度

请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

思路:还是单调栈的用法,只不过stack和result数组中存的是下标,而不是具体的值

 public int[] dailyTemperatures(int[] temperatures) {
        //单调栈问题,特殊点同时需要保存最高温所在的时间,即pos【用两个栈,一个存temp,一个存pos同时出栈入栈??】
        //变通
        int[] result = new int[temperatures.length];
        Stack<Integer> stack = new Stack<Integer>();
        for(int i = temperatures.length-1;i>=0;i--){
            while(!stack.isEmpty()&& temperatures[i]>=temperatures[stack.peek()]){
                stack.pop();
            }
            result[i] = stack.isEmpty()? 0 : stack.peek()-i;
            stack.push(i);
        }
        return result;
    }
下一个更大的元素II

给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。

示例 1:
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
思路:主要区别是这是一个循环数组,所以我们可以凑出2倍的数组,就可以解决这个问题了。
比较经典的就是可以通过%运算符求模取余数,来获得环形特效

int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
while (true) {
    print(arr[index % n]);
    index++;
}

本题题解为:

public int[] nextGreaterElements(int[] nums) {
        int n = nums.length;
        int[]res = new int[n];
        Stack<Integer> stack = new Stack<Integer>();
        for(int i = 2*n-1;i>=0;i--){
            while(!stack.isEmpty()&&nums[i%n]>=stack.peek()){
                stack.pop();
            }
            res[i%n] = stack.isEmpty()? -1 : stack.peek();
            stack.push(nums[i%n]);
        }
        return res;
    }

队列

队列和栈一样也是一种操作受限的线性表数据结构,队列可以由数组实现也可以由链表实现,数组实现的叫顺序队列,链表实现的叫链式队列。
队列需要两个指针:一个是head指针指向头,一个是tail指针,指向队尾
出队dequeue;入队enqueue;都需要判断一下是否满了或者是否为空;
队列满的条件是tail == n,n是数组的大小;
队列空的条件是head == tail;

循环队列

队空的条件:head == tail
队满的条件:(tail+1)%n ==head

阻塞队列和并发队列

阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。典型的例子就是生产者消费者模型。
线程安全的队列我们叫作并发队列。最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。

几个小的例子

队列常用的函数offer、poll、peek、add、remove等
队列里常用的有两套表示方式:

  1. 第一种
String[] items;//记录队里中的内容
int n =0;//数组大小,即申请的队列的大小
int head = 0;//队头下标
int tail = 0;//队尾下标
  1. 第二种
String[] items;//记录队里中的内容
int n =0;//数组大小,即申请的队列的大小
int head = 0;//队头下标
int size = 0;//队列中数据的个数

tail可以通过head和size 的计算来获得,所以两者都是可以的,只不过在循环队列中,如果使用tail,tail始终指向下一个待插入的位置,在循环队列中需要多申请一个空间,例如需要k个,则需要申请k+1才可以;笔者更喜欢用第一种方式来实现,只要逻辑正确任何一种都可以
循环队列中,判断为空的条件为head == tail,判断满的条件是**(tail+1)%n == head** 实际上当队列满时,tail指向的位置实际上是没有存储数据的,所以循环队列会浪费一个数组的存储空间。

用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
思路:对于push函数,需要特别注意,考虑到栈是先进后出,而队列是先进先出,所以可以用两个队列来相互交换实现

class MyStack {

    /** Initialize your data structure here. */
    Queue<Integer> queue1;//主
    Queue<Integer> queue2;//辅助
    public MyStack() {
        queue1 = new LinkedList<Integer>();
        queue2 = new LinkedList<Integer>();
    }
    
    /** Push element x onto stack. */
    public void push(int x) {
        //先放入到queue2中,然后将queue1的也放入到queue2中,然后在都导入到queue1中
        queue2.offer(x);
        while(!queue1.isEmpty()){
            queue2.offer(queue1.poll());
        }
        Queue<Integer> temp  = queue1;
        queue1 = queue2;
        queue2 = temp;
    }
    
    /** Removes the element on top of the stack and returns that element. */
    public int pop() {
        return queue1.poll();
    }
    
    /** Get the top element. */
    public int top() {
        return queue1.peek();
    }
    
    /** Returns whether the stack is empty. */
    public boolean empty() {
        return queue1.isEmpty();
    }
}

设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
思路:用第一种来实现,注意申请空间的时候需要多申请一个

class MyCircularQueue {
    int[]iterms;
    int count =0;//记录容量
    int head = 0,tail =0;//头和尾
     
    public MyCircularQueue(int k) {
        iterms = new int[k+1];
        count=k+1;
    }
    
    public boolean enQueue(int value) {
        if((tail+1)%count == head){
            return false;
        }
        iterms[tail % count] = value;
        tail = (tail+1) % count;
        return true;

    }
    
    public boolean deQueue() {
        if(head == tail){
            return false;
        }
        head = (head+1) % count;
        return true;
    }
    
    public int Front() {
        if(head == tail){
            return -1;
        }
        return iterms[head];
    }
    
    public int Rear() {
        if(head == tail){
            return -1;
        }
        return iterms[(tail+count-1)%count];

    }
    
    public boolean isEmpty() {
        return head == tail;
    }
    
    public boolean isFull() {
        return (tail+1)%count == head;
    }
}

第二种方法实现

class MyCircularQueue {

  private int[] queue;
  private int headIndex;
  private int count;
  private int capacity;

  /** Initialize your data structure here. Set the size of the queue to be k. */
  public MyCircularQueue(int k) {
    this.capacity = k;
    this.queue = new int[k];
    this.headIndex = 0;
    this.count = 0;
  }

  /** Insert an element into the circular queue. Return true if the operation is successful. */
  public boolean enQueue(int value) {
    if (this.count == this.capacity)
      return false;
    this.queue[(this.headIndex + this.count) % this.capacity] = value;
    this.count += 1;
    return true;
  }

  /** Delete an element from the circular queue. Return true if the operation is successful. */
  public boolean deQueue() {
    if (this.count == 0)
      return false;
    this.headIndex = (this.headIndex + 1) % this.capacity;
    this.count -= 1;
    return true;
  }

  /** Get the front item from the queue. */
  public int Front() {
    if (this.count == 0)
      return -1;
    return this.queue[this.headIndex];
  }

  /** Get the last item from the queue. */
  public int Rear() {
    if (this.count == 0)
      return -1;
    int tailIndex = (this.headIndex + this.count - 1) % this.capacity;
    return this.queue[tailIndex];
  }

  /** Checks whether the circular queue is empty or not. */
  public boolean isEmpty() {
    return (this.count == 0);
  }

  /** Checks whether the circular queue is full or not. */
  public boolean isFull() {
    return (this.count == this.capacity);
  }
}

其他参考文献[^3] 和[4]和[5]


  1. 单调栈说明 ↩︎

  2. 单调栈说明2 ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值