前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
队列是针对线性表,进行了封装和限制,核心操作有三个:入队列,出队列,获取队首元素,队列的特点是“先进先出”
队列的底层实现有两种,链表和数组,这篇博客会分别模拟这两种底层实现,同时搭配算法题的练习来学习队列
这个专栏的数据结构是代码都是用Java来写的,JavaSE专栏现在已经全部更新完成,铁汁们复习基础知识时非常推荐使用,可以试一下💪💪💪
🎇个人主页:洋不写bug的博客
🎇所属专栏:数据结构专栏
🎇复习Java基础知识:Java学习之旅,从入门到进阶
🎇铁汁们对于数据结构基础的各种核心知识(不太常用的也有😆),都可以在上面的数据结构专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,队列简介
队列跟栈是比较类似的,只不过栈是一端进,一端出,所以先进的最后才能出
而队列有两端,队尾进,队首出,队列是先进先出

在实际开发中,队列的使用是远远多于栈的,因为队列中的元素进出有“先来后到”的顺序,有些耗时比较长的操作没办法一口气处理完,需要按照顺序一个一个处理,为了保证处理的顺序不乱,就可以把数据放到队列中
队列在标准库中的实现就是Queue,Queue是一个接口,是没办法直接创建实例的,只能创建其他类,来实现接口
add方法和offer方法就是入队列,一般使用offer(因为听起来比较吉利🐵)队首元素,入队列方法在传入元素类型不匹配时会抛出异常
元素出队列就是remove和poll方法,并且返回该元素,element和peek是取出队首元素
当队列为空时,remove和element方法会抛出异常,poll方法和peek方法会返回null,一般使用的是不抛出异常的版本,就是使用poll和peek

创建接口时,实现类可以是LinkedList,那为什么用LinkedList就可以了呢,按ctrl键点入LinkedList看一下,LinkedList实现了Deque接口(Deque是双端队列,两头都能进,两头都能出),而Deque接口又继承了Queue接口,所以LinkedList就相当于是实现了Queue接口

ArrayList是没有实现Queue接口的,不能作为Queue的实现类,但是队列是完全可以基于数组来实现的,下面博客会聊到
创建一个队列,用offer在里面添加元素,当列表为空时,使用poll会返回null,使用remove则会直接报错
import java.util.LinkedList;
import java.util.Queue;
public class Test {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.remove());
}`在这里插入代码片`
}

判定栈是否为空,就可以使用empty(),或者isempty()方法,但是队列是没有这些方法的,一般是用peek取出队首元素,如果返回值为null,就说明队列为空
if(queue.peek() == null){
}
2,队列模拟实现(基于链表)
我们模拟实现一个自己的队列MyQueue,基于单链表来实现,存储的元素类型为String
首先搞一个Node类,表示节点
public class Node {
public String val;
public Node next;
public Node(String val) {
this.val = val;
this.next = null;
}
}
用链表模拟实现队列,元素入队列就进行尾插,元素出队列就进行头删,读取队首元素就返回头节点val即可
offer方法就是先判断下head是否为null,如果不为null就直接在tail后插入新节点
public void offer(String val){
Node newNode = new Node(val);
if(head == null){
head = newNode;
tail = newNode;
return;
}
tail.next = newNode;
tail = newNode;
}
再重写下toString方法,利用字符串拼接,写好后就可以在main方法中测试offer方法了
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
Node cur = head;
while(cur != null){
stringBuilder.append(cur.val);
if(cur.next != null){
stringBuilder.append(",");
}
cur = cur.next;
}
stringBuilder.append("]");
return stringBuilder.toString();
}
public class Test {
public static void main(String[] args) {
MyQueue myQueue = new MyQueue();
myQueue.offer("aaa");
myQueue.offer("bbb");
myQueue.offer("ccc");
System.out.println(myQueue);
}
}

