代码随想录-6 刷题记录 栈与队列

一、理论基础

Java中的栈(Stack)是一种后进先出(LIFO,Last In First Out)的数据结构,这意味着最后放入栈的数据是最先被取出的。栈可以用于存储临时数据,如递归调用中的局部变量、函数调用链等。

Java 中的栈实现

Java 中的栈主要有两种实现方式:

  • 栈类 java.util.Stack

    • Stack 类是 Java 提供的一个标准栈实现,继承自 Vector 类。
    • 常用方法:
      • push(E item):将元素压入栈顶。
      • pop():移除并返回栈顶元素。
      • peek():返回栈顶元素但不移除它。
      • empty():判断栈是否为空。
      • search(Object o):返回对象在栈中的位置(从栈顶开始的1-based index)。
  • 使用 Deque 实现栈

    • Deque(双端队列)接口及其实现类(如 ArrayDequeLinkedList)也可以用于实现栈。
    • 这是推荐的方式,因为 Stack 类已经过时且设计不够现代,而 Deque 更加灵活和高效。
    • 常用方法:
      • push(E e):将元素压入栈顶。
      • pop():移除并返回栈顶元素。
      • peek():返回栈顶元素但不移除它。
import java.util.ArrayDeque;
import java.util.Deque;

public class DequeAsStackExample {
    public static void main(String[] args) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println("Stack: " + stack); // 输出: [3, 2, 1]

        int topElement = stack.pop(); // 移除栈顶元素3
        System.out.println("Popped element: " + topElement); // 输出: 3
        System.out.println("Stack after pop: " + stack); // 输出: [2, 1]

        int peekElement = stack.peek(); // 查看栈顶元素
        System.out.println("Top element: " + peekElement); // 输出: 2
        System.out.println("Stack after peek: " + stack); // 输出: [2, 1]
    }
}

Java 中的队列实现

Java 提供了多种方式来实现队列,最常用的接口是 java.util.Queue,该接口有多个实现类可以选择使用。

1. Queue 接口

Queue 接口是 Java 中队列的主要接口,它定义了队列的基本操作方法:

  • add(E e): 将指定元素插入队列,如果队列已满会抛出异常。
  • offer(E e): 将指定元素插入队列,成功返回 true,否则返回 false
  • remove(): 移除并返回队列头部的元素,如果队列为空会抛出异常。
  • poll(): 移除并返回队列头部的元素,如果队列为空返回 null
  • element(): 返回队列头部的元素,但不移除它,如果队列为空会抛出异常。
  • peek(): 返回队列头部的元素,但不移除它,如果队列为空返回 null
2. 常见的 Queue 实现类
1. LinkedList

LinkedListQueue 的常用实现类之一,底层是一个双向链表,适合于频繁的插入和删除操作。

2. ArrayDeque

ArrayDeque 是一个高效的双端队列实现类,适合用作栈或队列。它比 LinkedList 更快,且没有容量限制。

3. PriorityQueue

PriorityQueue 是一个基于堆的优先级队列,它的元素按照自然顺序或指定的比较器排序。最小元素(或根据比较器规则最优先的元素)在队列头部。

4. ConcurrentLinkedQueue

ConcurrentLinkedQueue 是一个无界线程安全队列,基于链接节点的无锁算法实现,适用于高并发场景。

二、题目

1.232.用栈实现队列

本题考查用两个栈实现队列操作。

使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。

在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。

最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。

class MyQueue {

    Deque<Integer> stackIn;
    Deque<Integer> stackOut;

    public MyQueue() {
        stackIn = new ArrayDeque<>();
        stackOut = new ArrayDeque<>();

    }
    
    public void push(int x) {
        stackIn.push(x);

    }
    
    public int pop() {
        int res = 0;
        if(stackOut.isEmpty()){
            while(!stackIn.isEmpty()){
                int tmp = stackIn.pop();
                stackOut.push(tmp);
            }
        }
        res = stackOut.pop();
        return res;
    }
    
