leetcode--栈和队列专栏

2 篇文章 0 订阅
1 篇文章 0 订阅
本文详细介绍了如何使用栈和队列实现特定功能,如232题用栈实现队列,225题用队列实现栈,以及有效括号匹配、删除字符串相邻重复项、逆波兰表达式求值、滑动窗口最大值和找到前K个高频元素等算法问题的解决方案。文章强调了在实际编程中避免复制粘贴代码,提倡代码复用和抽象,同时提供了多种思路和优化技巧。
摘要由CSDN通过智能技术生成

前提知识

统一用下面的定义队列和栈

//队列
Queue<String> queue = new LinkedList<String>();
*****注意!用add、poll、peek
//栈
Deque<Integer> stack = new ArrayDeque<Integer>();
*****注意!这个方法中的push和add都可以用,但是一定要用push、pop、peek
但是这个也可以实现双端队列
queue.poll()从前面弹出
queue.pollLast()从后面弹出
queue.offer()从后面加入 = queue.offerLast()
queue.offerFirst()从前面加入
//双端队列,一般用
LinkedList<Integer> queue = new LinkedList<Integer>();

原因

题目速览

232. 用栈实现队列
225. 用队列实现栈
20. 有效的括号
1047. 删除字符串中的所有相邻重复项
150. 逆波兰表达式求值
239. 滑动窗口最大值
347. 前 K 个高频元素

题目详解

232. 用栈实现队列

整体思路:
在push数据的时候,只要数据放进输入栈就好。

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

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

class MyQueue {
       Deque<Integer> stack1;
       Deque<Integer> stack2;
        public MyQueue() {
            stack1 = new ArrayDeque<Integer>();
            stack2 = new ArrayDeque<Integer>();
        }

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

        public int pop() {
            if (stack2.isEmpty()){
                while (!stack1.isEmpty()){
                    stack2.push(stack1.pop());
                }
            }
            return stack2.pop();
        }

        public int peek() {
            if (stack2.isEmpty()){
                while (!stack1.isEmpty()){
                    stack2.push(stack1.pop());
                }
            }
            return stack2.peek();
        }

