栈是 后进先出(LIFO),队列是 先进先出(FIFO)
一、双栈实现队列
核心思路:
实现的关键是给两个栈定好角色:
- 入队栈:只负责 "接收新元素"(入队操作直接压栈);
- 出队栈:只负责 "输出元素"(出队 / 取队首时从这里拿)。
核心操作逻辑就 3 步:
- 入队(offer) :直接把元素压入
inStack(简单粗暴,O (1)); - 出队 / 取队首(poll/peek) :
- 若
outStack为空,把inStack里的所有元素弹出并压入outStack(这一步会反转元素顺序,把栈的 LIFO 变成队列的 FIFO); - 若
outStack非空,直接从outStack弹出 / 查看栈顶(就是队列的队首);
- 若
- 判空(isEmpty):两个栈都为空时,队列才是空的。
代码实现:
java
/**
* 双栈实现队列
*/
public class MyQueue {
// 入队栈
private final Deque<Integer> inStack;
// 出队栈
private final Deque<Integer> outStack;
public MyQueue() {
// 用LinkedList实现Deque
inStack = new LinkedList<>();
outStack = new LinkedList<>();
}
/**
* 入队操作
*/
public void offer(int x) {
inStack.push(x);
}
/**
* 出队操作
*/
public int poll() {
// 确保出队栈有元素
transferInToOut();
if (outStack.isEmpty()) {
throw new NoSuchElementException("队列已空,无法出队");
}
return outStack.pop();
}
/**
* 获取队首元素(不移除)
* 队列为空时抛出NoSuchElementException
*/
public int peek() {
transferInToOut();
if (outStack.isEmpty()) {
throw new NoSuchElementException("队列已空,无法获取队首");
}
return outStack.peek();
}
/**
* 判断队列是否为空
*/
public boolean isEmpty() {
return inStack.isEmpty() && outStack.isEmpty();
}
/**
* 仅当出队栈为空时,将入队栈所有元素转移到出队栈
*/
private void transferInToOut() {
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
}
细节:
1、元素转移的时机:仅当 outStack 为空时转移
2、peek 方法别丢数据:peek 是 "查看队首" 不是 "拿走",所以不能直接 pop 元素 ------ 代码里通过outStack.peek()获取即可,不需要修改栈结构。
3、别用错栈的实现类:别用ArrayList实现Deque。ArrayList的remove(0)(对应栈的pop)是 O (n) 复杂度,而LinkedList的pop是 O (1),效率差很多。
4、空队列要抛异常,别返回默认值:空队列执行 poll/peek 时,返回 0 会让用户误以为 "队首是 0"------ 正确做法是抛NoSuchElementException,和 Java 原生Queue(比如LinkedList)的行为保持一致。
时间复杂度
- 入队(offer):O (1)------ 直接压入 inStack,无额外操作;
- 出队(poll)/ 取队首(peek) :均摊 O (1)------ 每个元素只会从 inStack 转移到 outStack 一次,后续出队直接从 outStack 拿,整体平均下来是 O (1)。
二、双队列实现栈
核心思路:
始终保持一个队列为空,另一个队列存所有元素,通过 "转移元素" 实现栈的 LIFO:
- 入栈(push) :直接把元素加到非空队列 里(如果都空,随便选一个,比如
q1); - 出栈(pop) :把非空队列里的前 n-1 个元素移到空队列,剩下的最后一个元素就是栈顶,直接弹出;
- 取栈顶(top):逻辑和 pop 一样,但弹出后要把这个元素再放到空队列里(别丢数据!);
- 判空(empty):两个队列都为空时,栈才是空的。
代码实现:
java
public class MyStack {
//栈:先进后出
//队列:先进先出
/**
* 双队列实现栈
*/
//定义两个队列
private Queue<Integer> q1;
private Queue<Integer> q2;
/**
*初始化两个空队列
*/
public MyStack(){
q1 = new LinkedList<>();
q2 = new LinkedList<>();
}
/**
* 入栈操作:将元素加入非空队列
* @param x
*/
public void push(int x){
// 优先加入非空队列
if (!q1.isEmpty()){
q1.offer(x);
}else {
q2.offer(x);
}
}
/**
* 出栈操作:弹出栈顶元素
* @return
*/
public int pop(){
Queue<Integer> src = q1.isEmpty() ?q2:q1;
Queue<Integer> dst = q1.isEmpty() ?q1:q2;
//将源队列前n-1个元素放到目标队列
while (src.size()>1){
dst.offer(src.poll());
}
//弹出最后一个元素
return src.poll();
}
/**
* 获取栈顶元素
* @return
*/
public int top(){
Queue<Integer> src = q1.isEmpty() ? q2 : q1;
Queue<Integer> dst = q1.isEmpty() ? q1 : q2;
// 移动前n-1个元素到目标队列
while (src.size() > 1) {
dst.offer(src.poll());
}
// 记录栈顶元素,并将其移到目标队列
int top = src.poll();
dst.offer(top);
return top;
}
/**
* 判断栈是否为空
* @return 栈空返回true,否则返回false
*/
public boolean empty() {
return q1.isEmpty() && q2.isEmpty();
}
}
细节:
1、用**final** 修饰q1和q2,必须在构造器里初始化------ 否则 IDE 会提示 "final 变量未初始化"
2、top 操作只是 "看一眼栈顶",不是 "拿走"。所以弹出 src 的最后一个元素后,必须把它加到 dst 里,否则这个元素就丢了
3、别用ArrayList实现 Queue!因为ArrayList的remove(0)(对应队列的poll())是 O (n) 复杂度,而LinkedList的poll()是 O (1),效率高多了。
4、空栈执行 pop/top 时,直接返回 0 会误导用户 ------ 应该抛NoSuchElementException,和 Java 原生栈(Stack类)的行为保持一致。
时间复杂度:
- push(入栈):O (1)------ 直接加元素到队列尾部,不用动其他元素;
- pop/top(出栈 / 取栈顶):O (n)------ 需要把 n-1 个元素从一个队列移到另一个队列。
用双队列实现栈的核心,其实是用 "队列的元素转移" 模拟 "栈的后进先出"------ 始终保持一个队列为空,通过移动元素让 "最后入队的元素" 变成 "队列的头元素"。