poll方法就是删除头部元素,并返回元素值,head为null时就返回null,head不为null时就head = head.next即可,如果删除后head等于null了,要把tail也改为null
public String poll(){
if(head == null){
return null;
}
String val = head.val;
head = head.next;
if(head == null){
tail = null;
}
return val;
}
peek方法就只需要判断下head是不是null,再返回head.val即可
public String peek(){
if(head == null){
return null;
}
return head.val;
}
再写个isEmpty()和size()方法
public boolean isEmpty(){
return head == null;
}
public int size(){
int size = 0;
for(Node cur = head;head != null;head = head.next){
size++;
}
return size;
}
3,数组队列原理
日常开发中,创建队列更多还是基于数组来实现,因为数组中元素出入的效率就比较高,虽然ArrayList不能是实现类,但是我们可以用ArrayDeque作为实现类
Queue<Integer> queue = new ArrayDeque<>();
这个ArrayDeque也可以作为双端队列的实现类(双端队列有addLast,addFirst,removeLast,removeFirst方法)
Deque<Integer> deque = new ArrayDeque<>();
那队列是如何用数组来实现的呢,我们画图来分析下原理,首先创建出一个空的数组,创建两个引用head和tail,当head等于tail时,队列为空

当元素入队列时,元素存到tail指向的位置,tail再往后移动一位

那元素出队列时应该如何解决呢,如果把数组后面的元素都往前移动一格,那时间复杂度就是O(N),这样效率是很低的

那把元素出队列时,把head往后移动一位,并不真正的删除元素,只是把之前head指向的这块元素标记为无效,这样数组就无需进行元素搬运

按照这种思想,那tail到达数组末尾的时候,就并不意味着队列已经满了,前面可能又有一大片的空白区域,就可以让tail拐到前面继续存储数据

数组队列当作环形来看会更好理解一些,铁汁们可以自己画图模拟下,初始时head和tail都在下标0处,元素入队列时,tail往后顺时针移动

元素出队列时,head顺时针移动,如下图,两个元素出队列(数组中的元素实际上是没有删除的,只是标记为无效了,后续元素入队列直接把这块这之前的值给覆盖掉,图中为了美观,就把元素删除了)

接着一直往队列里面加入元素,当head和tail重合时,就说明队列已经满了

当队列为空时,head和tail也是重合的,所以靠head和tail重合是无法判断出队列满了,那想要判断,有两个办法
直接浪费一个格子,当head走到tail前面一个格子的位置时,就说明队列已经满了

