栈与队列理论基础
栈(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);(两个队列)
以上是个人的思考反思与总结,若只想根据系列题刷,参考卡哥的网址代码随想录算法官网代码随想录算法官网代码随想录算法官网