1、链表实现栈
上篇文章,我们讲到栈是用数组实现的,它入栈和出栈的时间复杂度都是O(1),那我如果要使用链表来实现栈应该怎么做呢?
那我们先假设使用单链表能否实现栈:
尾插法:
入栈:时间复杂度:O(n)(找到链表的倒数第一个元素)。
出栈:时间复杂度:O(n)(找到链表的倒数第二个元素)。
那么此时,有聪明的同学可能会想到:那我如果给它再加一个尾巴节点时间复杂度又是多少呢? 入栈:因为有last节点,时间复杂度变为O(1)。
出栈:时间复杂度仍为O(n),因为是单链表last只能找到last后面的节点无法找到它前面的节点。
头插法:
入栈:时间复杂度O(1)。
出栈:由于栈先进后出的特性,时间复杂度也为O(1)。
因此,在使用单链表实现栈时一般采用头插法。
假设双链表实现栈:
如果使用双链表实现栈:那么无论从那边出入栈时间复杂度都为O(1)。
如下代码所示:就是用链表创建了一个名为stack的对象,它可以正常使用push(),pop(),isEmpty()。
LinkedList<Integer> stack = new LinkedList<>();
System.out.println(stack.isEmpty());
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.peek());
点进LinkedList中,可以看见LinkedList的底层是实现了这些方法的。
2、队列
2.1 概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)的特性。入队列:进行插入操作的一段称为队尾。出队列:进行删除操作的一端称为队头。
我们也可以将链表当作一个队列,入队时使用尾插法,出队时删除头节点即可。 代码如下:
//普通队列
Queue<Integer> queue1 = new LinkedList<>();
//入队
queue1.offer(1);
queue1.offer(2);
queue1.offer(3);
queue1.add(4);
System.out.println(queue1);
//出队
System.out.println(queue1.poll());//1
//获取队头元素
System.out.println(queue1.peek());//2
2.2 队列的使用
在Java中, Queue是一个接口,而链表实现了这个接口
那细心的同学就会发现Queue下面还有个Deque,那这个Deque是什么呢?
Deque是双端队列,能够在同一端进出元素。
常用方法有这些: 注意:因为Queue只是一个接口,实例化的时候,必须要实例化一个LinkedList的对象,因为是LinkedList实现了Queue接口 。
在Queue的结构中,不止存在offer( ),poll( )和peek( )还存在add( ),remove( )和element方法,
在Java的Queue接口中,add/offer、remove/poll、element/peek 是两组功能类似但行为不同的方法。他们的核心区别在于 对队列边界条件(满或空)的处理方式 。
可以看到使用add()方法在队列满时抛出了异常。
在队列未满时,offer( )返回true,队列满了则返回false,不报异常。
在队列为空时:poll( )方法返回空
remove( )方法报异常
在队列为空时:peek( )方法返回空
element( )方法抛异常
3、队列的模拟实现
我们使用双向链表来实现一个队列:
//双向链表的节点
static class ListNode {
private int val;
private ListNode prev;
private ListNode next;
public ListNode(int val) {
this.val = val;
}
}
private ListNode front;//队头
private ListNode rear;//队尾
private int usedSize;
3.1 offer( )方法
使用头插法,进行入队。
//在头插入元素
public void offer(int x){
ListNode listNode = new ListNode(x);
if(front == null){//第一次插入
front = rear = listNode;
}else{
listNode.next = front;
front.prev = listNode;
front = listNode;
}
usedSize++;
}
3.2 poll( )方法
在上面的offer( )方法中我们使用头插法进行插入,根据先进先出的原则,应当在尾巴节点进行删除。
//出队列 相当于删除尾巴结点
public int poll(){
if(front == null){
throw new NullElementException("没有元素可被删除");
}
if(front == rear){
int x = front.val;
front = null;
rear = null;
usedSize--;
return x;
}
int ret = rear.val;
rear = rear.prev;
rear.next = null;
usedSize--;
return ret;
}
3.3 peek( )方法
public int peek(){
if(front == null){
throw new NullElementException("没有元素可被删除");
}
return rear.val;
}
3.4 isEmpty( )
public boolean isEmpty(){
return usedSize == 0;
}
4、双端队列
双端队列:指允许两端都可以进行入队和出队操作的队列,deque是"double ended queue"的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
注意:Queue和Queue都是一个接口,使用时必须创建LinkList对象。
可以看到Stack也实现了Deque接口,所以有了如下代码:
Deque<Integer> stack = new LinkedList<>();
stack.push(1);
stack.offer(2);
此外,双端队列可以有线性数组实现和链式实现 :
Deque<Integer> deque1 = new ArrayDeque<>();//双端队列的数组实现
Deque<Integer> deque2 = new LinkedList<>();//双端队列的链式实现
5、 面试题
5.1 622. 设计循环队列 - 力扣(LeetCode)
那么我们要如何实现这个队列呢?首先,我们知道,通过数组也可以实现队列:
队尾进,队头出,插入完后rear++。入队完成后: 出队的时候,只需要让front++即可,但是这么做会浪费出队元素的空间,那个空间无法再进行入队。
这时候我们想到可以让数组循环起来,这样就不会浪费那个空间,那如何让数组循环起来呢?很简单嘛,就像我们玩软尺一样,把数组卷起来不就行了!!!
front表示数组的第一个元素的下标,rear表示当前可插入元素的下标。
这时候如果要元素出队,只需让front往后走就行了,并不会影响后面的元素入队。
由上面所述,我们知道了循环队列通过数组实现,但实现循环队列还有最重要的一点:判断队列是否已满。
判断队列是否已满可以采用3种方法:
1、定义一个Usesized,入队Usesized++,出队Usesized--,当Usesized == 数组长度时,说明数组已满。
2、定义一个flg(Boolean),当第二次rear == front时,说明队列已满,此时flg = true。
3、浪费一个空间来进行区分,当rear的下一个就是front说明队列已满。效果如图所示:
那现在问题又来了,这里数组被卷起来了,如果想让rear往后走还是rear++吗?显然不对,通过上图,7的后面的下标是0,而你++后的下标应该是8。所以,这里让rear往后走应该是(rear + 1)% length(数组长度) ,front同理:(front + 1)% length。
综上所述很容易得到下述代码:
//通过数组实现循环队列
//判定:循环队列是否已满:1、定义usesized==len
//2、标记
//3、浪费一个空间进行区分
private int[] elem;
private int front;//队头下标
private int rear;//队尾下标
public MyCircularQueue(int k) {
elem = new int[k+1];
}
//入队
public boolean enQueue(int value) {
if(isFull()){
return false;
}
elem[rear] = value;
rear = (rear+1) % elem.length;
return true;
}
//从队头出队
public boolean deQueue() {
//空的 不能出
if(isEmpty()){
return false;
}
//不空 则保存队头元素 然后front往后走
//(当前位置+偏移次数)%数组长度
front = (front+1)%elem.length;
return true;
}
//得到队头元素
public int Front() {
if (isEmpty()){
return -1;
}
return elem[front];
}
//得到队尾元素
public int Rear() {
if (isEmpty()){
return -1;
}
//判断队尾的前一个是否合法
int index = rear == 0 ? (elem.length-1) : (rear-1);
return elem[index];
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
return (rear+1) % elem.length == front;
}
这里还需要注意:1、若队列不为空时,获取队尾元素时rear==0,说明rear已经走了一圈回来了,不能通过rear-1获取队尾元素,而是通过elem.length-1下标来获取。
2、因为我们需要多一个空间来判断数组是否已满,所以需要申请k+1个空间。
5.2 225. 用队列实现栈 - 力扣(LeetCode)
这里先提出一个问题,一个队列能够实现栈吗?答案是不能,因为队列是先进先出的数据结构;而栈是先进后出的数据结构。一个队列根本实现不了栈,所以我们需要两个队列。 当qu1和qu2都为空时说明,我们模拟实现的栈为空。
出栈:因为栈要出的是栈顶的元素,也就是队列中最后一个出的元素,所以我们可以在不为空的队列中先出size() - 1个元素,最后剩下的那个元素就是我们要出的元素。
入栈:入到不为空的队列即可,如果两个队列都为空就入到qu1。
入栈:
出栈 :
top方法:将不为空的队列的元素全部出队,最后一个出队的元素就是栈顶元素。
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
//入栈:入到不为空的队列,如果都为空,入到qu1即可
public void push(int x) {
if(!queue1.isEmpty()){
queue1.offer(x);
}else if (!queue2.isEmpty()){
queue2.offer(x);
}else{
queue1.offer(x);
}
}
//出栈时,出不为空的队列,出size-1个,最后那一个就是我要出栈的元素
public int pop() {
if (empty()){
return -1;
}
if(!queue1.isEmpty()){
int size = queue1.size();//这里的队列的长度随着元素的弹出会改变
for (int i = 0; i < size-1; i++) {
int x = queue1.poll();
queue2.offer(x);
}
return queue1.poll();
} else {
int size = queue2.size();
for (int i = 0; i < size-1; i++) {
int x = queue2.poll();
queue1.offer(x);
}
return queue2.poll();
}
}
//peek
//将所有元素通过tmp记录后入到另一个栈里面,最后tmp记录的元素就是我要peek的元素
public int top() {
if (empty()){
return -1;
}
int x = -1;
if(!queue1.isEmpty()){
int size = queue1.size();//这里的队列的长度随着元素的弹出会改变
for (int i = 0; i < size; i++) {
x = queue1.poll();
queue2.offer(x);
}
return x;
} else {
int size = queue2.size();
for (int i = 0; i < size; i++) {
x = queue2.poll();
queue1.offer(x);
}
return x;
}
}
//栈是否为空
public boolean empty() {
//两个队列都为空的时候,说明我模拟实现的栈为空
return queue1.isEmpty() && queue2.isEmpty();
}
}
需要注意的是因为在出栈的过程中,栈的长度在不断变小,所以需要定义一个变量获取栈的长度,防止出栈出少元素。
5.3 232. 用栈实现队列 - 力扣(LeetCode)
一个队列实现不了栈,那一个栈能实现队列吗?同样不能,所以我们同样需要两个栈来实现队列。
入队:直接将所有元素入到s1(第一个栈)。
出队:将s1(第一个栈)的元素全部倒到s2(第二个栈)中,再出第二个栈的元素。
入队:
出队 :
class MyQueue {
//入队时,把所有元素放到第一个栈中
//出队时(判断空不空)把第一个栈的所有元素全部倒回第二个栈中,出第二个栈的栈顶元素
//定义两个栈
Stack<Integer> s1;
Stack<Integer> s2;
//构造方法
public MyQueue() {
s1 = new Stack<>();
s2 = new Stack<>();
}
//统一入到s1
public void push(int x) {
s1.push(x);
}
//出的时候统一出s2的
public int pop() {
if(s1.empty()){
return -1;
}
while(!s1.empty()){
s2.push(s1.pop());
}
int x = s2.pop();
while(!s2.empty()){
s1.push(s2.pop());
}
return x;
}
public int peek() {
if(s1.empty()){
return -1;
}
while(!s1.empty()){
s2.push(s1.pop());
}
int x = s2.peek();
while(!s2.empty()){
s1.push(s2.pop());
}
return x;
}
public boolean empty() {
return s1.empty();
}
}