前言:开启栈与队列篇,之前学习过一段时间栈,需要再复习一下。
栈和队列的底层实现
1. 栈(Stack)
栈是一种后进先出(LIFO,Last In First Out)的数据结构,通常支持以下操作:
- push:向栈顶添加一个元素。
- pop:移除栈顶元素并返回它。
- peek:查看栈顶元素但不移除它。
Java 标准库中的栈实现主要依赖于 java.util.Stack 类,这是一个基于 Vector 的实现。Vector 是一个线程安全的动态数组。
Stack 类的特点
- 线程安全:由于 Vector 是线程安全的,所以 Stack 也是线程安全的。
- 性能:由于同步操作,Stack 在多线程环境下的性能较差。
- 扩展性:Stack 可以自动扩展容量。
2.队列(Queue)
队列是一种先进先出(FIFO,First In First Out)的数据结构,通常支持以下操作:
- enqueue:向队尾添加一个元素。
- dequeue:移除队头元素并返回它。
- peek:查看队头元素但不移除它。
Java 标准库提供了多种队列实现,主要包括:
- LinkedList:基于双向链表的队列实现,支持高效的元素插入和删除操作。
- ArrayDeque:基于固定大小数组的双端队列实现,支持高效的头部和尾部操作。
- PriorityQueue:基于优先级队列的实现,通常使用二叉堆来实现,是一种基于最小堆(默认)或最大堆(通过自定义比较器)的优先级队列实现。
ArrayDeque 和 LinkedList 是 Java 中常用的两种队列实现方式,下面进行详细介绍:
1) LinkedList
LinkedList 是一个基于双向链表实现的集合类,它同时实现了 List 和 Deque 接口。这意味着 LinkedList 不仅可以用作列表,还可以用作队列或栈。
特点
- 双向链表:LinkedList 的每个节点包含三个部分:一个数据字段、一个指向列表中前一个节点的引用和一个指向列表中下一个节点的引用。
- 插入和删除操作:由于 LinkedList 是双向链表,所以在头部或尾部添加和删除元素非常高效,通常的时间复杂度为 O(1)。
- 随机访问:虽然 LinkedList 支持随机访问,但由于它是链表而不是数组,所以随机访问的时间复杂度为 O(n),因为需要从头开始遍历链表。
- 线程安全性:LinkedList 的方法是非线程安全的,如果多个线程同时访问一个 LinkedList 实例,则必须自己保证同步。
常用方法
队列操作:
- LinkedList 实现了 Queue 接口,提供了 add, remove, element, offer, poll, peek 等方法。
- 双端队列操作:LinkedList 实现了 Deque 接口,提供了 addFirst, addLast, removeFirst, removeLast, getFirst, getLast, peekFirst, peekLast 等方法。
- 列表操作:LinkedList 实现了 List 接口,提供了 add, remove, get, set 等方法。
2) ArrayDeque
ArrayDeque 是一个基于循环数组实现的双端队列。它是一个高性能的队列实现,适用于需要频繁进行头部和尾部操作的场景。
特点
- 循环数组:ArrayDeque 使用一个固定大小的数组,并通过指针来记录当前的头部和尾部位置。
- 插入和删除操作:ArrayDeque 在头部或尾部添加和删除元素非常高效,通常的时间复杂度为 O(1)。
- 扩容机制:当数组空间不足时,ArrayDeque 会创建一个新的更大的数组,并将现有元素复制过去。
- 线程安全性:ArrayDeque 的方法是非线程安全的,如果多个线程同时访问一个 ArrayDeque 实例,则必须自己保证同步。
常用方法
双端队列操作:ArrayDeque 实现了 Deque 接口,提供了 addFirst, addLast, removeFirst, removeLast, getFirst, getLast, peekFirst, peekLast 等方法。
选择建议
- LinkedList:适用于需要频繁进行头部或尾部插入和删除操作的场景,以及需要同时作为队列和栈使用的场景。如果随机访问的需求较少,可以选择 LinkedList。
- ArrayDeque:适用于需要高效进行头部和尾部操作的场景。如果只需要双端队列的功能,并且对随机访问的需求不高,可以选择 ArrayDeque。
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() {
if(stackOut.isEmpty()){
while(!stackIn.isEmpty()){
int temp=stackIn.pop();
stackOut.push(temp);
}
}
return stackOut.pop();
}
public int peek() {
if(stackOut.isEmpty()){
while(!stackIn.isEmpty()){
int temp=stackIn.pop();
stackOut.push(temp);
}
}
return stackOut.peek();
}
public boolean empty() {
return (stackIn.isEmpty() && stackOut.empty());
}
}
注意:这里peek()的实现最好直接复用pop(),避免形成代码的冗余,下面将输入栈的元素全部导入输出栈这一过程抽象成了一个新的函数可以直接调用。
225. 用队列实现栈
思路:考虑使用一个队列来实现,在输入时如果前面存在元素,就将元素依次移到队列后面。每次pop即为最后加入的元素。
代码如下:
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue=new LinkedList<>();
}
public void push(int x) {
queue.offer(x);
int size=queue.size();
while(size-->1){
queue.offer(queue.poll());
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
20. 有效的括号
前言:这个题目之前做过,栈的应用还是比较有趣的。
思路:遇到左括号’(‘,’{‘,’['压入栈,遇到右括号弹出顶端元素,如果能够匹配说明字符串有效。此外,如果栈中剩余元素,说明左括号过多,return false;如果右括号匹配时栈为空,说明右括号多余,return false。
代码如下:
class Solution {
public boolean isValid(String s) {
Stack<Character> stack =new Stack<>();
for(int i=0;i<s.length();i++){
char ch=s.charAt(i);
if(ch=='(' || ch=='{'|| ch=='['){
stack.push(ch);
}else if(ch==')'){
if(stack.peek()!='('|| stack.isEmpty())
return false;
else stack.pop();
}else if(ch=='}'){
if(stack.peek()!='{' || stack.isEmpty())
return false;
else stack.pop();
}else if(ch==']'){
if(stack.peek()!='[' || stack.isEmpty())
return false;
else stack.pop();
}
}
return stack.isEmpty();
}
}
思考:当前代码考虑了所有情况,相对复杂冗余,遇到左括号时将对应的右括号压入栈,会简单很多。
代码优化如下:
class Solution {
public boolean isValid(String s) {
Stack<Character> stack =new Stack<>();
for(int i=0;i<s.length();i++){
char ch=s.charAt(i);
if(ch=='('){
stack.push(')');
}else if(ch=='{'){
stack.push('}');
}else if(ch=='['){
stack.push(']');
}else if(stack.isEmpty()||ch!=stack.peek()){
return false;
}else {
stack.pop();
}
}
return stack.isEmpty();
}
}
1047. 删除字符串中的所有相邻重复项
思路:这个题利用栈操作还是比较简单的,在栈不为空的情况下,只需要检查栈顶元素与当前元素是否相同即可,相同则pop,不相同则压入栈中。
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack=new Stack<>();
for(int i=0;i<s.length();i++){
if(stack.isEmpty() || stack.peek()!=s.charAt(i)){
stack.push(s.charAt(i));
}else{
stack.pop();
}
}
char[] ns=new char [stack.size()];
int len=stack.size()-1;
while(!stack.isEmpty()) {
ns[len--]=stack.pop();
}
return new String(ns);
}
}
思考:该题还可以新建一个字符串作为栈或者使用双指针法。