栈(Stack)
栈和队列也是基于List来实现的,但是它的限制比List更严格,换句话说它提供的操作更少或者List比栈和队列要更灵活。
- 对于栈(Stack)来说,它只支持3个核心操作:入栈、出栈、取栈顶元素,因为它具有的这些操作,所以他有一个特点叫做后进先出。
- 对于队列(Queue)来说,它也支持3个核心操作:入队列、出队列、取队首元素,因为它具有的这些操作,所以他有一个特点叫做先进先出(类似于食堂排队,谁先来谁先打饭谁就先离开)。
为什么他们的限制比List更严格呢?
- 在写程序的时候,限制不见得是坏事,灵活往往也不见得是好事,越灵活就越容易出错。
- 正因为有限制,未来在java学习的后面,会学习Java中的一些框架。
- 所谓框架的本质,就是限制你写代码的灵活性。有了框架的限制,就可以让你的代码写的不会很差。
- JVM的内存区域划分(也称为JVM的内存模型),其中有一个JVM虚拟机栈,此处的JVM栈特指JVM中的那个内存区域。而现在所说的栈是数据结构中的通用概念。
- 数据结构和编程语言是无关的,各种编程语言都有数据结构的概念。
概念
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈
顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
栈图:Stack的Push和Pop遵循先进后出(Last In First Out)规则
栈的实现
- 利用顺序表实现,即使用尾插+尾删的方式实现。
- 利用链表实现,则头尾皆可。
相对来说,顺序表的实现上要更为简单一些,所以优先用顺序表实现栈。
public class MyStack {
//管理一些int元素即可,也不考虑扩容问题
private int[] array=new int[100];//创建一个数组,初始化成100个元素
private int size=0;//表示栈中存在多少个有效元素
//《要实现的三个方法如下》
//入栈:插入元素
public void push(int x){
array[size]=x;
size++;
}
//取栈顶(最后进来的那个元素)
public int peek(){
return array[size-1];
}
//出栈
public int pop(){
int ret=array[size-1];
size--;
return ret;
}
//从以上代码来看,实现一个栈还是很容易的,只要在顺序表的基础上稍加限制就可以了
// 一个栈只要能支持这三种操作就好了,栈的实现还是很容易的,主要是我们能够结合栈这样的思想来帮助我们解决一些实际问题
}
队列(Queue)
概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First
In First Out)的规则。
入队列:进行插入操作的一端称为队尾(Tail/Rear)
出队列:进行删除操作的一端称为队头(Head/Front)。
队列图:先进先出FIFO(First In First Out) 。
队列的实现
队列也可以使用数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
package java2021_1003;
/**
* Description:通过链表实现队列
*/
public class MyQueueByLinkedList {
//定义一个链表节点
//static的效果就是:常见Node的实例不依赖MyQueueByLinkedList 这个类的实例
static class Node{//Node 这个类叫做“内部类”,内部类就是定义在某个类或者方法中的类
public int val;
Node next=null;//初始化为空
//alt+insert,找一个构造方法
public Node(int val) {
this.val = val;
}
}
//创建一个链表,需要有头结点
//基于链表来实现队列,可以入队列从尾部插入,出队列从头部删除;也可以入队列从头部插入,出队列从尾部删除
//但是不管是哪种实现方式,最好把头和尾都记录下来
//此时这个头head结点不是傀儡节点
private Node head=null;//头结点初始为null
private Node tail=null;
//《要实现的三个方法如下》
//此处按照尾部入队列,头部出队列的方式实现
//入队列(标准库中的队列,入队列操作就叫offer(提供))
public void offer(int val){
Node newNode=new Node(val);
if(head==null){//如果是空队列,直接让head和tail都指向新结点
head=newNode;
tail=newNode;
return;
}
//当前如果不是空链表,就可以把这个结点插到末尾
tail.next=newNode;
tail=tail.next;
}
//出队列(从头部删除)
public int poll(){//把删除的元素返回来
//如果当前队列就是空队列,再去poll显然不科学
if(head==null){
//如果出队列失败,就返回一个错误值
return -1;//我们不知道当前得到的-1,是因为出队列失败得到的-1,还是说本来这里面存的就是-1,所以说-1在这里不能作为非法值
//为了能够更好地进行返回值操作,可把int改为Integer,出错的时候直接返回一个null,null肯定是一个非法值
//Integer叫做包装类,包装类也是一个引用类型的对象,引用类型就可以是一个空引用,空引用就可以表示她是非法情况
//如果是其他值,那它就是非空引用,在里面包含一个具体的值就ok了。
}
int ret=head.val;
head=head.next;
if(head==null){//删除当前元素之后,队列变成了空的队列
tail=null;
}
return ret;
}
//取队首元素
public Integer peek(){
if(head==null){
return null;
}
return head.val;
}
}
循环队列的实现
使用数组实现队列的代码示例:
package java2021_1003;
/**
* Description:通过数组实现队列
*/
public class MyQueueArray {
//如果使用数组实现队列它的出队列效率太低,不太适合,所以可以使用循环队列的方式实现出队列操作
private int[] array=new int[100];
//有效元素的范围是[head,tail),注意,tail可能在head之前的情况
private int head=0;
private int tail=0;
private int size=0;//size表示元素个数
//写里面的操作
//进队列操作(插入元素)
public void offer(int val){
//先判断队列是否为满
if(size==array.length){
//此时表明队列满了,无法继续插入,直接返回
return;
}
//保证这个操作下标不能越界
array[tail]=val;//如果没满的话就把tail位置的元素赋成val
tail++;
if(tail>=array.length) {//如果tail++之后,超出了数组的有效范围,就从头开始
tail=0;
}
size++;//同时增加元素
}
//出队列操作
public Integer poll(){
//先判断size是否为0,如果等于0,就直接返回null队列
if(size==0){
return null;
}
//如果不是空队列
Integer ret=array[head];
head++;
if(head>=array.length){
head=0;
}
size--;//同时删除元素
return ret;
}
//取队首元素
public Integer peek(){
if(size==0){//判断队列是否为空
return null;
}
return array[head];//如果队首为null,直接返回head
}
}
双端队列
概念
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。因为,栈(Stack),它是一个类,所以我们在使用栈的时候,就可以直接去创建Stack这样的一个实例,而Queue是一个接口,所以在使用它的时候只能基于其他的类来创建实例。
Queue
错误处理 | 抛出异常 | 返回特殊值 |
---|---|---|
入队列 | add(e) | offer(e) |
出队列 | remove() | poll() |
队首元素 | element() | peek() |
如果队列满了或者队列为空,失败就会抛出异常;
返回特殊值是指如offer会返回true或false这样的结果特殊值;
poll 、peek会返回一个null表示当前是一个特殊情况.