目录
栈与队列理论基础
栈(Stack)
栈是一种后进先出(LIFO)的数据结构,即最近添加到栈中的元素将是第一个被移除的元素。栈通常有两个主要的操作:
push
用于添加一个元素到栈顶,而pop
用于移除栈顶的元素。此外,还有一个peek
操作,它允许查看栈顶元素而不移除它。在Java中,
Stack
类是一个继承自Vector
的类,提供了栈的实现。但是,由于Vector
是同步的,这可能导致不必要的性能开销,因此在单线程应用程序中,更推荐使用ArrayDeque
类,它实现了Deque
接口并提供了一个更高效的栈实现。
队列(Queue)
队列是一种先进先出(FIFO)的数据结构,即最先添加到队列中的元素将是第一个被移除的元素。队列的基本操作包括
enqueue
(添加元素到队列尾部)和dequeue
(移除队列头部的元素)。与栈类似,还有一个peek
操作,用于查看队列头部的元素而不移除它。Java中,
LinkedList
类可以作为队列使用,因为它实现了List
接口,而List
接口又扩展了Queue
接口。但是,LinkedList
不是专门为队列操作优化的。对于需要高性能队列的场景,可以使用ArrayDeque
类,它同样实现了Deque
接口,并且提供了队列的高效实现。
栈与队列的使用场景
- 栈:适用于需要后进先出特性的场合,例如函数调用栈(每次调用新函数时将其推入栈,返回时弹出栈),表达式求值(如后缀表达式的计算),以及深度优先搜索(DFS)等。
- 队列:适用于需要先进先出特性的场合,例如任务调度(按顺序处理任务),广度优先搜索(BFS),以及实现生产者-消费者问题等。
栈和队列的物理与逻辑存储结构
物理存储结构指的是数据在计算机内存中实际的存储方式,而逻辑存储结构描述的是数据元素之间的抽象关系。
- 数组是一种顺序存储结构,数据在内存中连续存放。
- 链表是一种链式存储结构,数据在内存中可能分散存放,每个元素通过指针连接。
栈和队列都可以通过数组和链表来实现,具体选择哪种物理存储结构取决于具体的应用场景和性能需求。例如,对于栈,如果我们知道栈的大小不会变化太多,可以使用数组实现;如果栈大小经常变化,使用链表可能更加合适。对于队列,如果队列操作频繁,且队列大小固定,可以使用循环数组实现;如果队列大小可变,使用链表实现可能更加高效。
算法题
Leetcode 232.用栈实现队列
题目链接:232.用栈实现队列
大佬视频讲解:用栈实现队列视频讲解
个人思路
需要两个栈一个输入栈,一个输出栈;push数据的时候,数据直接放进输入栈就好;但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来,再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
解法
class MyQueue {
Stack<Integer> stackIn;//输入栈
Stack<Integer> stackOut;//输出栈
public MyQueue() {
stackIn = new Stack<>(); // 负责进栈
stackOut = new Stack<>(); // 负责出栈
}
public void push(int x) {//插入数据
stackIn.push(x);
}
public int pop() { //弹出数据
dumpstackIn();//1.先将输入栈全部弹出到输出栈
return stackOut.pop();//2.再弹出输出栈数据
}
public int peek() {//获取一个输入的数据
dumpstackIn();//1.先将输入栈全部弹出到输出栈
return stackOut.peek();//2.输出栈的第一个数据 就是队列的第一个数据
}
public boolean empty() {//判空
return stackIn.isEmpty() && stackOut.isEmpty();
}
// 如果stackOut为空,那么将stackIn中的元素全部放到stackOut中
private void dumpstackIn(){
if (!stackOut.isEmpty()) return;
while (!stackIn.isEmpty()){
stackOut.push(stackIn.pop());//将输入栈全部弹出到输出栈
}
}
}
时间复杂度:
O(n);(
push和empty为O(1), pop和peek为O(n))
空间复杂度:O(n);(2个辅助栈)
Leetcode 225. 用队列实现栈
题目链接:225. 用队列实现栈
大佬视频讲解:225. 用队列实现栈
个人思路
也是需要用两个队列que1和que2实现栈的功能,其中que2其实完全就是一个数据备份的作用.在实现栈的push功能时,先将push 的元素放进que2,再看que1中有没有其他元素,有就把元素输出放入que2,再将que2变成que1, 这样que1弹出元素的顺序和原来栈完全一样了;
举个例子
队列1中现在有一个元素1, 需要放入4, 先将4放入队列2备份,再将1弹出放入队列2,这样队列2就变成了1,4 再将队列2和队列1交换,这样队列1的顺序就和栈的顺序一样了; 这样后续实现栈的弹出就直接弹出即可
解法
class MyStack {
Queue<Integer> queue1; // 和栈中保持一样元素的队列
Queue<Integer> queue2; // 备份队列
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
queue2.offer(x); // 先放在备份队列中
while (!queue1.isEmpty()){
queue2.offer(queue1.poll());//将队列1的元素全部弹出放入队列2
}
Queue<Integer> queueTemp;
queueTemp = queue1;
queue1 = queue2;
queue2 = queueTemp; // 最后交换队列1和队列2,将元素都放到队列1中
}
public int pop() {
return queue1.poll(); // 因为queue1中的元素和栈中的保持一致,所以这个和下面两个的操作只看queue1即可
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
时间复杂度:O(n);(pop为O(n),其他为O(1))
空间复杂度:O(n);(两个队列)
还可以优化到一个队列实现
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<>();
}
public void push(int x) {
queue.add(x);
}
public int pop() {
rePosition();
return queue.poll();
}
public int top() {
rePosition();
int result = queue.poll();
queue.add(result);
return result;
}
public boolean empty() {
return queue.isEmpty();
}
public void rePosition(){//将队头元素移到队尾 除了最后一个元素
int size = queue.size();
size--;
while(size-->0)
queue.add(queue.poll());
}
}
时间复杂度:O(n);(top,pop为O(n),其他为O(1))
空间复杂度:O(n);(两个队列)
以上是个人的思考反思与总结,若只想根据系列题刷,参考卡哥的网址代码随想录算法官网