刷了两题栈和队列的力扣后发现,难点不在算法,但是对Java中对栈和队列的定义好混乱,会涉及到Stack/Queue/Deque/ArrayDeque/LinkedList,我们到底要怎么选,以及他们分别有什么函数,add/offer/push/pop/poll/peek这几个函数到底该什么时候用。对于第一次用Java写栈和队列的新手来说,确实很容易搞糊涂。所以我参考的网上的各种资料和jdk11开发者文档对这些类和接口的说明,把他们一起整理一下。
下面我就会分别介绍Stack/Queue/Deque/ArrayDeque/LinkedList这个五个的作用和常用函数,以及他们直接的关系,最后会说明力扣里面我们要如何选择和使用。
说明:下面的介绍轻底层原理,简单介绍完理论之后,更多的是强调如何使用。
目录
一.Stack
1.简单说明
Stack是一个普通类,继承了Vector接口,增加了五个操作,push、pop、
peek、empty和search方法。
2.Stack的定义
栈的构造方法只有一个,如下图。
因此,如果我们要构造一个Stack对象,要怎么写呢?
下面我构造了一个空栈,栈内的元素分别是是Integer和Character包装类。其他类型的栈格式是一样的。
//力扣里上下两种都能用
Stack<Integer> stack = new Stack<>();
Stack<Character> stack = new Stack<>();
//IJ里面只能用下面这种,<>内部要写东西
Stack<Integer> stack = new Stack<Integer>();
Stack<Character> stack = new Stack<Character>();
我也在我的IJ里面试了,大家可以看图。<>里面不写元素类型,会直接报错。
3.Stack的函数调用
Stack的全部方法如下,只有5个。而我们在刷力扣是常用的是前4个。
只要会下面几个函数,在力扣上用Stack实现栈就没问题了。
(1)empty():判断stack是否为空,如果为空返回true,不为空返回false。
isEmpty():和empty功能一样,且这两个判空的函数在力扣里都能编译通过,大家可以随便选来用。
那为什么Stack类里面isEmpty方法但是我们可以使用呢,因此isEmpty方法是封装在Collection里的,而Stack类是Collection的子类,所有可以用上层父类的方法。(Collection->List->Vector->Stack这个是由父到子的继承的关系)
(2)peek():获取栈顶的元素
(3)pop():获取栈顶的元素,并将其删除
(4)push(x):在栈顶添加元素x
(5)size():获取当前栈的长度(是Collection父类中继承过来的)
4.使用举例
下面的代码是力扣232.用栈实现队列的题目,包括的栈的定义,push、pop以及判空操作。这里的判空用empty()和isEmpty()都可以。
class MyQueue {
Stack<Integer> in;
Stack<Integer> out;
public MyQueue() {
in = new Stack<>();
out = new Stack<>();
}
public void push(int x) {
in.push(x); //输入栈模拟队列的输入
}
public int pop() {
if(out.isEmpty()){ //输出栈模拟队列的输出
while(!in.isEmpty()){ //如果输入栈为空,把输入栈元素全部移入输出栈
out.push(in.pop());
}
}
int result = out.pop();
return result;
}
public int peek() {
int result = this.pop(); //函数复用,调用上面的pop
out.push(result); //把pop的元素push回去
return result;
}
public boolean empty() {
return in.isEmpty() && out.isEmpty();
}
}
二.Queue
1.简单说明
Queue是一个接口,继承了Collection接口。
2.Queue的定义
因为Queue是一个接口,所有没有构造方法。因此在定义的时候,右边new的对象必须是Queue接口的实现类,这里有两个类LinkedList和ArrayDeque可以选择。下面我给了Queue接口定义的示例代码。
//这一种只能在力扣里用,IJ里会报错
Queue<Integer> queue = new LinkedList<>();
Queue<Integer> queue = new ArrayDeque<>();
//这一种在力扣和IJ里都能用
Queue<Integer> queue = new LinkedList<Integer>();
Queue<Integer> queue = new ArrayDeque<Integer>();
先说说结论,如果要在力扣上通过,LinkedList和ArrayDeque其实都能用,性能上也没什么差距,大家不用纠结。如果非要选,
ArrayDeque更好
,因为它在性能和内存使用方面通常更优越,而且力扣上队列的题基本都是在队列的头尾操作。下面简单介绍一下两者的区别。
(1)LinkedList和ArrayDeque是什么?
他俩都是实现了Queue接口的实现类。
(2) LinkedList和ArrayDeque有什么区别?
LinkedList
是一个基于链表的数据结构,它使用双向链表来存储元素。每个元素在内存中不是连续存储的,而是通过指针(或引用)连接在一起。
ArrayDeque
是一个基于数组的双端队列,它使用循环数组来存储元素。这意味着当达到数组的末尾时,它会回到数组的开头继续存储。
其实简单来说,LinkedList底层用的是链表实现了双向队列,ArrayDeque的底层用的是数组实现了双向队列,原理一样,只是实现时用的数据结构不一样罢了。
(3)
LinkedList和ArrayDeque我怎么选择使用?
ArrayDeque优点就是内存少,主要针对在队列的头部和尾部操作。 LinkedList的优点就是方便在任意位置插入元素。
3.Queue的函数调用
Queue的全部方法如下,我们在力扣上常用的其实也只有add、offer、peek和poll。
只要会下面几个函数,在力扣上用Queue实现队列就没问题了。
(1)poll():删除并获取队头元素
(2)peek():获取队尾元素
(3)add():在队尾添加元素
offer():在队尾添加元素
add和offer都能在队列尾部添加元素,那两者的区别是什么呢?
add :继承
Collection
接口。当队列已满时,add
方法会抛出IllegalStateException。
offer :是
Queue
接口特有的。当队列已满时,offer
方法不会抛出异常,会返回false。add和offer如何选择?
其实在力扣里用哪个都行。
但在日常实际开发中,如果希望在队列满时立即抛出异常,且代码能够处理这种异常情况,那么可以使用
add
方法。如果你不希望因为队列满而抛出异常,而是希望检查添加操作是否成功,并据此做出进一步的逻辑判断或处理,那么应该使用offer
方法。在实际应用中,
offer
方法通常更受欢迎,因为它提供了更大的灵活性,允许调用者在不中断程序执行的情况下处理添加操作的失败情况。
(4)isEmpty():判断队列是否为,是空则返回true(是Collection父类中继承过来的)
(5)size():获取当前队列的长度(是Collection父类中继承过来的)
4.使用举例
下面的代码是力扣225.用队列实现栈的题目,包括的队列的定义,add或offer、push、poll、size以及判空操作,这里的判空用empty()和isEmpty()都可以。
class MyStack {
Queue<Integer> que;
public MyStack() {
que = new LinkedList<>(); //双向链表
}
public void push(int x) {
que.add(x); //add添加到列表末尾,模拟栈的输入
}
public int pop() {
tackle(); //如果队列元素大于1,要把前面的元素依次移到队尾
int result = que.poll(); //poll用于删除队头元素
return result;
}
public int top() {
//以123为例,要想获取队尾元素3,先移动成312,把队尾元素3给poll,变成12,获取到3
//再把3重新add到队尾,变成123,恢复初始状态
tackle(); //如果队列元素大于1,要把前面的元素依次移到队尾
int result = que.poll(); //poll删除队头元素,并返回队头元素
que.add(result); //把删除的元素重新加到队尾,
return result;
}
public boolean empty() {
return que.isEmpty();
}
//如果队列元素大于1,要把前面的元素依次移到队尾
public void tackle(){
int size = que.size();
while(size-- > 1){
que.add(que.poll());
}
}
}
三.Deque
1.简单说明
Deque是一个接口,继承的了Queue接口,Deque定义了访问双端队列两端元素的方法。且Deque接口可以帮助我们同时实现栈或者队列。简单来说,如果我们要实现一个栈or一个队列,Deque是功能最强大的。
2.Deque的定义
和Queue类似,Deque是继承了Queue的接口,因此在定义时,右边必须是实现类。
//这一种只能在力扣里用,IJ里会报错
Deque<Integer> queue = new LinkedList<>();
Deque<Integer> queue = new ArrayDeque<>();
//这一种在力扣和IJ里都能用
Deque<Integer> queue = new LinkedList<Integer>();
Deque<Integer> queue = new ArrayDeque<Integer>();
Deque接口提供的方法很多,那我们怎么用呢?
3.Deque中关于栈的函数
(1)如何在栈顶添加元素
push:用于在栈顶添加元素。(Deque额外加的)
记住push就行,addFisrt和offerFisrt只是实现功能一样,但是没必要用,容易搞乱
addFisrt:可以在栈顶添加元素(Deque额外加的)
offerFisrt:可以在栈顶添加元素(Deque额外加的)
(2)如何在栈顶弹出元素
pop:用于在栈顶弹出元素。(Deque额外加的)
记住pop就行,pollFisrt和removeFirst只是实现功能一样,但是没必要用,容易搞乱
pollFisrt:用于在栈顶弹出元素。(Deque额外加的)
removeFirst:可以获取并删除在队头的元素(Deque额外加的)
(3)如何获取队列头部的元素
peek:可以获取栈顶的元素(Queue中继承来的)
peekFirst:可以获取栈顶的元素(Deque额外加的)
getFirst:可以获取栈顶的元素(Deque额外加的)
记住peek就行,peekFirst和getFirst只是实现功能一样,但是没必要用,容易搞乱
4.Deque中关于队列的函数
(1)如何在队列尾部添加元素
offer:可以在队列尾部添加元素(Queue中继承来的)
add:可以在队列尾部添加元素(Queue中继承来的)
offerLast:可以在队列尾部添加元素(Deque额外加的)
addLast:可以在队列尾部添加元素(Deque额外加的)
记住offer就行,add、addLast和offerLast只是实现功能一样,但是没必要用,容易搞乱
(2)如何在队列头部弹出元素
poll:可以获取并删除在队头的元素(Queue中继承来的)
记住poll就行,pollFisrt、remove和removeFirst只是实现功能一样,但是没必要用,容易搞乱
pollFisrt:可以获取并删除在队头的元素(Deque额外加的)
remove:可以获取并删除在队头的元素(Deque额外加的)
removeFirst:可以获取并删除在队头的元素(Deque额外加的)
poll
和remove
的主要区别?
poll删除
队列的首个元素,即使队列为空也不会抛出异常。(类似于offer不抛异常)
remove
队列的首个元素,如果队列为空则会抛出异常。(类似于add抛出异常)
(3)如何获取队列头部的元素
peek:可以获取队列头部的元素(Queue中继承来的)
记住peek就行,peekFirst、getFirst只是实现功能一样,但是没必要用,容易搞乱
peekFirst:可以获取在队头的元素(Deque额外加的)
getFirst:可以获取在队头的元素(Deque额外加的)
peekFirst
和getFirst
的主要区别?
peek
获取队列的首个元素,即使队列为空也不会抛出异常。(类似于offer不抛异常)
get
获取队列的首个元素,如果队列为空则会抛出异常。(类似于add抛出异常)
四.总结
上面说的有点细了,特别是Deque的方法有点太多,大家可能已经晕了,但是为了理论知识保证,也得给大家写全。下面我就主打一个使用方便,再整理一下子,大家直接记住就行。
1.Stack栈的使用
pop压栈+push进栈+peek获取栈顶。就这三个特殊的记住就行。
Stack本类 | Collection继承 | |
压栈 | push() | - |
出栈 | pop() | - |
获取栈顶元素 | peek() | - |
判空 | empty() | isEmpty() |
长度 | - | size() |
2.Queue队列的使用
poll出队+offer进队+peek获取队头。就这三个特殊的记住就行。
Queue本类 | Collection继承类 | ||
返回null | 返回异常 | - | |
队头出队 | poll() | remove() | - |
peek() | element() | ||
队头获取元素 | - | ||
队尾入队 | offer() | add() | - |
判空 | - | isEmpty() | |
长度 | - | size() |
3.Deque双端队列的使用
Deque是双端队列,只是它能够同时拥有栈和队列的方法。其实Deque实现队列,用的 “poll出队+offer进队+peek获取队头”就是把Queue的继承过来了。而用Deque实现栈,只是增加了pop压栈+push进栈功能,且复用了peek获取队头等价于获取栈顶而已。
下面的表格看着很复杂方法很多,一方面是因为很多功能相同的操作,分成了返回值null和返回异常两种函数,而返回值异常的这一套函数我们可以不用,主打用返回值为null的。另一方面是,Deque是双端队列,因此增加了很多xxxFirst和xxxLast函数,用于单独对队尾和队头进行操作,而我们实现栈和队列时,完全可以不用这些xxxFirst和xxxLast函数。
Deque本类 | Queue继承类 | Collection继承类 | |||||
返回null | 返回异常 | 返回null | 返回异常 | ||||
栈需要的 | 入栈(队头添加) | push() offerFirst() | addFirst() | - | - | ||
出栈(队头删除) | pop() pollFirst() | removeFirst() | poll() | remove() | - | ||
获取栈顶(队头获取) | peekFirst() | getFirst() | peek() | element() | - | ||
队列需要的 | 队头出队 | pop() pollFirst() | removeFirst() | poll() | remove() | - | |
队头获取元素 | peekFirst() | getFirst() | peek() | element() | - | ||
队尾入队 | offer() | add() | offerLast() | addLast() | |||
判空 | - | - | - | - | isEmpty() | ||
长度 | - | - | - | - | size() |
终于结束啦!其实大家只要记住Stack和Queue的使用,在Deque里面一样可以用的,Deque只是把Stack和Queue的功能全包括进来了,还自己加了一堆first和last罢了。上面的全部内容会加深你对这三个对象的理解,以及他们直接的继承实现关系。
但是如果你只想会用,就只要理解和记忆下面6个方法+知道如何定义一个栈or队列or双端队列就行啦。
如果要用Deque实现栈:pop压栈+push进栈+peek获取栈顶。就这三个的记住就行。
如果用Deque实现队列:poll出队+offer进队+peek获取队头。就这三个的记住就行。