    public int peek() {
        int res = pop();
        stackOut.push(res);
        return res;
    }
    
    public boolean empty() {
        return stackIn.isEmpty() && stackOut.isEmpty();
    }
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */

pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以看出peek()的实现,直接复用了pop()。

在工业级别代码开发中,最忌讳的就是 实现一个类似的函数,直接把代码粘过来改一改就完事了。

这样的项目代码会越来越乱,一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)

3.225.用队列实现栈

队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。

所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。

但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的!

class MyStack {
    Deque<Integer> que1;
    Deque<Integer> que2;

    public MyStack() {
        que1= new ArrayDeque<>();
        que2= new ArrayDeque<>();

    }
    
    public void push(int x) {
        que1.add(x);

    }
    
    public int pop() {
        int size = que1.size();
        size--;
        while(size-->0){
            que2.add(que1.poll());
        }
        int res = que1.poll();
        while(!que2.isEmpty()){
            que1.add(que2.poll());
        }
        return res;
    }
    
    public int top() {
        return que1.peekLast();
    }
    
    public boolean empty() {
        return que1.isEmpty();
    }
}

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack obj = new MyStack();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.top();
 * boolean param_4 = obj.empty();
 */

只用一个栈:

class MyStack {
    Deque<Integer> que1;


    public MyStack() {
        que1= new ArrayDeque<>();
    }
    
    public void push(int x) {
        que1.add(x);

    }
    
    public int pop() {
        int size = que1.size();
        size--;
        while(size-- > 0){
            que1.add(que1.poll());
        }
        int res = que1.poll();
    
        return res;
    }
    
    public int top() {
        return que1.peekLast();
    }
    
    public boolean empty() {
        return que1.isEmpty();
    }
}

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack obj = new MyStack();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.top();
 * boolean param_4 = obj.empty();
 */

4.20. 有效的括号

有一些技巧,在匹配左括号的时候,对应右括号入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了

class Solution {
    public boolean isValid(String s) {
        Deque<Character> deque = new LinkedList<>();
        char ch;
        for (int i = 0; i < s.length(); i++) {
            ch = s.charAt(i);
            //碰到左括号,就把相应的右括号入栈
            if (ch == '(') {
                deque.push(')');
            }else if (ch == '{') {
                deque.push('}');
            }else if (ch == '[') {
                deque.push(']');
            } else if (deque.isEmpty() || deque.peek() != ch) {
                return false;
            }else {//如果是右括号判断是否和栈顶元素匹配
                deque.pop();
            }
        }
        //最后判断栈中元素是否匹配
        return deque.isEmpty();
    }
class Solution {
    public boolean isValid(String s) {
        Deque<Character> stack = new ArrayDeque<>();
        int len = s.length();
        for(int i = 0; i<len ; i++ ){
            if(isLeft(s.charAt(i))){
                stack.push(s.charAt(i));
            }else if(stack.isEmpty()){
                return false;
            }else if(s.charAt(i)==')'){
                if(stack.pop()!='(') return false;
            }else if(s.charAt(i)=='}'){
                if(stack.pop()!='{') return false;
            }else{
                if(stack.pop()!='[') return false; 
            }
        } 
        if(!stack.isEmpty()) return false;
        return true;

    }

