摘要
本文主要探讨了栈和队列的理论基础,并提供了LeetCode上的一道经典问题:225.用队列实现栈的解题思路和代码。
1、栈和队列的理论基础
1.1 Java中的栈LinkedList
在Java中,你可以使用LinkedList来实现栈数据结构。LinkedList是Java集合框架中的一个类,它实现了List接口,但也可以用作栈,因为它支持在列表的开头和末尾进行高效插入和删除操作,非常适合栈的操作。
下面是如何使用LinkedList来实现栈的基本操作:
-
创建一个LinkedList对象:
LinkedList<Integer> stack = new LinkedList<>(); -
入栈操作: 使用
push方法将元素推入栈顶。stack.push(1); stack.push(2); stack.push(3); -
出栈操作: 使用
pop方法从栈顶弹出并返回元素。int topElement = stack.pop(); // 弹出并返回栈顶元素 -
查看栈顶元素: 使用
peek方法查看但不移除栈顶元素。int topElement = stack.peek(); // 查看栈顶元素 -
检查栈是否为空: 使用
isEmpty方法检查栈是否为空。boolean isEmpty = stack.isEmpty(); // 检查栈是否为空 -
获取栈的大小: 使用
size方法获取栈中元素的数量。int size = stack.size(); // 获取栈的大小 -
遍历栈元素: 通常使用增强的for循环来遍历栈元素。
for (Integer element : stack) { // 处理每个元素 } -
注意事项:
LinkedList可以用作栈,但也可以用作队列或双端队列,因此要确保只使用栈的操作。- 在出栈前检查栈是否为空,以避免出现栈空异常。
- 使用
LinkedList实现的栈是线程安全的,适用于单线程环境。如果在多线程环境中使用栈,建议使用java.util.concurrent包中的ConcurrentLinkedDeque,它是线程安全的。
这些是使用LinkedList实现栈的基本操作和注意事项。LinkedList通常用于栈和队列等数据结构的实现,因为它提供了高效的插入和删除操作。
1.2 Java中的队列LinkedList
在Java中,你可以使用LinkedList来实现队列数据结构。LinkedList是Java集合框架中的一个类,它实现了List接口,但也可以用作队列,因为它支持在列表的开头和末尾进行高效插入和删除操作,非常适合队列的操作。
下面是如何使用LinkedList来实现队列的基本操作:
-
创建一个LinkedList对象:
LinkedList<Integer> queue = new LinkedList<>(); -
入队操作: 使用
offer方法将元素添加到队列的末尾。queue.offer(1); queue.offer(2); queue.offer(3); -
出队操作: 使用
poll方法从队列的开头删除并返回元素。int frontElement = queue.poll(); // 从队列开头删除并返回元素 -
查看队列的头元素: 使用
peek方法查看但不移除队列的头元素。int frontElement = queue.peek(); // 查看队列的头元素 -
检查队列是否为空: 使用
isEmpty方法检查队列是否为空。boolean isEmpty = queue.isEmpty(); // 检查队列是否为空 -
获取队列的大小: 使用
size方法获取队列中元素的数量。int size = queue.size(); // 获取队列的大小 -
遍历队列元素: 通常使用增强的for循环来遍历队列元素。
for (Integer element : queue) { // 处理每个元素 } -
注意事项:
LinkedList可以用作队列,但也可以用作栈、双端队列或普通列表,因此要确保只使用队列的操作。- 在出队前检查队列是否为空,以避免出现队列空异常。
- 使用
LinkedList实现的队列是线程安全的,适用于单线程环境。如果在多线程环境中使用队列,建议使用java.util.concurrent包中的ConcurrentLinkedQueue,它是线程安全的。
这些是使用LinkedList实现队列的基本操作和注意事项。LinkedList通常用于队列、栈、双端队列等数据结构的实现,因为它提供了高效的插入和删除操作。
1.3 Java中的队列ArrayDeque
ArrayDeque是Java中的一个双端队列(deque)实现,它可以用来实现队列的操作,但不包括栈的操作。下面是如何在Java中使用ArrayDeque来实现队列的基本操作:
1. 创建一个ArrayDeque对象:
ArrayDeque<Integer> queue = new ArrayDeque<>();
2. 入队操作: 使用offer方法将元素添加到队列的末尾。
queue.offer(1);
queue.offer(2);
queue.offer(3);
3. 出队操作: 使用poll方法从队列的开头删除并返回元素。
int frontElement = queue.poll(); // 从队列开头删除并返回元素
4. 查看队列的头元素: 使用peek方法查看但不移除队列的头元素。
int frontElement = queue.peek(); // 查看队列的头元素
5. 检查队列是否为空: 使用isEmpty方法检查队列是否为空。
boolean isEmpty = queue.isEmpty(); // 检查队列是否为空
6. 获取队列的大小: 使用size方法获取队列中元素的数量。
int size = queue.size(); // 获取队列的大小
7. 遍历队列元素: 可以使用增强的for循环来遍历队列元素。
for (Integer element : queue) {
// 处理每个元素
}
8. 注意事项:
ArrayDeque是非线程安全的,适用于单线程环境。- 在出队前检查队列是否为空,以避免出现队列空异常。
这些是使用ArrayDeque实现队列的基本操作和注意事项。ArrayDeque通常用于需要高效的插入和删除操作的队列场景。如果要实现栈的操作,可以使用ArrayDeque的push和pop方法。
1.4 LinkedList vs. ArrayDeque
在Java中,LinkedList和ArrayDeque都可以用来实现队列,但它们有一些区别:
-
底层数据结构:
LinkedList是一个双向链表,每个元素都包含了指向前一个元素和后一个元素的引用。ArrayDeque是一个基于动态数组的双端队列,底层使用数组实现。
-
性能特点:
LinkedList在插入和删除操作(包括队列的入队和出队)上通常比较慢,因为它需要调整链表中的引用。ArrayDeque在插入和删除操作上通常更快,因为它使用数组,可以在常数时间内执行这些操作。
-
空间复杂度:
LinkedList通常需要更多的内存空间来存储链表节点对象和相关引用。ArrayDeque通常需要较少的内存,因为它只需要存储数组元素和一些额外的控制信息。
-
随机访问:
LinkedList不支持高效的随机访问,因为要遍历链表以查找特定位置的元素。ArrayDeque支持高效的随机访问,因为它基于数组,可以通过索引直接访问元素。
-
线程安全性:
- 无论是
LinkedList还是ArrayDeque,它们都不是线程安全的,如果在多线程环境中使用,需要自行考虑同步或使用线程安全的队列实现。
- 无论是
-
功能特性:
LinkedList可以用于双向队列的操作,因为它支持在队列头部和尾部进行高效的插入和删除。ArrayDeque是一个专门设计用于双端队列的数据结构,因此通常在队列操作上更为高效。
综上所述,ArrayDeque通常更适合用作队列的实现,因为它在插入和删除操作上通常更快,而LinkedList适合那些需要双向链表特性的场景。选择哪个取决于你的具体需求和性能要求。如果你只需要一个普通队列,ArrayDeque可能是更好的选择。
2、232.用栈实现队列
2.1 思路
可以类比成一堆书,首先将所有的书按照一定的顺序依次放入位置A,接着再将这些从位置A取出的书依次放入位置B。此时,第一次放入A位置的书一定会被第一个从位置B取出,这符合了队列的先进先出原则。
- 栈:后进先出;队列:先进先出
- stack1 负责出队列,stack2 负责进队列
- 比如1->2->3 依次加入到stack2 中[1, 2, 3]
- 当需要出站时,stack2 的所有元素加入到 stack1 [3, 2, 1]
- stack1 出站依次为1->2->3
2.2 代码
class MyQueue {
private LinkedList<Integer> stack1;
private LinkedList<Integer> stack2;
public MyQueue() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
public void push(int x) {
stack2.push(x);
}
public int pop() {
transform();
return stack1.pop();
}
public int peek() {
transform();
return stack1.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
private void transform() {
if(!stack1.isEmpty()) {
return;
}
while(!stack2.isEmpty()) {
stack1.push(stack2.pop());
}
}
}
3、225.用队列实现栈
3.1 使用两个队列
思路
可以将其类比为排队的场景。当有新人要加入队列时,首先将队列A中的人加入到队列B,然后将新人加入队列A中,接着再将队列B中的人加入队列A。这样,新加入的人会第一个出队列,依此类推,符合了栈的后进先出原则。
-
栈:后进先出;队列:先进先出
-
queue1 作为和栈中保持一样元素的队列,queue2 作为辅助队列
-
比如1->2->3 依次加入到queue1 时,使用辅助队列 queue2,使得queue1 的顺序为 [3, 2, 1]
-
具体操作步骤如下:
- 先将queue1 的元素加入到queue2
- 接着加入该元素
- 最后将queue2 的元素加入queue1
代码
class MyStack {
private LinkedList<Integer> queue1;
private LinkedList<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
// 先将queue1 的元素加入到queue2
while(!queue1.isEmpty()) {
queue2.offer(queue1.poll());
}
// 接着加入该元素
queue1.offer(x);
// 最后将queue2 的元素加入queue1
while(!queue2.isEmpty()) {
queue1.offer(queue2.poll());
}
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
3.2 使用一个队列
思路
这情景可以类比排队,当有新人要加入队列时,直接将新人加入队列,并将之前在队列的人再次加入队列。不管何时加入,新加入的人总是会第一个出队,遵循栈的后进先出原则。
- 使用一个队列,当加入新元素时,先加入到队列,然后队列前面的元素(size-1)重新加入
代码
class MyStack {
private LinkedList<Integer> queue1;
public MyStack() {
queue1 = new LinkedList<>();
}
public void push(int x) {
queue1.offer(x);
int size = queue1.size();
while(--size > 0) {
queue1.offer(queue1.poll());
}
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}