1、栈与队列理论基础
2、用栈实现队列
- 题目:https://leetcode.cn/problems/implement-queue-using-stacks/
- 思路:用两个栈,一个栈作压入,一个栈作弹出;
- 压入正常压;
- 弹出的时候如果弹出栈里面为空,就按把压入栈的所有元素全部压入弹出栈,这样先被压入的元素就仍然会先弹出;
- 获取队头元素就复用弹出,将弹出(该操作可以复用)栈栈顶元素弹出再压入回来;
- 代码实现:
class MyQueue {
public Stack<Integer> stackIn;
public Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
if(stackOut.isEmpty()){
while(!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
return stackOut.pop();
}
public int peek() {
int val = this.pop();
stackOut.push(val);
return val;
}
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
}
3、用队列实现栈
- 题目:https://leetcode.cn/problems/implement-stack-using-queues/
- 思路:只用一个队列实现,需要注意的地方就是每次插入新元素后,为了让其成为对头,在插入后将其前面的所有元素从队列弹出再插入;
- 插入时在当前元素被插入以后,将其前面的所有元素弹出再插入一遍,实现栈的后入先出;
- 弹出正常弹出;
- 栈顶元素直接获取队列的队头元素;
- 判断是否为空调用队列的判断为空;
- 代码实现:
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<>();
}
public void push(int x) {
queue.offer(x);
int size = queue.size();
while(size-- > 1){
queue.offer(queue.poll());
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
4、有效的括号
- 题目:https://leetcode.cn/problems/valid-parentheses/
- 思路:就近匹配,利用栈,每当出现一个右括号,栈顶应该有一个与其对应的;
- 每遇到一个左括号,将其对应的右括号(方便判断)压入栈;
- 每遇到一个右括号,如果栈不为空并且栈顶与其相同——跳过下一个;
- 否则,返回false;
- 代码实现:
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
char c;
for(int i = 0; i < s.length(); ++i){
c = s.charAt(i);
if(c == '('){
stack.push(')');
}else if(c == '['){
stack.push(']');
}else if(c == '{'){
stack.push('}');
}else if(stack.isEmpty() || stack.pop() != c ){
return false;
}
}
if(stack.isEmpty()){
return true;
}
return false;
}
}
5、删除字符串中的所有相邻重复项
- 题目:https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/
- 思路:利用栈的后进先出,每当当前元素与栈顶元素匹配就进行删除操作;为了方便结果是字符串,用字符串StringBuilder 模拟栈;
- 每遇到一个字符,如果栈(StringBuilder )中有元素并且栈顶元素与其相同,将栈中元素删除;
- 否则,将其加入栈中;
- 代码实现:
class Solution {
public String removeDuplicates(String s) {
StringBuilder sb = new StringBuilder();
int top = -1;
char c;
for(int i = 0; i < s.length(); ++i){
c = s.charAt(i);
if(top >= 0 && sb.charAt(top) == c){
sb.deleteCharAt(top--);
}else{
sb.append(c);
++top;
}
}
return sb.toString();
}
}
6、逆波兰表达式求值
- 题目:https://leetcode.cn/problems/evaluate-reverse-polish-notation/
- 思路:计算机进行运算的思考方式;栈,遇到数字压入栈中,遇到运算符从栈中取数进行操作;避免了传统运算方式中顺序读取遇到优先级更高的运算符打乱运算顺序;
- 遇到数字——入栈;
- 遇到运算符,根据运算符从栈中弹出两个数字进行运算;
- 总结果最终返回;
- 代码实现:
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(String s : tokens){
if("+".equals(s)){
stack.push(stack.pop() + stack.pop());
}else if("-".equals(s)){
int num1 = stack.pop();
int num2 = stack.pop();
stack.push(num2 - num1);
}else if("*".equals(s)){
stack.push(stack.pop() * stack.pop());
}else if("/".equals(s)){
int num1 = stack.pop();
int num2 = stack.pop();
stack.push(num2 / num1);
}else{
stack.push(Integer.parseInt(s));
}
}
return stack.pop();
}
}
7、滑动窗口最大值
- 题目:https://leetcode.cn/problems/sliding-window-maximum/
- 思路:单调队列!滑动窗口滑动时,保持窗口内的数被队列单调记录,只用返回队列头部的元素就可以;
- 首先要自己实现单调队列,关键在于每次的插入操作执行后能够维持队列内的单调性——每插入一个元素,从队列尾部开始比较,只要尾部元素比它小就弹出并且一直弹到要插入的元素前面的元素都比它大,弹出去的那些元素本来就对统计窗口内的最大值没有意义,当前窗口已经有比他们大的 元素;
- 弹出元素时因为永远是最大的元素在队头,所以只要窗口移动后要弹出的元素刚好是队列头部的元素才去进行弹出,否则要弹出的小元素放在队列里面也不会影响窗口内的最大值;
- 代码实现:
class MyQueue {
private Deque<Integer> deque;
MyQueue(){
deque = new LinkedList<>();
}
public void push(int val){
while(!deque.isEmpty() && val > deque.getLast()){
deque.pollLast();
}
deque.offerLast(val);
}
public void poll(){
deque.poll();
}
public int peek(){
return deque.peekFirst();
}
}
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int len = n - k + 1;
int[] res = new int[len];
MyQueue queue = new MyQueue();
for(int i = 0; i < k; ++i){
queue.push(nums[i]);
}
res[0] = queue.peek();
for(int i = k; i < n; ++i){
if(nums[i - k] == queue.peek()){
queue.poll();
}
queue.push(nums[i]);
res[i - k + 1] = queue.peek();
}
return res;
}
}
8、前K个高频元素
- 题目:https://leetcode.cn/problems/top-k-frequent-elements/
- 思路:优先级队列!创建的优先级队列设置内部排序方式,大顶堆是栈顶(队首)元素大,最小的在栈底(队尾);小顶堆是栈顶(队首)元素最小,最大的在栈底(队尾);这里利用大顶堆,将所有元素插入优先级队列中,然后弹出前k个元素;
- 代码实现:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for(int num : nums){
map.put(num, map.getOrDefault(num, 0) + 1);
}
PriorityQueue<int[]> pq = new PriorityQueue<>((int1, int2) -> int2[1] - int1[1]);
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
pq.add(new int[]{entry.getKey(), entry.getValue()});
}
int[] res = new int[k];
for(int i = 0; i < k; ++i){
res[i] = pq.poll()[0];
}
return res;
}
}
9、栈与队列总结
- 栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布;
- 栈经典题目:括号匹配,字符串去重,逆波兰表达式,系统中的目录切换;递归实现借助栈在递归返回的时候重新取回局部变量、 参数值和返回地址;
- 队列经典题目:
- 单调队列:单调队列不是直接等价于对窗口里的数据进行排序(否则就是优先队列了),在滑动窗口最大值问题中,单调队列不是一直包含窗口里面的元素,每次插入元素的时候窗口里面一些小的元素可能直接被弹出并不在队列里面,而窗口移动走了的元素也有可能会在队列里面留着;队列最关心的任务是保持窗口的最大值在队首。
- 优先队列:大顶堆、小顶堆。