Leetcode 刷题笔记(十) —— 栈与队列篇之经典题目

文章目录

  • 系列文章目录
  • 前言
    • 栈和队列的简单使用
  • 题录
    • 232. 用栈实现队列
    • 225. 用队列实现栈
    • 20. 有效的括号
    • 1047. 删除字符串中的所有相邻重复项
    • 150. 逆波兰表达式求值
    • 239. 滑动窗口最大值

系列文章目录

一、 数组类型解题方法一:二分法
二、数组类型解题方法二:双指针法
三、数组类型解题方法三:滑动窗口
四、数组类型解题方法四:模拟
五、链表篇之链表的基础操作和经典题目
六、哈希表篇之经典题目
七、字符串篇之经典题目
八、字符串篇之 KMP
九、解题方法:双指针
十、栈与队列篇之经典题目
十 一、栈与队列篇之 top-K 问题
更新中 …


前言

刷题路线来自 :代码随想录

栈和队列的简单使用

栈: 先进后出

	// 栈
	Stack<Integer> stack = new Stack<>();
	// 入栈
	stack.push(1);  // 返回入栈元素
	stack.add(2);  // 返回 boolean 类型
	// 出栈,返回出栈元素
	stack.pop();
	// 返回栈顶元素
	stack.peek();

队列: 先进先出

	// 队列
	Queue<Integer> queue = new LinkedList<>();
	// 入队列
	queue.offer(1);  // 返回 boolean 类型
	queue.add(2);  // 返回 boolean 类型
	// 出队列,返回出队列元素
	queue.poll();
	// 返回队头元素
	queue.peek();  

双端队列: 双端队列可以返回队头和队尾元素,并且两端都可以进行出队和入队操作

	// 双端队列
	Deque<Integer> deque = new ArrayDeque<>();
	Deque<Integer> deque1 = new LinkedList<>();
	// 入队列,返回 boolean 类型
	deque.addFirst(1); // 头插
	deque.addLast(1);  // 尾入
	
	// 出队列
	deque.pollFirst();  // 队头
	deque.pollLast();   // 队尾
	
	// 返回队头元素和队尾元素
	deque.peekFirst();  // 队头
	deque.peekLast();   // 队尾

优先级队列(堆): 默认为小根堆(队头为最大或最小元素,入队时会自己调整)

	// 优先级队列
	PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
	// 方法同 Queue
	...
	// 大堆的创建
    PriorityQueue<Integer> maxHeap=new PriorityQueue<>(new Comparator<Integer>(){
       @Override
       public int compare(Integer o1,Integer o2){
           return o2-o1;
       }
   });

题录

232. 用栈实现队列