    private boolean isLeft(char c) {
        if(c == '[' || c=='{' || c=='(') return true;
        return false;
    }
}

5.1047. 删除字符串中的所有相邻重复项

匹配问题都是栈的强项

本题要删除相邻相同元素,相对于20. 有效的括号 (opens new window)来说其实也是匹配问题,20. 有效的括号 是匹配左右括号,本题是匹配相邻元素,最后都是做消除的操作。

本题使用栈的目的是记录遍历的前一个元素,以便判断是否是相邻的重复项。

class Solution {
    public String removeDuplicates(String S) {
        //ArrayDeque会比LinkedList在除了删除元素这一点外会快一点
        //参考:https://stackoverflow.com/questions/6163166/why-is-arraydeque-better-than-linkedlist
        ArrayDeque<Character> deque = new ArrayDeque<>();
        char ch;
        for (int i = 0; i < S.length(); i++) {
            ch = S.charAt(i);
            if (deque.isEmpty() || deque.peek() != ch) {
                deque.push(ch);
            } else {
                deque.pop();
            }
        }
        String str = "";
        //剩余的元素即为不重复的元素
        while (!deque.isEmpty()) {
            str = deque.pop() + str;
        }
        return str;
    }
}

6.150. 逆波兰表达式求值

将中缀表达式,转化为后缀表达式之后,计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的。

class Solution {
    public int evalRPN(String[] tokens) {
        Deque<Integer> stack = new ArrayDeque<>();
        for(String s : tokens){
            if(s.equals("+")){
                stack.push(stack.pop()+stack.pop());
            }else if (s.equals("-")){
                stack.push(-stack.pop()+stack.pop());
            }else if(s.equals("*")){
                stack.push(stack.pop()*stack.pop());
            }else if (s.equals("/")){
                int tmp1 = stack.pop();
                int tmp2 = stack.pop();
                stack.push(tmp2/tmp1);
            }else{
                stack.push(Integer.valueOf(s));
            }
        }
        return stack.pop();

    }
}

7.239. 滑动窗口最大值

这是使用单调队列的经典题目。

暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法。

个人认为单调队列和单调栈都有点类似于搜索里的剪枝思想,去掉对求解问题不影响结果的部分来降低时间复杂度。

本题中使用单调队列,只需要维护窗口内的最大值和排列在最大值之后的值,对于在最大值之前比其小的值,不影响窗口内的最大值并且最终比最大值先pop出去,不必再记录。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length - k + 1;
        int num = 0;
        int[] res = new int[len];
        MyQueue myQueue = new MyQueue();
        for(int i = 0; i< k; i++){
            myQueue.add(nums[i]);
        }
        res[num++]= myQueue.peek();
        for(int i = k ;i < nums.length ; i++){
            myQueue.poll(nums[i-k]);
            myQueue.add(nums[i]);
            res[num++]=myQueue.peek();
        }
        return res;
    }
}

class MyQueue{
    Deque<Integer> queue = new ArrayDeque<>();

    public void add(int x){
        while(!queue.isEmpty() && x > queue.getLast()){
            queue.removeLast();
        }
        queue.add(x);
    }

    public void poll(int x){
        if(!queue.isEmpty() && queue.peek()==x){
            queue.poll();
        }
    }

    public int peek(){
        return queue.peek();
    }
}

8.优先队列

347.前 K 个高频元素

什么是优先级队列呢?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

如果使用大顶堆,则需要维护所有元素到堆中。

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        int nlen = nums.length;
        Map<Integer,Integer> map = new HashMap<>();
        for(int i = 0 ; i<nlen ;i++){
            map.put(nums[i],map.getOrDefault(nums[i],0)+1);
        }

        PriorityQueue<int[]> queue = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
        for(Map.Entry<Integer,Integer> entry : map.entrySet()){
            if(queue.size()<k){
                queue.add(new int[]{entry.getKey(),entry.getValue()});
            }else{
                if(queue.peek()[1]<entry.getValue()){
                    queue.poll();
                    queue.add(new int[]{entry.getKey(),entry.getValue()});
                }
            }
        }
        int[] ans = new int[k];
        for(int i = k-1; i>=0 ; i--){
            ans[i] = queue.poll()[0];
        }
        return ans;

    }
}

滑动窗口最大值问题

主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。

那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列

而且不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。

设计单调队列的时候,pop,和push操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  2. push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。

不要以为本题中的单调队列实现就是固定的写法。

求前 K 个高频元素

什么是优先级队列呢?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

本题就要使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序!

所以排序的过程的时间复杂度是 O(log k) ,整个算法的时间复杂度是 O(n*log k) 。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值