在做题之前说明
ArrayDeque
与Stack
在Java中,推荐使用ArrayDeque
而不是Stack
来实现栈结构,主要基于以下几点原因:
-
性能:
ArrayDeque
是基于数组实现的,而Stack
是基于Vector
实现的。Vector
在所有的方法上都加了同步处理,以保证线程安全,这导致了Stack
在单线程环境中的性能较低。相比之下,ArrayDeque
是非线程安全的,没有同步开销,因此其性能更优。 -
设计:
ArrayDeque
设计上就是为了提供双端队列(deque)的功能,而栈(stack)只是双端队列的一种特殊用法,即只在一端(队尾)添加或删除元素。ArrayDeque
内部优化了这种使用方式,使得作为栈使用时效率更高。 -
功能性:虽然
Stack
提供了栈的基本操作(如push
,pop
,peek
等),但ArrayDeque
作为Deque
接口的实现,除了可以用作栈,还可以用作队列,提供了更多的灵活性。 -
空值处理:
ArrayDeque
不允许插入null
值,这有助于避免潜在的空指针异常。而Stack
允许在栈中插入null
值,这可能导致运行时错误。 -
API更新:随着Java集合框架的发展,
Stack
被视为遗留类,其功能在Deque
接口及其实现类(如ArrayDeque
)中得到了更好的支持和实现。因此,从最佳实践的角度来看,建议使用新的集合框架API。
ArrayDeque
和 Deque<Integer>
Deque<Integer>
是一个双端队列的泛型接口,而 ArrayDeque
是 Deque
接口的一个具体实现,它使用数组来存储元素。
当声明一个变量为
Deque<Integer>
类型时,在指定该变量将持有实现了Deque
接口的对象。这样做的好处是,可以轻松地更换不同的实现,比如ArrayDeque
、LinkedList
或者其他任何Deque
的实现,而不会影响到使用该变量的代码。这提供了更高的灵活性和可维护性。
Deque<Integer> deque = new ArrayDeque<>();
// 或者
Deque<Integer> deque = new LinkedList<>();
在上面的代码中,
deque
可以指向任何Deque
接口的实现。如果决定改变实现,只需要更改构造函数调用的类即可。另一方面,当直接使用
ArrayDeque
时,是在指定使用这个特定的双端队列实现:在这种情况下,arrayDeque
变量只能引用ArrayDeque
的实例,不能引用其他实现了Deque
接口的类。
ArrayDeque<Integer> arrayDeque = new ArrayDeque<>();
使用场景
-
当需要双端队列的功能并且关注性能时,通常会选择
ArrayDeque
。由于它是基于数组实现的,它在随机访问和栈(后进先出)操作上表现得非常好。 -
当需要双端队列的功能并且想要保持最大的通用性时,可以声明一个
Deque<Integer>
类型的变量。这样,你可以在不改变使用代码的情况下,自由地切换不同的实现,比如出于测试目的或者为了适应不同的运行时环境。 -
当需要一个既可以作为队列又可以作为双端队列使用的数据结构时,使用
LinkedList
实现的Deque<Integer>
可能更合适,因为LinkedList
同时实现了List
和Deque
接口。 - ArrayDeque会比LinkedList在除了删除元素这一点外会快一点
算法题
Leetcode 20. 有效的括号
题目链接:20. 有效的括号
大佬视频讲解:有效的括号视频讲解
个人思路
这种括号匹配肯定是用栈结构; 可以先入右括号,然后只需要比较当前元素和栈顶相不相等就可以了!
解法
当遇到一个左括号时,将这个左括号放入栈顶。当遇到一个右括号时,取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s无效,返回 False。遍历结束后,如果栈中没有左括号,说明将字符串中的所有左括号闭合,返回 True,否则返回 False 。
为了快速判断括号的类型,可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。
class Solution {
public boolean isValid(String s) {
int len= s.length();
if(len%2==1){ return false;}//有效字符串的长度一定为偶数,若为奇数则直接返回false
Map<Character , Character > pairs =new HashMap<Character,Character>(){{
put(')','(');//键为右括号,值为相同类型的左括号
put('}','{');
put(']','[');
}};
Deque<Character> stack=new LinkedList<Character>();
//遇到一个右括号时,将一个相同类型的左括号闭合
for(int i=0;i<len;i++){
char ch=s.charAt(i);//遍历字符串
if(pairs.containsKey(ch)){//碰到右括号时
//栈为空或者没有匹配的左括号 返回false
if(stack.isEmpty() || stack.peek() !=pairs.get(ch)){
return false;
}
stack.pop();//否则闭环
}else { stack.push(ch);}//左括号入栈
}
return stack.isEmpty();//栈若为空说明全部括号都闭合了
}
}
时间复杂度:O(n);(遍历字符串)
空间复杂度:O(n);(使用到一个栈和一个常量字符集6种括号)
Leetcode 1047. 删除字符串中的所有相邻重复项
题目链接:1047. 删除字符串中的所有相邻重复项
大佬视频讲解:删除字符串中的所有相邻重复项视频讲解
个人思路
就是消消乐,上上面这题原理一样,栈结构非常合适,进一个元素,判断一个元素,如果能消除则弹出,否则放入
解法
注意:因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。
class Solution {
public String removeDuplicates(String s) {
ArrayDeque<Character> stack=new ArrayDeque<>();
char ch;
for(int i=0;i<s.length();i++){//遍历字符串s
ch=s.charAt(i);
if(stack.isEmpty() || ch!=stack.peek()){//若栈为空或者栈中元素与ch不同则放入
stack.push(ch);
}else {
stack.pop();
}
}
String result="";//结果字符串
while(!stack.isEmpty()){//因为是栈,所以需要倒序弹出
result=stack.pop()+ result;
}
return result;
}
}
时间复杂度:O(n);(一个遍历,一个弹出结果字符串 2n)
空间复杂度:O(n);(使用了数组队列)
Leetcode 150. 逆波兰表达式求值
题目链接:150. 逆波兰表达式求值
大佬视频讲解:逆波兰表达式求值视频讲解
个人思路
也是消消乐,栈结构搞定它;遍历判断如果是运算符号则弹出两个数据做运算后再放入,如果是数字则直接放入。
解法
逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 7 + 7 ) * ( 8+ 8 ) 。该算式的逆波兰表达式写法为 ( ( 7 7 + ) ( 8 8 + ) * ) 。
逆波兰表达式主要有以下两个优点:
去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中
用逆波兰表达式后计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的。
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack=new LinkedList();
for(String s:tokens){
if(s.equals("+")){
stack.push(stack.pop()+stack.pop());
}else if(s.equals("-")){
stack.push(-stack.pop()+stack.pop());//因为是栈,顺序是反的,减法放前面
}
else if(s.equals("*")){
stack.push(stack.pop()*stack.pop());
}
else if(s.equals("/")){//要先弹出,再t2除t1,不然运算顺序不正确
int t1=stack.pop();
int t2=stack.pop();
stack.push(t2 / t1);
}
else {
stack.push(Integer.valueOf(s));
}
}
return stack.pop();
}
}
时间复杂度:O(n);(遍历字符数组)
空间复杂度:O(n);(栈空间)
以上是个人的思考反思与总结,若只想根据系列题刷,参考卡哥的网址代码随想录算法官网