一、理论基础
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
(双端队列)接口及其实现类(如ArrayDeque
、LinkedList
)也可以用于实现栈。- 这是推荐的方式,因为
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
LinkedList
是 Queue
的常用实现类之一,底层是一个双向链表,适合于频繁的插入和删除操作。
2. ArrayDeque
ArrayDeque
是一个高效的双端队列实现类,适合用作栈或队列。它比 LinkedList
更快,且没有容量限制。
3. PriorityQueue
PriorityQueue
是一个基于堆的优先级队列,它的元素按照自然顺序或指定的比较器排序。最小元素(或根据比较器规则最优先的元素)在队列头部。
4. ConcurrentLinkedQueue
ConcurrentLinkedQueue
是一个无界线程安全队列,基于链接节点的无锁算法实现,适用于高并发场景。
二、题目
本题考查用两个栈实现队列操作。
使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。
在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()。
在工业级别代码开发中,最忌讳的就是 实现一个类似的函数,直接把代码粘过来改一改就完事了。
这样的项目代码会越来越乱,一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)
队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。
所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。
但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的!
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();
*/
有一些技巧,在匹配左括号的时候,对应右括号入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了
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;
}
}
匹配问题都是栈的强项
本题要删除相邻相同元素,相对于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;
}
}
将中缀表达式,转化为后缀表达式之后,计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的。
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();
}
}
这是使用单调队列的经典题目。
暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是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.优先队列
什么是优先级队列呢?
其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
用小顶堆,因为要统计最大前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操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。
不要以为本题中的单调队列实现就是固定的写法。
求前 K 个高频元素
什么是优先级队列呢?
其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
本题就要使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序!
所以排序的过程的时间复杂度是 O(log k) ,整个算法的时间复杂度是 O(n*log k) 。