还有一种更简单直接的做法,就是引入一个size变量,表示队列中元素的个数,如果size为0,队列就为空;如果size等于arr.length,那数组就为满
数组这种方案,相比链表有三大优点:
- 使用数组时的效率要高于链表
数组中元素入队列和出队列就是简单的head++和tail++,而链表再修改应用的指向时,还涉及到一个间接寻址操作(需要先读取到引用的值,得到地址,再根据地址来访问内存空间) - 使用数组对于队列中元素个数的上限是可控的
对于链表来说,可以一直往里面插入元素,如果代码出现了bug,可能不会报错,就是一直插入,直到把内存耗尽,结果就可能会造成整个程序的瘫痪😅
而对于数组版本,如果不去自动扩容的话,出现类似的问题,就能在入队列时及时报错,把问题影响范围缩小 - 数组版本的空间利用率更高
因为链表版本还要保存next引用
4,队列模拟实现(基于数组)
接下来就基于数组来模拟实现队列,存储元素类型为String
首先创建个数组,以及head、tail、size这些属性,构造方法无参的默认数组长度为1000,有参的可以自己指定数组长度
public class MyQueueByArray {
private String[] arr = null;
private int head = 0;
private int tail = 0;
private int size = 0;
public MyQueueByArray() {
arr = new String[1000];
}
public MyQueueByArray(int capacity){
arr = new String[capacity];
}
}
offer方法,首先判断下队列是否已经满了,也就是比较size是否等于length,队列满了可以选择扩容(这样就没有控制元素上限的作用了),也可以选择直接抛出异常,
public void offer(String val){
if(size == arr.length){
return;
}
arr[tail] = val;
tail++;
if(tail == arr.length){
tail = 0;
}
size++;
}
poll方法,首先判断下队列是否为空,如果为空就返回null,不为空就返回arr[head],size- -,head++,如果head等于arr.length,就让head等于0,移动到数组头部
public String poll(){
if(size == 0){
return null;
}
String val = arr[head];
head++;
size--;
if(head == arr.length){
head = 0;
}
return val;
}
重写toString方法,创建一个拼接字符串,让cur从head的位置开始,往后移动,每次都把cur的元素添加到拼接字符串中,直到拼接字符串中添加了size个元素,当cur等于arr.length时,就让cur等于0,回到数组开头
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
int cur = head;
for(int i = 0;i < size;i++){
stringBuilder.append(arr[cur]);
cur++;
if(cur == arr.length){
cur = 0;
}
if(i < size - 1){
stringBuilder.append(",");
}
}
stringBuilder.append("]");
return stringBuilder.toString();
}
peek方法就是当队列不为空时,就直接返回arr[head]
public String peek(){
if(size == 0){
return null;
}
return arr[head];
}
接下来聊个题外话,在前面的offer方法中,我们写的是先让tail++,接着判断tail是否等于arr.length,如果等于arr.length了,再让tail = 0
但是铁汁们可能看到过直接求余数的写法(这个写法还是挺流行的),但是这个写法就真的好吗?
tail++;
if(tail == arr.length){
tail = 0;
}
tail = (tail + 1) % arr.length;
tail + 1的意思就是tail++,再对arr.length进行取模运算,当tail + 1等于arr.length的时候,取模后结果会等于0,其他情况取模后结果还是tail + 1;这个写法可能看起来比较牛,一行代码就搞定了,但是这样写真的比第一种写法好吗
评价代码看两个方面:
- 对于开发效率的影响,是否可读性好,是否容易理解,是否好维护
- 对于运行效率的影响,也就是程序的执行效率
取模写法从可读性上看,是不如第一种写法的
不考虑编译器优化的话,第一种写法的执行效率也更高,第一种写法在大部分情况下,cpu进行一次赋值操作,进行一次比较操作即可(tail等于arr.length时,才会进行两次赋值操作) ;而第二种写法,每次都会有赋值操作和取模操作,而取模指令的效率是低于比较指令的,编译器在执行第二种写法时,不会真的去进行取模运算,还是会优化成第一种写法😂
5,用队列实现栈
这个题目没啥实用价值,就是纯考察我们的代码能力,要求使用两个队列来模拟一个栈的功能,队列的特点是先进先出,栈的特点是先进后出
可以这样搞,A和B两个队列,入栈时把元素加到A队列中,元素需要出栈时,就把队列A中的元素逐个取出来,加到队列B中,队列A中剩最后一个元素时,直接删除即可,接着交换下队列A和队列B的指向,后面进行入栈操作还是把元素加到队列A中去
再额外写个swap方法,先创建个tmp引用把引用A保存下来,接着再进行交换
class MyStack {
private Queue<Integer> A = new ArrayDeque<>();
private Queue<Integer> B = new ArrayDeque<>();
public MyStack() {
}
public void push(int x) {
A.offer(x);
}
public int pop() {
while(A.size() > 1){
int n = A.poll();
B.offer(n);
}
int result = A.poll();
swap();
return result;
}
public int top() {
while(A.size() > 1){
int n = A.poll();
B.offer(n);
}
int result = A.poll();
B.offer(result);
swap();
return result;
}
public boolean empty() {
return A.isEmpty();
}
public void swap(){
Queue<Integer> tmp = A;
A = B;
B = tmp;
}
}
6,用栈实现队列

用两个栈来模拟实现队列,也是让元素再两个栈之间来回导,两个栈A和B,入队列时把元素添加到A栈中,需要删除元素时,再把元素从栈中导到B中,这时候B的队首元素就是要删除或者要取出的元素

但是这个题目,如果再交换A和B的引用,就不行了,因为在A中先入队列的元素是在栈底,而在B中先入队列的元素却是在栈底,如果交换引用顺序就乱掉了,因此,还要再把B中的元素再导入到A中,确保原来的顺序不变
后面入队列就还是加到A中,规则就是先入的在栈底,后入的在栈顶
class MyQueue {
private Stack<Integer> A = new Stack<>();
private Stack<Integer> B = new Stack<>();
public MyQueue() {
}
public void push(int x) {
A.push(x);
}
public int pop() {
while(A.size() > 1){
B.push(A.pop());
}
int result = A.pop();
while(B.size() > 0){
A.push(B.pop());
}
return result;
}
public int peek() {
while(A.size() > 0){
B.push(A.pop());
}
int result = B.peek();
while(B.size() > 0){
A.push(B.pop());
}
return result;
}
public boolean empty() {
return A.isEmpty();
}
}
结语
- 队列在数据结构中相对来说是比较简单的,因为只有三个操作,用数组和链表模拟实现下自己的队列,就能很好的掌握队列的用法
- 下篇数据结构博客会解析二叉树,二叉树的知识点相对来说就比较繁杂,大量题目依赖递归,也涉及到一些算法思想,预计会用5篇博客来解析二叉树,欢迎铁汁们来观看后续的二叉树博客🐵
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉



2924



被折叠的 条评论
为什么被折叠?



