文章目录
- [1. 栈的模拟实现](#1. 栈的模拟实现)
-
- [1.1 普通栈的模拟实现](#1.1 普通栈的模拟实现)
- [1.2 泛型栈的模拟实现](#1.2 泛型栈的模拟实现)
- [2. 栈的介绍](#2. 栈的介绍)
- [3. 栈的使用](#3. 栈的使用)
- [4. 栈的应用场景](#4. 栈的应用场景)
-
- [4.1 改变元素的序列](#4.1 改变元素的序列)
- [4.2 将递归转换为循环](#4.2 将递归转换为循环)
- [4.3 使用栈解题](#4.3 使用栈解题)
- [5. 栈的链表实现](#5. 栈的链表实现)
- [6. 队列概念](#6. 队列概念)
- [7. 队列的使用](#7. 队列的使用)
- [8. 模拟队列的实现](#8. 模拟队列的实现)
-
- [8.1 链式队列](#8.1 链式队列)
- [8.2 顺序队列](#8.2 顺序队列)
- [9. 双端队列](#9. 双端队列)
1. 栈的模拟实现
从上图中可以看到,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。
1.1 普通栈的模拟实现
栈的模拟实现没有顺序表和链表难,实现代码和对应的注释如下。
java
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(){
//判空,如果为空返回-1
if(empty()){
return -1;
}
//出栈后元素个数减一
usedSize--;
return elem[usedSize];
}
//查看
public int peek(){
//判空,如果为空返回-1
if(empty()){
return -1;
}
//peek只返回,不删除
return elem[usedSize-1];
}
//判空
public boolean empty(){
return usedSize==0;
}
}
测试代码
java
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 泛型栈的模拟实现
要使栈变成泛型,仅需改变数据类型。
java
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(){//返回类型为E
if(empty()){
return null;
}
usedSize--;
return (E)elem[usedSize];//类型转换
}
public E peek(){//返回类型为E
if(empty()){
return null;
}
return (E)elem[usedSize-1];//类型转换
}
public boolean empty(){
return usedSize==0;
}
}
测试代码
java
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. 栈的使用
java
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 将递归转换为循环
当我们逆序打印单向链表时,可以通过递归的方式打印。
java
// 递归方式
void printList(Node head){//传入链表的头结点
if(null != head){
printList(head.next);//递归
System.out.print(head.val + " ");
}
}
我们也可以利用栈的方式,将链表的节点入栈,然后出栈。就可以实现逆序打印。
java
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)。因此,如果我们要使用链式栈,就可以选用双线链表来操作。
代码示例
java
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
使用代码示例
java
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 链式队列
我们使用双向链表来实现链式队列。实现代码如下:
java
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时就为满。
java
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()){//判满,如果队列满了返回false
return false;
}
//元素入队列
elem[last]=value;
//队尾位置往后移一个
last=(last+1)%elem.length;
//元素个数加1
usedSize++;
//成功入队列,返回true
return true;
}
//出队列
public boolean deQueue() {
if(isEmpty()){//判空,如果队列为空返回false
return false;
}
//出队列直接让对头往后移动一个
first=(first+1)%elem.length;
//元素个数减1
usedSize--;
//出队列成功,返回true
return true;
}
//看队头元素
public int Front() {
if(isEmpty()){//判空,如果为空返回-1
return -1;
}
//不为空,first下标即为队头元素
return elem[first];
}
//看队尾元素
public int Rear() {
if(isEmpty()){//判空,如果为空返回-1
return -1;
}
int index;
if(last==0) {//如果last为0,队尾元素下标就为elem.length-1
index = elem.length-1;
}else {//否则,队尾元素下标为last-1
index = last-1;
}
//返回对应下标的元素
return elem[index];
}
//判空
public boolean isEmpty() {
//如果usedSize为0,队列尾空
return usedSize==0;
}
//判满
public boolean isFull() {
//如果usedSize等于数组长度队列为满
return usedSize==elem.length;
}
}
- 使用一个boolean类型的标记,。
java
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()){//判满,如果队列满了返回false
return false;
}
//元素入队列
elem[last]=value;
//队尾位置往后移一个
last=(last+1)%elem.length;
if(last==first){//入队列时,last走完一圈回到first队列就满了
flag=true;
}
//成功入队列,返回true
return true;
}
//出队列
public boolean deQueue() {
if(isEmpty()){//判空,如果队列为空返回false
return false;
}
//出队列直接让对头往后移动一个
first=(first+1)%elem.length;
flag=false;//出队列时,队列就不满,标志设为false
//出队列成功,返回true
return true;
}
//看队头元素
public int Front() {
if(isEmpty()){//判空,如果为空返回-1
return -1;
}
//不为空,first下标即为队头元素
return elem[first];
}
//看队尾元素
public int Rear() {
if(isEmpty()){//判空,如果为空返回-1
return -1;
}
int index;
if(last==0) {//如果last为0,队尾元素下标就为elem.length-1
index = elem.length-1;
}else {//否则,队尾元素下标为last-1
index = last-1;
}
//返回对应下标的元素
return elem[index];
}
//判空
public boolean isEmpty() {
//如果last==first且flag为false,队列为空
return last==first&&!flag;
}
//判满
public boolean isFull() {
//如果队列为满,flag为true
return flag;
}
}
- 浪费一个空间,当first+1==last时,为满。
java
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()){//判满,如果队列满了返回false
return false;
}
//元素入队列
elem[last]=value;
//队尾位置往后移一个
last=(last+1)%elem.length;
//成功入队列,返回true
return true;
}
//出队列
public boolean deQueue() {
if(isEmpty()){//判空,如果队列为空返回false
return false;
}
//出队列直接让对头往后移动一个
first=(first+1)%elem.length;
//出队列成功,返回true
return true;
}
//看队头元素
public int Front() {
if(isEmpty()){//判空,如果为空返回-1
return -1;
}
//不为空,first下标即为队头元素
return elem[first];
}
//看队尾元素
public int Rear() {
if(isEmpty()){//判空,如果为空返回-1
return -1;
}
int index;
if(last==0) {//如果last为0,队尾元素下标就为elem.length-1
index = elem.length-1;
}else {//否则,队尾元素下标为last-1
index = 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接口是比较多的,栈和队列均可以使用该接口。
java
//下面这两个类也可以当做链表栈使用,也可以当做队列使用
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现
下面是Deque的一些方法。
栈和队列相关练习题链接