算法-栈和队列汇总

知识

栈和队列都是比较简单的数据结构,这里就不做过多的解释,只解释对应到Java中的类(建议自行查看源码–极其友好)。

Stack类

Stack extends Vector

synchronized E peek()  取栈顶元素-------时间复杂度O(1)
E push(E item)   进栈-------不考虑扩容,时间复杂度O(1)
synchronized E pop()  出栈-------时间复杂度O(1)
boolean empty() 判空--------时间复杂度O(1)
还有继承Vectorsize()clear()方法

⚡️对于单步操作的方法,方法是不用加同步的

Queue接口

队列一般使用LinkedList类,LinkedList implements Deque, Deque extends Queue

查看
E peek()	仅获取元素----O(1)
E element()	仅获取元素----O(1)     -----两者区别在于对空指针的处理不同-返回null或抛出异常
移除队头,Last结尾的用于移除队尾,如pollLast
E poll()	移除----O(1) 
E remove()	移除----O(1)		     -----同上
添加到队尾,First结尾的添加到队头
boolean offer(E e)	入队,调用了add(E e)---O(1)

💡栈和队列其实操作不多,基本方法就查看、添加、删除。

练习

BM42 用两个栈实现队列

不难,别被哪个O(1)误导就行,O(1)给感觉就是不用循环。。。。。其实这里的O(1)是指能让操作尽量多的达到O(1)的时间复杂度,但中间可能存在超过O(1)的操作。

思路:把一个栈当成删除pop;一个栈当成添加push;删除的栈空的时候,就把添加栈的元素全部放到删除的栈