        public boolean empty() {
            return stack1.isEmpty() && stack2.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();
 */

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

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

所以代码修改成:

class MyQueue {
       Deque<Integer> stack1;
       Deque<Integer> stack2;
        public MyQueue() {
            stack1 = new ArrayDeque<Integer>();
            stack2 = new ArrayDeque<Integer>();
        }

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

        public int pop() {
            dumpStack1();
            return stack2.pop();
        }

        public int peek() {
            dumpStack1();
            return stack2.peek();
        }

        public boolean empty() {
            return stack1.isEmpty() && stack2.isEmpty();
        }

        //抽出方法,如果stack2为空,那么将stack1中的元素全部放到stack2中
        private void dumpStack1(){
            if (stack2.isEmpty()){
                while (!stack1.isEmpty()){
                    stack2.push(stack1.pop());
                }
            }
        }
}

/**
 * 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();
 */

225. 用队列实现栈

这道题有很多的思路可以借鉴
法一:存在队列1、队列2,一定有一个队列是保持没数的。
push的时候,向有数的队列中加入队尾。
pop的时候,有数的一方向无数的队列倾倒数,直到有数的一方的size等于1,这时候pop出来
peek的时候,有数的一方向无数的队列倾倒数,直到有数的一方的size等于1,这时候peek出来,保存数,再将数继续倾入另一个队列
判断是不是empty的时候,如果两个都为空就是空。
相关的代码:

class MyStack {
        Queue<Integer> queue1;
        Queue<Integer> queue2;
        public MyStack() {
            queue1 = new LinkedList<>();
            queue2 = new LinkedList<>();
        }

        public void push(int x) {
            if (!queue1.isEmpty()){
                queue1.add(x);
            }else {
                queue2.add(x);
            }
        }

        public int pop() {
            return dump();
        }

        public int top() {
            int res = dump();
            if (!queue1.isEmpty()){
                queue1.add(res);
            }else {
                queue2.add(res);
            }
            return res;
        }

        public boolean empty() {
            return queue2.isEmpty() && queue1.isEmpty();
        }

        private int dump(){
            if (!queue2.isEmpty()){
                while (queue2.size() != 1){
                    queue1.add(queue2.poll());
                }
                int res = queue2.poll();
                return res;
            }else {
                while (queue1.size() != 1){
                    queue2.add(queue1.poll());
                }
                int res = queue1.poll();
                return res;
            }
        }
}

/**
 * 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();
 */

法二:存在队列1、队列2,一定有一个队列是保持没数的。
push的时候:存入无数的队列中数,再将有数的队列中的数全部倾倒入无数的队列中,形成反着来的顺序,也就是栈的顺序。
peek的时候,直接peek就行
pop的时候,直接pop就行
判断是不是empty的时候,如果两个都为空就是空。
相关的代码:

class MyStack {
        Queue<Integer> queue1;
        Queue<Integer> queue2;
        public MyStack() {
            queue1 = new LinkedList<>();
            queue2 = new LinkedList<>();
        }

        public void push(int x) {
            if (!queue1.isEmpty()){
                queue2.add(x);
                while (!queue1.isEmpty()){
                    queue2.add(queue1.poll());
                }
            }else {
                queue1.add(x);
                while (!queue2.isEmpty()){
                    queue1.add(queue2.poll());
                }
            }
        }

        public int pop() {
            if (!queue1.isEmpty()){
               return queue1.poll();
            }else {
               return queue2.poll();
            }
        }

        public int top() {
            if (!queue1.isEmpty()){
                return queue1.peek();
            }else {
                return queue2.peek();
            }
        }

        public boolean empty() {
            return queue2.isEmpty() && queue1.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();
 */

法三:一个队列
push的时候,先加入到队尾,再依次从头弹出加入到队尾,弹入的次数是加入新数前的次数
pop的时候,直接pop
peek的时候,直接peek
判断是不是empty的时候,如果两个都为空就是空。

class MyStack {
        Queue<Integer> queue;
        public MyStack() {
            queue = new LinkedList<>();
        }

        public void push(int x) {
            int size = queue.size();
            queue.add(x);
            while (size != 0){
                queue.add(queue.poll());
                size--;
            }
        }

        public int pop() {
               return queue.poll();
        }

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

        public boolean empty() {
            return queue.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();
 */

20. 有效的括号

括号匹配是使用栈解决的经典问题。
如果还记得编译原理的话,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。
在这里插入图片描述
再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。
这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(其实可以出一道相应的面试题了)

思路:
分三种情况:

  1. ((((]]左括号多了
  2. (([}))中间有不匹配的
  3. (()))右括号多了

第一种情况,遇到左括号就填入stack,遇到右括号就弹出比对,是不是成对的,最后stack不为空为false
第二种情况,遇到左括号就填入stack,遇到右括号就弹出比对,是不是成对的,发现不成对为false
第三种情况,遇到左括号就填入stack,遇到右括号就弹出比对,是不是成对的,最后stack为空没法匹配为false
代码如下:

class Solution {
    public boolean isValid(String s) {
        Map<Character,Character> map = new HashMap<>();
        map.put(')','(');
        map.put(']','[');
        map.put('}','{');
        Deque<Character> stack = new ArrayDeque<Character>();
        char[] arr = s.toCharArray();
        for (char a : arr){
            if (!map.containsKey(a)){
                stack.push(a);
            }else {

                if (stack.isEmpty() || stack.pop().charValue() != map.get(a)){
                    return false;
                }
            }
        }
        if (stack.isEmpty()){
            return true;
        }else {
            return false;
        }
    }
}

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

题外话:
在企业项目开发中,尽量不要使用递归!
在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!)

思路:
利用栈
当栈是空的时候,直接加入。
如果加入的数和栈顶的一样,栈顶弹出。
如果加入的数和栈顶的不一样,加入。
最后,弹出栈的字符,反转

class Solution {
    public String removeDuplicates(String s) {
        char[] arr = s.toCharArray();
        Deque<Character> stack = new ArrayDeque<>();
        for (char a : arr){
            if (stack.isEmpty()){
                stack.push(a);
            }else if (stack.peek().charValue() != a){
                stack.push(a);
            }else {
                stack.pop();
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!stack.isEmpty()){
            sb.append(stack.pop());
        }
        return sb.reverse().toString();
    }
}

150. 逆波兰表达式求值

题外话:
我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。

例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算法,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法,你说麻不麻烦!

那么将中缀表达式,转化为后缀表达式之后:[“4”, “13”, “5”, “/”, “+”] ,就不一样了,计算机可以利用栈里顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的。

思路:
遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。

相关代码:

class Solution {
    public int evalRPN(String[] tokens) {
        Set<String> set = new HashSet<>();
        set.add("+");
        set.add("-");
        set.add("*");
        set.add("/");
        Deque<String> stack = new ArrayDeque<>();
        for (String abc : tokens){
            if (!set.contains(abc)){
                stack.push(abc);
            }else {
                int second = Integer.parseInt(stack.pop());
                int first = Integer.parseInt(stack.pop());
                stack.push(String.valueOf(calculate(first, second, abc)));
            }
        }
        return Integer.parseInt(stack.pop());
    }

    public int calculate(int a, int b, String sign){
        if ("/".equals(sign)){
            return a / b;
        }else if ("+".equals(sign)){
            return a + b;
        }else if ("*".equals(sign)){
            return a * b;
        }else {
            return a - b;
        }
    }
}
    public static int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
        //存储结果值
        int[] res = new int[len - k + 1];
        int index = 0;
        //建立双端对列,维持一个单调递减的
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < len; i++) {
            //如果加入的数比最后一个大,因为要优先放编号大的,所以都弹出直到小于
            while (!list.isEmpty() && nums[list.peekLast()] <= nums[i]) {
                list.pollLast();
            }
            //****注意,这里加入的是编号
            list.addLast(i);
            //除去不属于现在k空间范围上的,因为编号一直递增,所以一定会排干净
            if (list.peekFirst() == i - k) {
                list.pollFirst();
            }
            //当i + 1 >= k的时候,结果数组就可以存数了
            if (i + 1 >= k) {
                //注意双端队列中存的是地址,所以nums[]
                res[index++] = nums[list.peekFirst()];
            }
        }
        return res;
    }

239. 滑动窗口最大值

思路:
像这种滑动窗口,从前加入,从后弹出的,双端队列最好用
双端队列用:LinkedList queue = new LinkedList();
双端队列操作步骤:

  1. 里面的头值是这个k区间最大的。
  2. addLast:如果add的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到add元素的数值小于队列入口元素的数值为止
  3. pollFirst(value):如果窗口头值的元素编号到达临界值编号,那么队列弹出元素,否则不用任何操作

代码

    public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
		//建立答案个数目的返回数组
        int[] res = new int[len - k + 1];
        int index = 0;
        //双端队列
        LinkedList<Integer> list = new LinkedList<>();
        for(int i = 0; i < len; i++){
        	//空的时候,比最后一个值小的时候可以直接进,但是遇到大于,等于(编号越大越好)最后一个值的时候,就需要从后弹出,直到比要加入的大
            while(!list.isEmpty() && nums[list.peekLast()] <= nums[i]){
                list.pollLast();
            }
            //注意,这里存的是编号
            list.addLast(i);
            //因为编号一直是上升的,如果可以满足最大值并且保留在队列中,总会遇到 i - k的时候,所以可以排干净
            if(list.peekFirst() == i - k){
                list.pollFirst();
            }
            //遇到完整的【k】就可以依次保存最大值了
            if(i + 1 >= k){
                //注意双端队列中存的是地址,所以nums[]
                res[index++] = nums[list.peekFirst()];
            }
        }
        return res;
    }

347. 前 K 个高频元素

//默认是小根堆
PriorityQueue<Interger> heap = new PriorityQueue<>();

//下面试利用Collections里的方法,对集合进行排序
Collections.sort(dictionary, (a, b) -> {
  //b在前,倒序; a在前,正序
  if (a.length() != b.length()) return b.length() - a.length();
  //字典序比较
  return a.compareTo(b);
});

在这里插入图片描述
思路:

  1. 借助 哈希表 来建立数字和其出现次数的映射,遍历一遍数组统计元素的频率
  2. 维护一个元素数目为 k 的最小堆
  3. 每次都将新的元素与堆顶元素(堆中频率最小的元素)进行比较
  4. 如果新的元素的频率比堆顶端的元素大,则弹出堆顶端的元素,将新的元素添加进堆中
  5. 最终,堆中的 k 个元素即为前 k 个高频元素
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        //得出每一个单词的词频
        for (int i : nums){
            map.put(i, map.getOrDefault(i, 0) + 1);
        }
        //定义优先级队列,存储k个空间,按照对应的字频来比较
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(map::get));
        for (int i : map.keySet()){
            if (priorityQueue.isEmpty() || priorityQueue.size() < k){
                priorityQueue.add(i);
            }else if (map.get(i) > map.get(priorityQueue.peek())){
                priorityQueue.poll();
                priorityQueue.add(i);
            }
        }
        int[] res = new int[k];
        int index = 0;
        while (!priorityQueue.isEmpty()){
            res[index++] = priorityQueue.poll();
        }
        return res;
    }
}

总结

栈的理论基础

  1. C++中stack,queue 是容器么?
  2. 我们使用的stack,queue是属于那个版本的STL?
  3. 我们使用的STL中stack,queue是如何实现的?
  4. stack,queue 提供迭代器来遍历空间么?

一道面试题C++
栈里面的元素在内存中是连续分布的么?

陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值