一.栈
1.1 栈的概念
栈 :一种特殊的线性表,其 只允许在固定的一端进行插入和删除元素操作 。进行数据插入和删除操作的一端称为栈 顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO ( Last In First Out )的原则
压栈:栈的插入操作叫做进栈 / 压栈 / 入栈, 入数据在栈顶 。出栈:栈的删除操作叫做出栈。 出数据在栈顶 。
栈在生活中是十分常见的,比如经常乘坐的电梯,就满足先进后出的特点
1.2 栈的方法
下图为栈的继承关系
栈只有无参构造,所以栈都初始化为
Stack<T> stack=new Stack();
下面看看栈的方法吧
方法 功能 E push(E e) 将 e 入栈,并返回 e E pop() 将栈顶元素出栈并返回 E peek() 获取栈顶元素 int size() 获取栈中有效元素个数 boolean empty() 检测栈是否为空
1.3 栈的模拟实现
1.3.1 用数组实现栈
为了满足栈先进后出的规则,使用数组实现栈时,栈顶应该是数组尾部
下面给出用数组模拟实现栈的代码
public class ArrayListStack { int[] elem; int size; static final int DEFAULTCAPACITY=10; public ArrayListStack() { elem=new int[DEFAULTCAPACITY]; } public int push(int data){ if(size==elem.length) { ensureCapacity(elem.length*2); } elem[size++]=data; return data; } private void ensureCapacity(int capacity){ elem= Arrays.copyOf(elem,capacity); } public int pop(){ if(size==0) { throw new NullException("栈内没有元素"); } return elem[--size]; } public int peek(){ if(size==0) { throw new NullException("栈内没有元素"); } return elem[size-1]; } public boolean empty(){ return size==0; } }
1.3.2 用链表实现栈
如果使用双向链表实现栈,既可以将last作为栈顶,也可以将head作为栈顶,push和pop操作的时间复杂度都是O(1)
如果使用单向链表实现栈,最好将head作为栈顶,push和pop时间复杂度为O(1)
如果将last作为栈顶,pop操作必须要找到last的前驱,时间复杂度是O(n)
因为双向链表实现栈比较简单,下面给出单向链表模拟实现栈的代码
public class LinkedListStack { class Node{ int val; Node prev; Node next; public Node(int val) { this.val = val; } } Node head; public int push(int data) { Node node=new Node(data); if(head==null) { head=node; return data; } node.next=head; head=node; return data; } public int pop(){ if(head==null) { throw new NullException("栈内没有元素"); } int data=head.val; head=head.next; return data; } public int peek() { if(head==null) { throw new NullException("栈内没有元素"); } return head.val; } public boolean empty(){ return head==null; } }
//异常定义 public class NullException extends RuntimeException{ public NullException() { } public NullException(String message) { super(message); } }
1.4 栈的练习
1.4.1 栈的压入、弹出序列
题目如下:
这道题的解题思路比较简单,我们只需要用栈来模拟元素入栈,弹出的过程即可。
基本过程:
用下标i遍历pushV,用下标j遍历popV,i每遍历一个元素,都要将当前元素入栈
将pushV[i]入栈后有两种情况:
1.栈顶元素!=popV[j],说明当前元素没有被弹出,i继续向后遍历即可
2. 栈顶元素==popV[j],说明当前pushV[i]入栈后立即被弹出,直接弹出栈顶元素即可
但是又出现一个问题:
所以要使用while循环继续弹出栈顶元素
最后,i遍历完pushV数组后,如果栈为空,说明所有的元素已被弹出,即入栈和出栈数组是匹配的,否则不匹配
实例代码如下
import java.util.ArrayList; import java.util.Stack; public class Solution { public boolean IsPopOrder(int [] pushA,int [] popA) { Stack<Integer> st=new Stack<>(); for(int i=0,j=0;i<pushA.length;i++) { st.push(pushA[i]); //此处不能用if,可能会一直弹出栈顶元素 while(!st.empty()&&st.peek()==popA[j]) { st.pop(); j++; } } if(st.empty()) return true; return false; } }
1.4.2 最小栈
最小栈
题目描述
这道题有两个难点:
1. 怎么保存最小的元素?
2. 当最小元素被弹出时怎么更新最小元素?
可以使用另一个栈专门保存当前入栈的最小元素,举例:
用minSt栈顶保存当前在栈内的最小元素
当元素入栈时分为三种情况:
1.2.入栈元素大于minSt栈顶元素,只入st,不入minSt
2.入栈元素小于minSt栈顶元素/minSt为空栈--入栈
第三种情况就是入栈元素==minSt栈顶元素,这种情况我们先来看看pop方法再议
pop元素分为两种情况
1. 要pop的元素是minSt的栈顶元素,两个栈同时pop
2. 要pop的元素不是minSt的栈顶元素,只pop普通栈
这样我们就实现了pop元素的同时更新最小元素
现在再来看push的第三种情况:如果要入栈的元素==minSt的栈顶元素
如下图,如果不入minSt栈,当我们要弹出-1时,根据刚才pop方法的逻辑,两个栈的-1均会被弹出,这时st栈内最小元素是-1,但它却并未被保存在minSt的栈顶
所以第三种情况也要入栈
实例代码如下
class MinStack { Stack<Integer> st; Stack<Integer> minSt; public MinStack() { st=new Stack<>(); minSt=new Stack<>(); } public void push(int val) { if(minSt.empty()||val<=minSt.peek()) { minSt.push(val); } st.push(val); } public void pop() { if(!st.empty()){ if(st.pop().equals(minSt.peek())) { minSt.pop(); } } } public int top() { return st.peek(); } public int getMin() { return minSt.peek(); } }
二. 队列
2.1 队列的概念
队列 :只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out)入队列:进行插入操作的一端称为 队尾( Tail/Rear )出队列:进行删除操作的一端称为队头( Head/Front )
2.2 队列的方法
在Java中,队列是一个接口,是由LinkedList实现的
上图中的Deque是双端队列,下文会提到
由于先进先出的局限性,队列的方法并不多
方法 功能 boolean offer(E e) 入队列 E poll() 出队列 peek() 获取队头元素 int size() 获取队列中有效元素个数 boolean isEmpty() 检测队列是否为空实际上Queue还有add和remove方法,与offer方法不同的是,add方法在堆空间不够时,会抛异常,但是offer方法只会返回false
remove方法当队列为空时,同样会抛异常,poll方法当队列为空时会返回null
2.3 队列的实现
可以使用双链表实现队列
如果让last当队尾,head当队头,push和pop方法时间复杂度为O(1);如果让last当队头,head当队尾,push和pop方法的时间复杂度同样是O(1)
也可以用单链表实现队列
如果让last当队尾,head当队头,push和pop方法时间复杂度均为O(1)
相反,如果让last当队头,pop元素时需要找到last的前驱,时间复杂度为O(n)
下面用单链表模拟实现队列
public class SingleQueue { class Node { int val; Node prev; Node next; public Node(int val) { this.val = val; } } Node head; Node last; int size; public boolean offer(int data) { Node node=new Node(data); if(head==null) { head=node; last=node; } else { last.next=node; last=node; } size++; return true; } public int poll() { if(head!=null) { int data=head.val; head=head.next; size--; return data; } return -1;//将-1当做Queue为空的标志 } public int peek() { if(head!=null) { return head.val; } return -1;//将-1作为队列为空的标记 } public int size(){ return size; } public boolean isEmpty(){ return head==null; } }
2.4 循环队列
实际上数组也可以实现队列,但这种队列呈环形。
那么环形队列的方法是怎么实现的呢?offer方法---队尾插入元素,队尾向前移动poll方法---队头向前移动,原先队头的元素可以被覆盖![]()
那么当队尾==数组长度时,这个队列并不一定是满的,比如下面这种情况
所以在插入元素时,last=(last+1)%len;
删除元素时同样,head=(head+1)%len;
那么又出现一个问题:队列为空或队列是满的时候,last==head,怎么区分这两种情况呢?
方式1:使用size来记录队列长度
方式2:浪费一个位置(见下图)
下面我们实现一下循环队列,下面的链接可以验证正确性
class MyCircularQueue { private int[] arr; int head; int last; public MyCircularQueue(int k) { arr=new int[k+1]; } public boolean enQueue(int value) { if(isFull()) { return false; } arr[last]=value; last=(last+1)%arr.length; return true; } public boolean deQueue() { if(isEmpty()) { return false; } head=(head+1)%arr.length; return true; } public int Front() { if(isEmpty()) return -1; return arr[head]; } public int Rear() { if(isEmpty()) return -1; int index=(last-1+arr.length)%arr.length; return arr[index]; } public boolean isEmpty() { return head==last; } public boolean isFull() { return (last+1)%arr.length==head; } }
2.5 双端队列
双端队列( deque )是指允许两端都可以进行入队和出队操作的队列deque 是 “double ended queue” 的简称。![]()
Deque 是一个接口,使用时必须创建 LinkedList 的对象。
实际上,栈和队列都可以用Deque来实现,所以Deque的适用场景要比栈和队列多
Deque<Integer> stack = new ArrayDeque<>(); // 双端队列的线性实现,可以代替栈Deque<Integer> queue = new LinkedList<>(); // 双端队列的链式实现,可以代替队列