栈与队列无疑是数据结构中重要的两个模型,接下来就让我们来好好的来对它两进行剖析
一: 栈(Stack)
1.1 概念
栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出的原则。我们来用图形理解一下.
如图所示:
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
如图所示:3最后入栈,但是出栈的时候是3先出.即后进先出
1.2 栈中的常用方法.
方法 | 功能作用 |
---|---|
Stack() | 构造一个空栈 |
E push(E e) | 将e入栈并且返回e |
E pop() | 将栈顶元素出栈并且返回 |
E peek() | 获取栈顶元素但是不出栈 |
int size() | 获取栈中有效元素个数 |
boolean empty() | 检测栈是否为空 |
对上述方法来进行简单的使用,我们直接上代码
public static void main(String[] args){
//创建一个空的栈,站里面存的数据类型为Integer型.
Stack<Integer> s=new Stack<>();
//往栈里面push元素.
s.push(1);
s.push(2);
s.push(3);
s.push(4);
//获取栈中元素个数,此时元素个数为4
System.out.println(s.size());
//获取栈顶元素,此时的栈顶元素为4,因为4是最后一个入栈的元素.
System.out.println(s.peek());
//将栈顶元素出栈,并且返回栈顶元素
s.pop();
//再次打印栈中的元素个数,来看4是否已经出栈
System.out.println(s.size()); //元素个数为3
System.out.println(s.peek()); //栈顶元素为3
//判断栈是否为空.
if(s.empty){
//显而易见,此栈不为空.
System.out.println("s is null");
}else{
System.out.println("s is not null");
}
}
1.3 栈的模拟实现(主要学思想)
这个部分我们对1.2中的方法来进行一个简单的实现,这里我们会使用泛型,没有接触过的好兄弟可以先去了解一下.这里就不过多解释了.
a: Stack()
//类似于一个线性表
public class Stack<E>{
E[] array;
//size,用来表示栈中总共有多少个元素 ||size-1表示栈顶元素的位置
int size;
public Stack(){
//将array初始化,并且将Object类型进行强制类型转换转换为E型的.
array=(E[])new Object[10];
//元素个数初始化为0
size=0;
}
b: push().
//入栈(相当于尾插)
public void push(E e){
//如果数组容量不足,则自动进行扩容.
ensureCapacity();
//将元素e放在size的位置.
array[size]=e;
//元素入栈以后,元素个数+1;
size++;
}
//进行扩容
private void ensureCapacity() {
if (size==array.length){
//如果元素个数和数组容量相同时,我们进行扩容.
//将容量扩大两倍
int newCapacity=size*2;
//用array来接收所申请到的容量.
array= Arrays.copyOf(array,newCapacity);
}
}
}
至于为什么要放在size的位置,我们来看图:
图中的元素个数为5,我们直接将要插入的元素e直接放在下标为5的位置,再将size++向后走一步即可实现入栈(尾插).
c: peek()
//返回栈顶元素,不用删除
public E peek(){
//对栈进行判空,为空肯定就不能出栈了.我们在此处抛出栈空的异常
if (empty()){
throw new RuntimeException("peek:栈是空的");
}
//返回栈顶元素.
return array[size-1];
}
size-1位置的元素刚好是最后一个元素.
d: pop()
//删除栈顶元素并且返回栈顶元素
public E pop(){
//对栈是否为空进行判断
if (empty()){
throw new RuntimeException("pop:栈是空的");
}
//先用e将栈顶元素标记起来,因为最后要返回栈顶元素.
//peek()方法得到的就是栈顶的元素.
E e=peek();
//size-1即可实现出栈
size--;
//返回开始标记的栈顶元素e
return e;
}
直接上图:
第一个图中,数组中元素个数为5个,经过size - -,让数组中有效元素个数变为了4个,即可实现删除元素,即出栈.刚开始我们用e标记了栈顶元素,所以最后直接返回e就行.
e: size()
//返回栈中元素的个数.
public int size(){
return size;
}
f: empty()
//判断栈是否为空
public boolean empty(){
return size==0;
}
栈在这就结束了,大家一定要记住栈是后进先出,我们接下来看队列.
二:队列(Queue)
2.1概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出的特点.
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
看图理解
如图,1从队尾先进队列,从队头先出队列.
2.2 队列中的常用方法
方法 | 功能作用 |
---|---|
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
E peek() | 获取队头元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
队列中所有的方法时间复杂度都为1.
对上述的方法来进行简单的使用.
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。
public static void main(String[] args) {
Queue<Integer> q=new LinkedList<>();
q.offer(1);
q.offer(2);
q.offer(3);
q.offer(4);
q.offer(5);
q.offer(6);
System.out.println(q); // [1, 2, 3, 4, 5, 6]
System.out.println(q.size()); //队列中元素个数 6
System.out.println(q.peek()); //获取队头元素 1
q.poll(); //删除队头元素
System.out.println(q.size()); //5
System.out.println(q.peek()); //2
System.out.println(q); //[2, 3, 4, 5, 6]
}
2.3 队列的模拟实现.
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,我们这里使用双向链表进行模拟实现.
a: 定义属性
public class Queue<E> {
//内部类,用来定义节点的属性.
public static class Node<E>{
E value;
//指向下一个节点
Node<E> next;
//标记前一个节点.
Node<E> prev;
//构造方法
public Node(E val){
this.value = val;
}
}
Node<E> first; //队头
Node<E> last; //队尾
int size; //队列中有效元素个数.
b: offer()
插入元素的时候要考虑一下,看队列是否为空.
如果队列为空的话可以直接插入.
如果队列不为空时要往最后一个节点的后面进行插入.
注意:在成功插入元素后要让last后移,保证last始终在最后一个位置.
用图来演示一下了可能更加直观:
last.next=newNode;让last的下一个指向新插入的节点.
newNode.prev=last;newNode的前一个节点指向last;
然后last=newNode;即让last往后移动到newNode的位置,也就是此时的最后一个位置.
这样就可以实现插入一个元素.
//插入元素(尾插)
public void offer(E val){
//创建值为val的节点
ListNode<E> newNode=new ListNode<>(val);
//队列为空时,直接插入.
if (first==null){
first=newNode;
}else{
//不为空时,往最后一个节点后面的位置插入.
last.next=newNode;
newNode.prev=last;
}
//插入成功后,让last指向最后一个位置.
last=newNode;
//元素个数+1;
size++;
}
c: poll()
删除队头元素时,我们要考虑三种情况.
a.队列是否为空: 队列为空的时候肯定是不能删除的.
b.队列不为空且队列中只有一个元素.
c.队列中有多个元素.
第一种情况就不需要看了,直接return null就行.
第二种情况:因为只有一个节点,所以令first和last都为空就行.
first=null;
last=null;
主要来看第三种情况:我们要删除对头元素,也就是节点1.
因为最后要返回对头元素的值,所以我们要先把first.val值保存一下.
然后让first向右移动一位.
上述操作之后.我们直接让first的前一个节点指向null,然后让first的前指向指向null,就可以删除1号节点,所以元素个数size-1,最后直接返回value的值就行.
d: peek()
返回栈顶元素的值.
这个直接给出代码就行,相信大家可以看懂.
public E peek(){
//为空时返回空
if (first==null){
return null;
}
//不为空时,直接返回first.val.
return first.val;
}
e: size()
返回队列中有效元素的个数
public int size(){
return size;
}
f: isEmpty
判断队列是否为空.
public boolean isEmpty(){
return size==0;
}
2.4循环队列
2.4.1 产生的背景
其实除了用双向链表可以模拟实现队列外,还可以使用连续空间实现,但是在使用连续空间的时候,会出现一些问题,循环队列就是用来解决这些问题的而出现的.
假设我们现在使用连续空间来模拟实现队列
用front标记对头元素,用rear表示队尾位置.
假设我们插入的是 1 2 3 4 5 6 六个元素.队列容量为10.
入队列:
入队列的时候比较简单,我们只需要将元素放到rear的位置,然后rear++就行.
时间复杂度和标准库中一样为O(1).
出队列:
出队列的时候有两种方式:
方式一:保持front不动,将对头后的元素整体往前搬移一个位置,最后将rear- -一下.
比如删除元素1.时间复杂度为O(N)
方式二:保持rear不动,front++.如图,相当于将front之前的元素全部出队列了.
时间复杂度为O(1).
在方式二中,假设继续给数组中添加元素,直到加满.此时的效果图如下:
此时数组就不能往进添加元素了,front前面的位置没有有效元素,即当前数组的有效元素没有存满,所以还有三个地方空着,这种情况我们称之为队列的假溢出.
为了解决上述使用连续空间实现队列时的假溢出问题,就引入了循环队列.
2.4.2 循环队列的实现
我们这里用
直接上图:
循环队列基础图
里面的数字是数组元素值,外围的是数组下标.
入队列: 将元素放在rear队尾位置,然后rear往后移动一步.
出队列: front往后走一步.
假设经过入队列后现在环形队列如下图,数组的容量为7,里面现在有7个元素:
当70入完队列后rear++重新走到队头位置.
当前10和20两个元素出队列后,空出两个位置,有效元素个数此时为5个.
front走到2号位置
此时如果想要入队列,直接放在rear的位置,rear++就行,解决了上述入队列方式2的队列假溢出的问题.
但是此时又出现了新的问题:如何判断循环队列的空满状态呢?
2.4.3 循环队列的空满判断
我们从上面的图中可以看出,在循环为空的时候,front和rear在同一个位置,但是当元素入队列后,队列满了后,rear又和front相遇了,在同一个位置.此时就不知道怎么判断了.别急,我们慢慢往下看.
方式一:使用count来进行计数.
队列空时:count == 0;
队列满时:count ==array.length;
方式二:少存储一个元素
队列空时:front=rear;
队列满时:(rear+1) % array.length == front
取模的原因,如图:
当rear处于当前位置时,我们看出来rear+1与front处于同一个位置,但是真实情况是
rear+1=7,而front=0,所以此时我们给(rear+1) % array.length,结果为0,就和front相遇了.
方式三:设置标志位.
flag=false;
入队列时:rear需要向下一个位置移动,同时让flag=true;
出队列时:front需要向下一个位置移动,同时让flag=false;
队列空时:front==rear && flag==false;
当满足上面条件时,flag==false,证明是在出队列之后,front与rear相遇了,
肯定表示队列为空.
队列满时:front==rear && flag==true;
当满足上面条件时,flag==true,证明是在入队列之后,front与rear相遇了,
肯定表示队列满.
2.4.4 队尾元素的获取
情况一:队列未满时,直接获取(rear-1)位置的元素
rear-1=5
array[5]=60;
情况二:队列满时,获取(rear-1+array.length)位置元素.
为了方便,我们让N=array.length;
rear-1+N=0-1+7=6.
array[6]=70;
这样看是不是挺麻烦的,还要分两种情况,那么有没有办法用一个式子来表示两种情况呢?答案是有的.
公式就是 array[(rear-1+N)%N].
我们来对上面两种情况进行验证:
a.情况一:
rear=6,N=7;
(rear-1+N)%N=12%7=5;
array[5]=60;
b.情况二:
rear=0,N=7;
(rear-1+N)%N=6%7=6;
array[6]=70;