Leetcode 链接
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:
输入:
[“MyQueue”, “push”, “push”, “peek”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

题解:
队列为先进先出,而栈为先进后出,所以使用栈模拟队列,模拟队列在出队列时需要将栈中元素按顺序出栈放到另外一个栈中再次出栈才能保证先进先出。也就是把一个栈中的元素放入另一个栈,会改变顺序。

class MyQueue {
	// 负责进栈
    Stack<Integer> stackIn = null;
    // 负责出栈
    Stack<Integer> stackOut = null;
    public MyQueue() {
        stackIn = new Stack<>();
        stackOut = new Stack<>();
    }
    // 模拟进队列
    public void push(int x) {
        stackIn.push(x);
    }
    // 模拟出队列
    public int pop() {
    	// 1.stackOut 为空:将 stackIn 中元素全部以出栈顺序入栈
    	// 2.stackOut 中右元素,直接出栈
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.pop();
    }
    // 返回队列对头元素
    public int peek() {
    	// 同模拟出队列一样
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        // 最后返回 stackOut 栈顶元素
        return stackOut.peek();
    }
    // 队列是否为空
    public boolean empty() {
    	// stackIn 和 stackOut 都为空,队列才为空
        return stackIn.isEmpty() && stackOut.isEmpty();
    }
}

225. 用队列实现栈

Leetcode 链接
请你仅使用两个队列实现一个后入先出(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(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:
输入:
[“MyStack”, “push”, “push”, “top”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

题解:
如果像上题一样想着用一个输入队列,一个输出队列,是不行的。因为队列中的数据导入另一个队列中,数据的顺序并没有变。这里使用一个队列,模拟出栈时,将元素从队头移动到队尾直到原队列队尾元素为现在的队头。

class MyStack {
	// 模拟栈用的队列
    Queue<Integer> queue = null;
    public MyStack() {
        queue = new LinkedList<>();
    }
    // 入栈
    public void push(int x) {
        queue.add(x);
    }
    // 出栈
    public int pop() {
    	// 移动 n 次,队尾元素移动到队头
        int n = queue.size() - 1;
        while (n-- > 0) {
            queue.add(queue.poll());
        }
        // 此时队头元素为原队尾元素,出队列并返回
        return queue.poll();
    }
    // 返回栈顶元素
    public int top() {
    	// 同出栈一样,最后得到原队尾元素后再添加到队尾即可
        int n = queue.size() - 1;
        while (n-- > 0) {
            queue.add(queue.poll());
        }
        int res = queue.peek();
        queue.add(queue.poll());
        return res;
    }
    // 返回栈是否为空
    public boolean empty() {
        return queue.isEmpty();
    }
}

20. 有效的括号

Leetcode 链接
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

示例 1:
输入:s = “()”
输出:true

示例 2:
输入:s = “()[]{}”
输出:true

示例 3:
输入:s = “(]”
输出:false

示例 4:
输入:s = “([)]”
输出:false

示例 5:
输入:s = “{[]}”
输出:true
题解:
使用栈模拟匹配过程,遍历 s ,若是左括号入栈。若是右括号,如与栈顶括号匹配,弹出栈顶元素;若不匹配或者剩下和不够都意味着不能全部匹配上返回 false。

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == '(' || ch == '[' || ch == '{') {
            	// 是左括号,入栈
                stack.push(ch);
            } else {
            	// 右括号,判断是否与栈顶元素匹配
                if (!stack.isEmpty() && isMate(stack.peek(), ch)) {
                	// 不匹配,弹出栈顶元素
                    stack.pop();
                } else {
                	// 没有左括号与其匹配,返回 false
                    return false;
                }
            }
        }
        // 如剩下左括号,栈不为空,返回 fasle;若全部匹配成功,栈空,返回 true
        return stack.isEmpty();
    }
	// 判断两个括号是否匹配
    public boolean isMate(char l, char r) {
        if (l == '(' && r == ')') return true;
        if (l == '{' && r == '}') return true;
        if (l == '[' && r == ']') return true;
        return false;
    }
}

优化:如果是左括号,入栈时可以用宇宙匹配的右括号入栈,这样在判断是否与栈顶元素匹配时只需要判断两个右括号是否相同即可

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == '(') stack.push(')');
            else if (ch == '{') stack.push('}');
            else if (ch == '[') stack.push(']');
            else if (!stack.isEmpty() && ch == stack.peek()) {
                stack.pop();
            } else {
                return false;
            }
        }
        return stack.isEmpty();
    }
}

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

Leetcode 链接
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:
输入:“abbaca”
输出:“ca”
解释:例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。

题解:
消消乐:遍历 s, 栈为空或者 字符与栈顶元素不同,入栈;如果栈不为空,并且字符与栈顶元素相同,弹出栈顶元素

class Solution {
    public String removeDuplicates(String s) {
        Stack<Character> stack = new Stack<>();
        // 遍历 s
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (!stack.isEmpty() && ch == stack.peek()) {
            	// 如果栈不为空,并且与栈顶元素相同,弹出栈顶元素
                stack.pop();
            } else {
            	// 栈为空或者与栈顶元素不同,入栈
                stack.push(ch);
            }
        }
        // 拼接栈中剩余字符并返回
        StringBuilder sb = new StringBuilder();
        for (char ch : stack) {
            sb.append(ch);
        }
        return String.valueOf(sb);
    }
}

优化:StringBuilder 可以充当栈,StringBuilder 有 deleteCharAt 可以删除指定下标元素

class Solution {
    public String removeDuplicates(String s) {
        StringBuilder sb = new StringBuilder();
        // 栈顶元素指针,始终指向栈顶元素,需要在入栈和出栈时动态维护
        int top = -1;
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (top >= 0 && ch == sb.charAt(top)) {
                sb.deleteCharAt(top);
                top--;
            } else {
                sb.append(ch);
                top++;
            }
        }
        return sb.toString();
    }
}

双指针法:出栈和入栈由数组中元素的覆盖代替,慢指针指向栈顶元素,快指针遍历字符

    public String removeDuplicates(String s) {
        char[] arr = s.toCharArray();
        int slow = 0;
        for (int fast = 1; fast < s.length(); fast++) {
            if (slow >= 0 && arr[fast] == arr[slow]) {
            	// 相同时,出栈,栈顶指针 slow--
                slow--;
            } else {
            	// 不相同时,应入栈。入栈位置在栈顶位置后,所以 slow++,再覆盖
                slow++;
                arr[slow] = arr[fast];
            }
        }
        return String.valueOf(arr).substring(0, slow + 1);
    }