import java.util.*;
import java.util.Stack;

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        stack1.push(node);
    }
    
    public int pop() {
        if(stack2.isEmpty()) {
            //栈1pop 栈2push
            while(!stack1.isEmpty()) {
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();  
    }
}

同类题目:232. 用栈实现队列

class MyQueue {

    Stack<Integer> s1;
    Stack<Integer> s2;
    public MyQueue() {
        s1 = new Stack<Integer>();
        s2 = new Stack<Integer>();
    }
    
    public void push(int x) {
        s2.push(x);
    }
    

    public int pop() {
         //若s1为空,将s2的pop进s1
        sameOperate();
       return s1.pop();
    }
    

    public int peek() {
        //若s1为空,将s2的pop进s1
       sameOperate();
       return s1.peek();
    }

    //相同代码提取
    private void sameOperate(){
        if(s1.empty()){
           while(!s2.empty()){
               s1.push(s2.pop());
           }
       }
    }
    
    public boolean empty() {
        return s1.size()==0 && s2.size()==0;//此时需要两个做判断
    }
}
225. 用队列实现栈

思路一:整两个队列,添加元素选择队列不为空的;删除/查看元素时,则将不为空的队列的元素依次移入另一个队列,并对最后一个元素进行特殊操作即可(删除则不入队,查看则入队)

💡这里一直保持q1为空,是为了减少if的判断,类似JVM垃圾回收复制算法,不交换版本

class MyStack {

    //让q1一直为空
    Queue<Integer> q1; 
    Queue<Integer> q2;

    public MyStack() {
        q1 = new LinkedList<>();
        q2 = new LinkedList<>();
    }
    
    public void push(int x) {
        q2.add(x);
    }
    
    public int pop() {
        int size = q2.size() - 1;
        while(size-- > 0) {
            q1.add(q2.poll());
        }
        int last = q2.poll();
        //保证q1一直为空        
        swap();
        return last;
    }
    //交换两队列
    private void swap() {
        Queue<Integer> tmp = q2;
        q2 = q1;
        q1 = tmp;
    }    
    
    public int top() {
        int size = q2.size() - 1;
        while(size-- > 0) {
            q1.add(q2.poll());
        }
        int last = q2.poll();
        //注意查看后需要入队
        q1.add(last);
        swap();
        return last;
    }
    
    public boolean empty() {
        return q1.isEmpty() && q2.isEmpty();
    }
}

思路二:单个队列实现,在添加元素后,将其前面的所有元素依次出队并再次加入该队列(出队后再入队),也可以在删除和查看时操作,一样的。

class MyStack {
    Queue<Integer> queue;
    public MyStack() {
        queue = new LinkedList<Integer>();
    }
    
    public void push(int x) {
        int l = queue.size();
        queue.offer(x);
        while(l-- > 0){
            queue.offer(queue.poll());
        }
    }
    
    public int pop() {
        return queue.poll();
    }
    
    public int top() {
       return queue.peek();
    }
    
    public boolean empty() {
        return queue.size()==0;
    }
}
*155. 最小栈

思路:双栈,一个实现栈的基本操作;一个实现最小值,对于每个入栈元素,都判断当前其最小值是什么,如果当前位置的值比最小值小,则更新最小值,并将最小值入栈,例如2 4 1 3 那么其最小值栈为 2 2 1 1(每个元素的位置到栈底的最小值都是明确的)

class MinStack {

    Stack<Integer> sk;
    Stack<Integer> msk;
    int min;
    public MinStack() {
        sk =  new Stack<Integer>();
        msk = new Stack<Integer>();
        min = Integer.MAX_VALUE;
    }
    
    public void push(int x) {
        sk.push(x);
        if(x<min){
            min=x;
        }
        msk.push(min);//每一元素所处的位置都有其对应的最小值
    }
    
    public void pop() {
        sk.pop();
        msk.pop();//此时也需要pop,因为此位置不存在元素了,则也就不存在此位置的最小值了
        //更新最小值
        min = msk.empty() ? Integer.MAX_VALUE : msk.peek();
    }
    
    public int top() {
        return sk.peek();
    }
    
    public int getMin() {
        return min;
    }
}
20. 有效的括号

思路:遇到左括号([{就入栈,遇到右括号}])就出栈匹配对不对,需要注意单边的(())的情况,会导致结果不对和空栈异常,其中栈大小超过一半未匹配直接false,可下可不下,只是优化作用,例如((((((((((((((()

class Solution {
    public boolean isValid(String s) {
        int l = s.length();
        //奇数直接false
        if((l & 1) == 1) return false; 
        //栈的长度不可能超过一半
        l = l / 2;
        //栈
        Stack<Character> sk = new Stack<>();
        for(char a : s.toCharArray()) {             
            if(sk.size() > l) {
                //超过一半未匹配直接false
                return false;
            }
            if(a == '(' || a == '{' || a =='[') {
                sk.push(a);
            } else {
                if(sk.isEmpty()) {
                    //特殊情况"))"
                    return false; 
                }
                char tmp = sk.pop();                
                if(tmp == '(') {
                    if(a != ')') return false;
                } else if(tmp == '{') {
                    if(a != '}') return false;
                } else {
                    if(a != ']') return false;
                }
            }
        }
        //特殊情况"(("
        return sk.isEmpty();
    }
}
150. 逆波兰表达式求值

思路:仔细观察求指过程,不难发现,计算的时机就是遇到运算符就将前面的两个元素进行计算,然后将结果放回去,继续往后。这个操作过程其实就是出栈和入栈的过程。

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> sk = new Stack<>();
        for(String token : tokens) {            
            switch(token) {
                case "+" : {
                    sk.push(sk.pop() + sk.pop());
                    break;
                }
                case "-" : {
                    int a = sk.pop();               
                    int b = sk.pop();
                    //减法和除法需要注意运算顺序
                    sk.push(b - a); 
                    break;                    
                }
                case "*" : {
                    sk.push(sk.pop() * sk.pop());
                    break;                       
                }               
                case "/" : {
                    int a = sk.pop();
                    int b = sk.pop();
                    sk.push(b / a);
                    break;                     
                }   
                default : {
                    //数字直接入栈
                    sk.push(Integer.parseInt(token));
                }
                    
            }            
        }
        return sk.pop();
    }
}
739. 每日温度

思路:两两比较,小的则赋值为1,大于则入栈等到小的时候在拿出来比较。注意栈存放的是元素的下标,有下标才能计算相差几天,有下标才能定位修改哪个位置的元素,不然pop出来的是个什么东西都不知道?这里直接修改了temperatures数组,你也可以自己新建一个。

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        Stack<Integer> sk = new Stack<>();
        int l = temperatures.length - 1;        
        for(int i = 0; i < l; i++) {
            if(temperatures[i] < temperatures[i+1]) {
                //小于
                temperatures[i] = 1;
                 //拿出来比较
                while(!sk.isEmpty() && temperatures[sk.peek()] < temperatures[i+1]) {
                    int idx = sk.pop();
                    temperatures[idx] = i + 1 - idx;
                }
            } else {
                sk.push(i);
            }
        }
        //不存在升温的
        while(!sk.isEmpty()) {
            temperatures[sk.pop()] = 0;
        }
        //最后一位
        temperatures[l] = 0;
        return temperatures;
    }
} 

优化:力扣提交超过100%的哪个的答案是类似暴力法,但从后往前遍历,不知道为什么那么快。

496. 下一个更大元素 I

思路:类似每日温度,不过需要借助哈希表,也就是先求nums2每个元素的下一个更大数,然后再去求解nums1。题意容易让人误解,其实求的就是nums2每个元素的下一个更大数,只是从中抽取一些元素构成nums1,然后求nums1在nums2的最大数。栈的作用不变,两两比较,小的加入哈希表,大的暂存起来,直到下次小的时候才拿出来比较,类似每日温度,注意这里并不需要用下标,直接存放元素值更方便。

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int l2 = nums2.length - 1;
        //存放nums2各个元素和对应的下一个更大元素
        Map<Integer, Integer> map  = new HashMap<>(); 
        Stack<Integer> sk = new Stack<Integer>();
        for(int i = 0; i < l2; i++) {
            if(nums2[i] < nums2[i+1]) {                
                map.put(nums2[i], nums2[i+1]);
                //拿出来做比较
                while(!sk.isEmpty() && sk.peek() < nums2[i+1]) {
                    map.put(sk.pop(), nums2[i+1]);
                }
            } else {
                sk.push(nums2[i]);
            }
        }
        while(!sk.isEmpty()) {
            map.put(sk.pop(),-1);
        }
        map.put(nums2[l2], -1);
        //此时map已经获得了nums2每个元素的下一个更大数了
        int l1 = nums1.length;        
        for(int i = 0; i < l1; i++) {
            //修改到nums1中即可            
            nums1[i] = map.get(nums1[i]); 
        }
        return nums1;
    }
} 
503. 下一个更大元素 II

思路:上一题的扩展,求解的数组变为循环数组,可以遍历一次之后,再对栈中剩余的元素进行遍历(前面的题目是赋值-1,但这里还需要跟前面的比较一次,因为数组可循环)。注意这里的栈用于存放下标。除此之外不能修改原数组,因为第二次遍历需要用到前面的数据。

测试用例

[1,5,3,6,8]
[1,2,3,4,3]
[1,2,3,2,1]
[5,4,3,2,1]
[100,1,11,1,120,111,123,1,-1,-100]
class Solution {
    public int[] nextGreaterElements(int[] nums) {    
        int[] result = new int[nums.length];
        //初始化,也可以不要,但最后面栈剩余元素需要使用while将其填充为-1
        Arrays.fill(result,-1);
        int l = nums.length - 1;        
        Stack<Integer> sk = new Stack<>();
        //第一次比较
        for(int i=0;i<l;i++){
            if(nums[i] < nums[i+1]) {
                result[i] = nums[i+1];
                while(!sk.empty() && nums[sk.peek()]<nums[i+1]){
                    result[sk.pop()]=nums[i+1];
                }
            } else {
                sk.push(i);
            }               
        }     
        sk.push(l); //最后一个
        //第二次比较
        int j=0;
        while(!sk.empty() && j<=l){
            if(nums[sk.peek()] < nums[j]){
                result[sk.pop()]=nums[j];
            }else{
                j++;
            }
        }
        return result;
    }
}
剑指 Offer 31. 栈的压入、弹出序列
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值