文章目录
- 1. 栈的模拟实现
- 1.1 普通栈的模拟实现
- 1.2 泛型栈的模拟实现
- 2. 栈的介绍
- 3. 栈的使用
- 4. 栈的应用场景
- 4.1 改变元素的序列
- 4.2 将递归转换为循环
- 4.3 使用栈解题
- 5. 栈的链表实现
- 6. 队列概念
- 7. 队列的使用
- 8. 模拟队列的实现
- 8.1 链式队列
- 8.2 顺序队列
- 9. 双端队列
1. 栈的模拟实现
从上图中可以看到,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。
1.1 普通栈的模拟实现
栈的模拟实现没有顺序表和链表难,实现代码和对应的注释如下。
public class MyStack {int[] elem;int usedSize=0;public MyStack(){elem=new int[10];}//入栈public void push(int e){//判满,如果满了扩容if(isFull()){elem= Arrays.copyOf(elem,elem.length*2);}//入栈elem[usedSize]=e;usedSize++;}//判满public boolean isFull(){return usedSize==elem.length;}//出栈public int pop(){//判空,如果为空返回-1if(empty()){return -1;}//出栈后元素个数减一usedSize--;return elem[usedSize];}//查看public int peek(){//判空,如果为空返回-1if(empty()){return -1;}//peek只返回,不删除return elem[usedSize-1];}//判空public boolean empty(){return usedSize==0;}
}
测试代码
public static void main(String[] args) {MyStack s=new MyStack();s.push(1);s.push(2);s.push(3);System.out.println(s.pop());System.out.println(s.peek());System.out.println(s.peek());}
1.2 泛型栈的模拟实现
要使栈变成泛型,仅需改变数据类型。
public class MyStack2 <E>{public Object[] elem;//泛型的本质是Object类public int usedSize = 0;public MyStack2(){elem=new Object[10];}public void push(E e){if(isFull()){elem= Arrays.copyOf(elem,elem.length*2);}elem[usedSize]=e;usedSize++;}public boolean isFull(){return usedSize==elem.length;}//出栈public E pop(){//返回类型为Eif(empty()){return null;}usedSize--;return (E)elem[usedSize];//类型转换}public E peek(){//返回类型为Eif(empty()){return null;}return (E)elem[usedSize-1];//类型转换}public boolean empty(){return usedSize==0;}
}
测试代码
public static void main(String[] args) {MyStack2<String > s = new MyStack2<>();s.push("hello");s.push("zzuli");s.push("welcome");s.push("to");s.push("zzuli");System.out.println(s.pop());System.out.println(s.pop());System.out.println(s.peek());System.out.println(s.peek());}
2. 栈的介绍
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
3. 栈的使用
public static void main(String[] args) {Stack<Integer> s=new Stack<>();s.push(1);//入栈s.push(2);//入栈s.push(3);//入栈s.push(4);//入栈s.push(5);//入栈System.out.println(s.size());//栈的数据个数System.out.println(s.pop());//出栈System.out.println(s.pop());//出栈System.out.println(s.peek());//查看栈顶元素System.out.println(s.peek());//查看栈顶元素if(s.empty()){//判空System.out.println("栈空");}else{System.out.println("栈不为空");}}
4. 栈的应用场景
4.1 改变元素的序列
答案:C、B
4.2 将递归转换为循环
当我们逆序打印单向链表时,可以通过递归的方式打印。
// 递归方式
void printList(Node head){//传入链表的头结点if(null != head){printList(head.next);//递归System.out.print(head.val + " ");}
}
我们也可以利用栈的方式,将链表的节点入栈,然后出栈。就可以实现逆序打印。
void printList(Node head){if(null == head){return;}Stack<Node> s = new Stack<>();// 将链表中的结点保存在栈中Node cur = head;while(null != cur){s.push(cur);cur = cur.next;}// 将栈中的元素出栈while(!s.empty()){System.out.print(s.pop().val + " ");}}
4.3 使用栈解题
- 括号匹配
- 逆波兰表达式求值
- 出栈入栈次序匹配
- 最小栈
上面四题题解链接
5. 栈的链表实现
Java集合类的Stack底层是一个数组。这种栈叫做顺序栈。那么是否可以使用链表来实现栈呢?
如果那单链表来实现栈,最好使用链表头部进行入栈和头栈,这样时间复杂度为O(1)。相反,如果从链表的尾部入栈和出栈,时间复杂度为O(n)(此时需要遍历链表才能实现入栈和出栈)。
如果使用双向链表,从链表的头或者尾进行入栈和出栈,时间复杂度均为O(1)。因此,如果我们要使用链式栈,就可以选用双线链表来操作。
代码示例
public static void main(String[] args) {LinkedList<Integer> stack=new LinkedList<>();stack.push(1);//入栈stack.push(2);stack.push(3);stack.pop();//出栈}
通过Ctrl查看LInkedList
的push方法和pop方法
可以发现,其本质是链表的头插和头删。
6. 队列概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出得的特点FIFO(First In First Out)。
入队列:进行插入操作的一端称为队尾(Tail/Rear)
出队列:进行删除操作的一端称为队头(Head/Front)
7. 队列的使用
在Java中,Queue是个接口,底层是通过链表实现的。
方法 | 功能 |
---|---|
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
peek() | 获取队头元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。
上面是Queue接口的一些方法。add方法和offer方法
都是用来新增元素的。remove方法和poll方法
都是用来弹出元素的。element方法和peek方法
都是用来获取队头元素的。那他们有什么区别呢?
注释翻译:
add方法
如果可以立即插入到此队列中,则在不违反容量限制的情况下插入指定的元素,成功后返回 true,如果当前没有可用空间,则引发 IllegalStateException。
参数:
e – 要添加的元素
返回:
true(由 Collection.add 指定)
抛出:
IllegalStateException – 如果由于容量限制而此时无法添加元素
ClassCastException – 如果指定元素的类阻止将其添加到此队列中
NullPointerException – 如果指定的元素为 null,并且此队列不允许 null 元素
IllegalArgumentException – 如果此元素的某些属性阻止将其添加到此队列中
offer方法
如果可以立即插入到此队列中,则将其指定的元素插入到此队列中,而不会违反容量限制。当使用容量受限的队列时,此方法通常比 add 更可取,后者只能通过引发异常来无法插入元素。
参数:
e – 要添加的元素
返回:
如果元素已添加到此队列中,则为 true,否则为 false
抛出:
ClassCastException – 如果指定元素的类阻止将其添加到此队列中
NullPointerException – 如果指定的元素为 null,并且此队列不允许 null 元素
IllegalArgumentException – 如果此元素的某些属性阻止将其添加到此队列中
注释翻译:
remove方法
检索和删除此队列的头部。此方法与 poll() 的不同之处仅在于如果此队列为空,它会引发异常。
返回:
此队列的头部
抛出:
NoSuchElementException – 如果此队列为空
poll方法
检索并删除此队列的头部,如果此队列为空,则返回 null。
返回:
此队列的头部,如果此队列为空,则为 null
注释翻译:
element方法
检索但不删除此队列的头部。此方法与 peek 的唯一区别在于,如果此队列为空,它会引发异常。
返回:
此队列的头部
抛出:
NoSuchElementException – 如果此队列为空
peek方法
检索但不删除此队列的头部,如果此队列为空,则返回 null。
返回:
此队列的头部,如果此队列为空,则为 null
使用代码示例
public static void main(String[] args) {Queue<Integer> q=new LinkedList<>();q.offer(1);//入队列q.offer(2);q.offer(3);System.out.println(q.poll());//出队列System.out.println(q.poll());System.out.println(q.element());//查看第一个队列元素System.out.println(q.element());System.out.println(q.isEmpty());//队列判空}
8. 模拟队列的实现
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有两种:顺序结构和链式结构。
8.1 链式队列
我们使用双向链表来实现链式队列。实现代码如下:
public class MyQueue {static class ListNode{//队列节点public ListNode prev;public ListNode next;public int val;public ListNode(int val){this.val=val;this.prev=null;this.next=null;}}public ListNode head;public ListNode last;//入队列public void offer(int val) {ListNode node = new ListNode(val);//创建节点//尾插法入队列if(head==null){head=node;last=node;}else {last.next=node;node.prev=last;last=node;}}//出队列public int poll(){if(head==null){return -1;}//头删法出队列int ret=head.val;if(head.next==null){head=last=null;}else {head=head.next;head.prev=null;}return ret;}//查看队头元素public int peek(){if(head==null){return -1;}//如果队头不为空,返回队头的值return head.val;}//判断队列是否为空public boolean isEmpty(){//如果头节点为null,队列为空return head==null;}
}
8.2 顺序队列
使用顺序表来实现队列,如果不移动元素的位置来进行出队列和入队列就会造成一个问题:假设first为队头(可以出数据的下标),last为队尾(可以存放数据的下标),入队过程中last到顺序表的最后一个位置,而first端元素在出队列,这样就会使队列的前一段空出来了而没有存储数据。示例图如下:
如果将last重新回到0下标的空间,这样会使队列不连续。因此,我们通过数组来实现环形队列。
环形队列的连续问题解决了,但是当队尾走到顺序表的尽头,此时我们一直让fist++或者last++
可能会造成数组越界,我们可以在每次入队列或者出队列时加一个判断,当first==len或者last==len
时让他们的下标设置为0,。我们也可以通过表达式来设置下标,first=(first+1)%len或者last=(last+1)%len
,通过这个表达式就不用担心数组越界的问题了。
考虑完数组越界问题后,我们如何区分队列的空与满?单纯依靠头尾相遇并不能解决这个问题,因为环形链表头尾相遇时可能为空,也可能为满。我们有下面三种解决方案。题目:力扣循环队列。
- 使用usedSize,当usedSize==len时就为满。
class MyCircularQueue {public int[] elem;//存放队列元素public int first;//队列头public int last;//队列尾public int usedSize;//元素个数//构造方法public MyCircularQueue(int k) {elem=new int[k];this.usedSize=0;this.first=0;this.last=0;}//入队列 public boolean enQueue(int value) {if(isFull()){//判满,如果队列满了返回falsereturn false;}//元素入队列elem[last]=value;//队尾位置往后移一个last=(last+1)%elem.length;//元素个数加1usedSize++;//成功入队列,返回truereturn true;}//出队列public boolean deQueue() {if(isEmpty()){//判空,如果队列为空返回falsereturn false;}//出队列直接让对头往后移动一个first=(first+1)%elem.length;//元素个数减1usedSize--;//出队列成功,返回truereturn true;}//看队头元素public int Front() {if(isEmpty()){//判空,如果为空返回-1return -1;}//不为空,first下标即为队头元素return elem[first];}//看队尾元素public int Rear() {if(isEmpty()){//判空,如果为空返回-1return -1;}int index;if(last==0) {//如果last为0,队尾元素下标就为elem.length-1index = elem.length-1;}else {//否则,队尾元素下标为last-1index = last-1;}//返回对应下标的元素return elem[index];}//判空public boolean isEmpty() {//如果usedSize为0,队列尾空return usedSize==0;}//判满public boolean isFull() {//如果usedSize等于数组长度队列为满return usedSize==elem.length;}
}
- 使用一个boolean类型的标记,。
class MyCircularQueue {public int[] elem;//存放队列元素public int first;//队列头public int last;//队列尾public boolean flag;//标记//构造方法public MyCircularQueue(int k) {elem=new int[k];this.flag=false;//flag为false时,队列不满this.first=0;this.last=0;}//入队列public boolean enQueue(int value) {if(isFull()){//判满,如果队列满了返回falsereturn false;}//元素入队列elem[last]=value;//队尾位置往后移一个last=(last+1)%elem.length;if(last==first){//入队列时,last走完一圈回到first队列就满了flag=true;}//成功入队列,返回truereturn true;}//出队列public boolean deQueue() {if(isEmpty()){//判空,如果队列为空返回falsereturn false;}//出队列直接让对头往后移动一个first=(first+1)%elem.length;flag=false;//出队列时,队列就不满,标志设为false//出队列成功,返回truereturn true;}//看队头元素public int Front() {if(isEmpty()){//判空,如果为空返回-1return -1;}//不为空,first下标即为队头元素return elem[first];}//看队尾元素public int Rear() {if(isEmpty()){//判空,如果为空返回-1return -1;}int index;if(last==0) {//如果last为0,队尾元素下标就为elem.length-1index = elem.length-1;}else {//否则,队尾元素下标为last-1index = last-1;}//返回对应下标的元素return elem[index];}//判空public boolean isEmpty() {//如果last==first且flag为false,队列为空return last==first&&!flag;}//判满public boolean isFull() {//如果队列为满,flag为truereturn flag;}
}
- 浪费一个空间,当first+1==last时,为满。
class MyCircularQueue {public int[] elem;//存放队列元素public int first;//队列头public int last;//队列尾//构造方法public MyCircularQueue(int k) {//因为浪费了一个空间,所以为了保证能够存够k个元素就需要开辟k+1个空间elem=new int[k+1];this.first=0;//队头this.last=0;//队尾}//入队列public boolean enQueue(int value) {if(isFull()){//判满,如果队列满了返回falsereturn false;}//元素入队列elem[last]=value;//队尾位置往后移一个last=(last+1)%elem.length;//成功入队列,返回truereturn true;}//出队列public boolean deQueue() {if(isEmpty()){//判空,如果队列为空返回falsereturn false;}//出队列直接让对头往后移动一个first=(first+1)%elem.length;//出队列成功,返回truereturn true;}//看队头元素public int Front() {if(isEmpty()){//判空,如果为空返回-1return -1;}//不为空,first下标即为队头元素return elem[first];}//看队尾元素public int Rear() {if(isEmpty()){//判空,如果为空返回-1return -1;}int index;if(last==0) {//如果last为0,队尾元素下标就为elem.length-1index = elem.length-1;}else {//否则,队尾元素下标为last-1index = last-1;}//返回对应下标的元素return elem[index];}//判空public boolean isEmpty() {//头等于尾时为空return last==first;}public boolean isFull() {//浪费一个元素,尾的下一个为头就视为满return (last+1)%elem.length==first;}
}
9. 双端队列
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
Deque是一个接口,使用时必须创建LinkedList的对象。
在实际工程中,使用Deque接口是比较多的,栈和队列均可以使用该接口。
//下面这两个类也可以当做链表栈使用,也可以当做队列使用
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现
下面是Deque的一些方法。
栈和队列相关练习题链接