150. 逆波兰表达式求值

Leetcode 链接
根据 逆波兰表示法,求表达式的值。有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
注意 两个整数之间的除法只保留整数部分。可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例 1:
输入:tokens = [“2”,“1”,"+",“3”,"*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:
输入:tokens = [“4”,“13”,“5”,"/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:
输入:tokens = [“10”,“6”,“9”,“3”,"+","-11","","/","",“17”,"+",“5”,"+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
题解:
遍历 tokens ,第一次遇见运算符结合前两个数运算即可,如果遇见两个连载一起的运算符怎么办呢?
需要解决掉第一个运算符,并更新前两个数字,这样第二个运算符前边依旧是两个数字,这里使用栈去维护

  1. 如果是运算符,弹出栈顶两个数运算,并将运算结果入栈;
  2. 不是运算符,转为数字入栈
  3. 返回栈顶结果
class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < tokens.length; i++) {
            String str = tokens[i];
            // 1.如果是运算符,弹出栈顶两个数运算,并将运算结果入栈
            if (str.equals("+")) {
                stack.push(stack.pop() + stack.pop());
            } else if (str.equals("-")) {
                int num = stack.pop();
                stack.push(stack.pop() - num);
            } else if (str.equals("*")) {
                stack.push(stack.pop() * stack.pop());
            } else if (str.equals("/")) {
                int num = stack.pop();
                stack.push(stack.pop() / num);
            } else {
            	// 2.不是运算符,转为数字入栈
                stack.push(Integer.valueOf(str));
            }
        }
        // 3.返回栈顶结果
        return stack.pop();
    }
}

239. 滑动窗口最大值

Leetcode 链接
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值 。

示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口 的 位置 | 最大值

[1 3 -1] -3 5 3 6 7 | 3
1 [3 -1 -3] 5 3 6 7 | 3
1 3 [-1 -3 5] 3 6 7 | 5
1 3 -1 [-3 5 3] 6 7 | 5
1 3 -1 -3 [5 3 6] 7 | 6
1 3 -1 -3 5 [3 6 7] | 7

示例 2:
输入:nums = [1], k = 1
输出:[1]
题解:[ . . . .] 表示窗口中的值,窗口中有些区间的数不可能作为窗口中最大的数,我们先来分析分析

  1. [1,2,3,4,3,2,1] 中 前边123都不要,因为左边 [1,2,3,4] 中4让最大值不可能在 小于4 的123中出现,所以窗口中保留 [4,3,2,1];

  2. 那 4 后边的 321 还有用吗?有用,因为[4,3,2,1] 当 4 移出窗口后,3便成为了最大的数

  3. [4,3,2,1] 这时来了个5,[6,3,2,1,5] 中只有 [6,5] 有用,因为只要有 321 在 5 左边并且和 5 同时存在作为窗口左边一部分时,最大值只可能在5 或者 5 右边的窗口中出现。

所以我们来维护一个队列保存窗口中有用的数,保证队列中数从左到右,从大到小也就是一个单调队列。比如窗口 [5,1,2,3],新来一个数4,单调队列中应保存 [5,4]。而单调队列最左边就为最窗口中的最大数

那么当 k = 5 时,窗口右移,5 要出队列,怎么判断队头元素是否该出队列呢?
队列中应保存下标,如 5 下标是 2,新来的数下标是 7,通过下标差值判断队头元素是否要出队列。

为什么要使用双端队列呢?
窗口 [5,1,2,3,4],4是新来的 --> 队列 [5,4],需要新来的 4 和队尾元素比较,因为 4 大于队尾元素,队尾元素依次出队列

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length - k + 1];
        // 双端队列
        Deque<Integer> deque = new ArrayDeque<>();
        // 遍历数组
        for (int i = 0; i < nums.length; i++) {
        	// 队列不为空,deque.peekFirst() == i - k(窗口右移后,队头元素下标在窗口外)
            if (!deque.isEmpty() && deque.peekFirst() == i - k) {
            	// 队头元素下标出队列
                deque.pollFirst();
            }
			// 窗口不为空,新来的 nums[i] 大于 队尾元素,队尾元素出队列
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            // 新来的数下标入队列
            deque.addLast(i);
			// 从第 k - 1 个数入窗口开始,就要记录窗口中最大数了
            if (i >= k - 1) {
            	// 队头就为窗口中最大数下标,记录在返回数组中
                res[i - k + 1] = nums[deque.peek()];
            }
        }
        return res;
    }
}

你可能感兴趣的:(算法,leetcode,链表,算法)