初入数据结构的队列(Queue)及其Java实现
- 队列的基本概念
- 什么是队列?
- 队列的先进先出原则
- 队里的实现方式
- 顺序队列的概念及Java实现
- 什么是顺序队列
- 顺序队列的初级实现
- 顺序队列的升级版实现
- 链式队列的概念及Java实现
- 什么是链式队列?
- 链式队列的实现
队列的基本概念
什么是队列?
什么是队列?队列也是线性表的一种。队列就是数据与数据之间至少逻辑上相邻,整体呈线性关系,但又遵循先进先出原则的线性表。
队列的先进先出原则(FIFO)
什么是先进先出原则?
数据从某个数据结构的一端存入,另一端取出的原则称为“先进先出”原则。(first in first out,简称 “FIFO” )
队列的FIFO:
如上图,使用队列时,数据只能从队列的队尾插入,从队头取出。
就如日常生活中排队买票,先排队(入队列),等自己前面的人逐个买完票,逐个出队列之后,才轮到你买票。买完之后,你也出队列。先进入队列的人先买票并先出队列(不存在插队)。
队列的队尾和队头
队尾:
队尾就是插入数据的一端。就如我们排队时,是队伍的最后面,后面来的人要站在队尾
队头:
队头是数据出列的一端。就如我们排队时,队头就是队伍的最前面,最早完事,结束排队,离开队伍
队里的实现方式
队列跟栈一样,都是线性表的一种,都可以按照线性表的两种存储方式去实现:
- 顺序列队
采用顺序存储的且遵循先进先出原则的线性表就是顺序列队 - 链式列队
采取链式存储的且遵循先进先出原则的线性表就是链式列队
顺序队列的概念及Java实现
什么是顺序队列?
顺序队列就是采用顺序存储的队列,在代码实现中,一般用数组作为其数据结构去实现。
顺序队列的头下标和尾下标
因为顺序队列用数组去实习。在队列中,因为我们需要在队头删除数据,再队尾插入数据。所以我们需要两个指针,分别是头指针和尾指针,分别指向队列的队头和队尾,这样我们才能找到队头和队尾去做相应的操作。又因为顺序列队的底层数据结构是数组,所以我们可以直接用数组下标去实现头指针和尾指针。
头下标:
存储的是队头在数组的位置,作用相当于头指针
尾下标:
存储的是队尾在数组到的位置,作用相当于尾指针
顺序队列的初级实现
此为非循环的顺序列队,由数组构建,要注意的地方有几个点:
- 定义头下标和尾下标分别代表队头和队尾在数组的位置
- 队头对应数组头,既[0]位置,队尾对应数组尾
- 头下标和尾下标默认位置为-1,代表空表。当第一次入队时,头下标记得要改为0,不然容易导致错误,因为数组不存在-1的下标
package com.snailmann.datastructure.queue.seqqueue;
/**
* 非循环顺序队列
* 队头对应数组头,队尾对应数组尾
* @author SnailMann
*
*/
public class SeqQueue<T> {
private Object[] element; //存放数据的数组
private int head; //头下标,存放队头在数组的位置
private int tail; //尾下标,存放队尾在数组的位置
private int length; //队列长度
private int size; //队列可使用的最大容量,既数组的大小
/**
* 顺序列队的构造函数
* @param size
*/
public SeqQueue(int size) {
this.element = new Object[size]; //初始化size大小的数组
this.head = -1; //因为是空队列,所以头下标和尾下标暂时为-1,表示空表
this.tail = -1;
this.length = 0;
this.size = size;
}
/**
* 是否为空列队
* @return
*/
public boolean isEmpty(){
return this.head == this.tail; //如果头下标等于尾下标,则表示空表
}
/**
* 返回队列的长度
* @return
*/
public int length(){
return this.length;
}
/**
* 进队,从队尾进队
* @param t
* @throws Exception
*/
public void enQueue(T t) throws Exception{
if(isEmpty()){
this.head = 0; //如果首次入队,此时的头下标能再为默认值-1,而要该为0
}
if(this.tail == this.size - 1) //如果尾下标等于数组大小size - 1,队列可用空间已满
throw new Exception("队列已满,无法再入列");
this.element[++this.tail] = (Object)t; //尾下标位置的下一个位置插入新元素,同时尾下标+1
this.length++; //长度更新
}
/**
* 出列,从队头出列
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public T deQueue() throws Exception{
if(this.head == this.tail && this.head == -1 )
throw new Exception("队列已空,无法再出列");
Object temp = this.element[this.head]; //获得出列数据,等待返回
this.length--;
this.head++; //因为队头对应数组头, 队头下标加1,代表队列中一个元素从队头出列
return (T)temp;
}
/**
* 重写toString方法
*/
public String toString(){
StringBuffer buffer = new StringBuffer("(");
for(int i = this.head; i <= this.tail; i++){
buffer.append(this.element[i].toString());
if(i != this.tail){
buffer.append(",");
}
}
buffer.append(")");
return buffer.toString();
}
public static void main(String[] args) throws Exception {
SeqQueue<Integer> queue = new SeqQueue<>(10);
queue.enQueue(1);
queue.enQueue(2);
queue.enQueue(3);
queue.enQueue(4);
queue.enQueue(5);
queue.enQueue(6);
System.out.println(queue);
System.out.println(queue.length());
queue.deQueue();
queue.deQueue();
queue.deQueue();
System.out.println(queue);
System.out.println(queue.length());
queue.enQueue(7);
queue.enQueue(8);
queue.enQueue(9);
queue.enQueue(10);
System.out.println(queue);
System.out.println(queue.length());
}
}
这种方法是空间缺陷的。比如我们可以看到每次出来,都是想头下标+1,既原头下标的数据是还在的,只是形式上我们把头下标往后移动一位去代表队头出列了一个元素。而且出列数据的数组位置我们是再也用不到了。既出列一次,就失去了一个空间。本身空间就有限,这非常的浪费。所以就有了下面的改进型
顺序队列的升级版实现
纵观我们的上一个顺序列队的初级版本,因为按照先进先出的原则,队列的队尾一直不断的添加数据元素,队头不断的删除数据元素。由于数组申请的空间有限,到某一时间点,就会出现 tail
队列尾指针到了数组的最后一个存储位置,如果继续存储,由于tail
指针无法后移,就会出错。且在数组中做删除数据元素的操作,仅仅只是移动了队头指针head
,实际head前面还有很多空间可以利用。为了充分利用前面的空间,我们可以实现其升级版本:
此为循环顺序列队,采用数组构建,有以下几个要注意的点:
- 未循环的队头和队尾是对应数组头和数组尾
- 头下标和尾下标默认为-1,表示空表。第一次入队数据时头下标更新为0
value MOD size
的方法可以让你返回头数组头部的位置,达到循环的效果这是一种技巧- 判断队列为空就头下标等于尾下标,队列已就队列长度等于数组大小
- 因为这是循环利用空间的顺序队列,所以重写
toString
时,有两种情况,第一种是还没循环利用先前出列的空间。第二种是循环利用先前出列的空间。因为数组非链表,第二种情况,我们需要判断从队头循环到队尾,需要遍历几次。求法是。数组长度 - 队头在数组的第几个位置(头下标 +1) + 队尾在数组的第几个位置(尾下标+1) + 头下标
。为什么最后要加上头下标,因为我们初始遍历的i
就是头下标,为了抵消。
package com.snailmann.datastructure.queue.seqqueue;
import java.util.Arrays;
/**
* 循环顺序队列
*
* @author SnailMann
*
* @param <T>
*/
public class SeqCycleQueue<T> {
private Object[] element; // 存放数据的数组
private int head; // 头下标,存放队头在数组的位置
private int tail; // 尾下标,存放队尾在数组的位置
private int size;
private int length;
/**
* 循环顺序队列的默认构造函数
*
* @param size
*/
public SeqCycleQueue(int size) { // 创建一个空队列
this.element = new Object[size]; // 初始化大小为size的数组
this.head = -1; // 头下标和尾下标默认为-1,代表空队列
this.tail = -1;
this.size = size;
this.length = 0;
}
/**
* 是否为空列队
*
* @return
*/
public boolean isEmpty() {
return this.head == this.tail; // 如果头下标等于尾下标,则表示空表
}
/**
* 返回队列的长度
*
* @return
*/
public int length() {
return this.length;
}
/**
* 进队,从队尾进队
*
* @param t
* @throws Exception
*/
public void enQueue(T t) throws Exception {
if (isEmpty())
this.head = 0; // 如果首次入队,此时的头下标能再为默认值-1,而要是为0,因为此次插入就有第一个元素了
if (this.length == this.size) // 队列长度等于数组大小,则队列已满
throw new Exception("队列已满,无法再入列");
this.tail = ++this.tail%this.size; //为了循环,tail%size就是为了循环,回到数组头部,这种mod的方法是一种技巧
this.element[this.tail] = (Object) t;
this.length++;
}
/**
* 出列,从队头出列
*
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public T deQueue() throws Exception {
if(this.length == 0)
throw new Exception("队列已空,无法再出列");
Object temp = this.element[this.head]; //获得出列数据,等待返回
this.length--;
this.head = ++this.head % this.size; //移动头下标,有了mod计算,就可以回到数组头部,因为head下标也会回到循环的
return (T)temp;
}
/**
* 重写toString方法
*/
public String toString(){
StringBuffer buffer = new StringBuffer("(");
if(isEmpty()){ //如果是空表
return "()";
} else if(this.tail < this.head){ //当队列的队尾循环到数组的前面
//队列长度 - 队头是第几个元素(下标+1) + 队尾是第几个元素(下标+1) + 头下标(因为i也是头下标,为了抵消)
for(int i = this.head;
i <= this.size - (this.head + 1) + (this.tail + 1) + this.head;
i++){
buffer.append(this.element[i%this.size].toString());
if(i%this.size != this.tail){
buffer.append(",");
}
}
buffer.append(")");
} else { //正常情况下,内存还没有分配到垃圾数据区
for(int i = this.head; i <= this.tail; i++){
buffer.append(this.element[i].toString());
if(i != this.tail){
buffer.append(",");
}
}
buffer.append(")");
}
return buffer.toString();
}
public static void main(String[] args) throws Exception {
SeqCycleQueue<Integer> queue = new SeqCycleQueue<>(10);
queue.enQueue(1);
queue.enQueue(2);
queue.enQueue(3);
queue.enQueue(4);
queue.enQueue(5);
queue.enQueue(6);
System.out.println(queue);
System.out.println(queue.length());
queue.deQueue();
queue.deQueue();
queue.deQueue();
System.out.println(queue);
System.out.println(queue.length());
queue.enQueue(7);
queue.enQueue(8);
queue.enQueue(9);
queue.enQueue(10);
queue.enQueue(11);
queue.enQueue(12);
queue.enQueue(13);
queue.deQueue();
queue.deQueue();
queue.deQueue();
queue.deQueue();
queue.deQueue();
queue.deQueue();
queue.deQueue();
queue.deQueue();
queue.enQueue(1);
queue.enQueue(2);
queue.enQueue(3);
System.out.println(queue);
System.out.println(queue.length());
}
}
链式队列的概念及Java实现
什么是链式队列?
链式队列就是采用了链式存储且遵循先进先出原则的线性表
链式队列的实现
链式队列和顺序队列在实现上的不同:
链式队列就不需要考虑顺序队列是否循环利用出列的空间,因为链式队列是需要空间时分配,不需要则删除(交给JVM自行GC),不存在限制区域。
链式队列用代码实现要注意的几个点:
-
用链表的首元结点一端表示队列的队头,链表尾结点表示队列的队尾,这样的设置会使程序更简单。反过来的话,队列在增加元素的时候,要采用头插法,在删除数据元素的时候,由于要先进先出,需要删除链表最末端的结点,就需要将倒数第二个结点的next指向NULL,这个过程是需要遍历链表的。
-
因为链式队列是无头结点的,所以入列是分两种情况,第一种是空队列数据入列。第二种是非空队列数据入列
-
出列则要考虑是否还有数据可以出列,既队列是否已空
/**
*
* 链式队列(无头结的非循环链表)
* @author SnailMann
*
* @param <T>
*/
public class LinkedQueue<T> {
//在队列层面,头指针指向队头,尾指针指向队尾。在链表层面,头指针指向首元结点,尾指针指向尾结点
private Node<T> head,tail;
/**
* 默认构造方法,构造空链式队列
*/
public LinkedQueue() { //构造空队列
this.head = null; //因为是空队列,且无头结点,所以头尾指针指向null
this.tail = null;
}
/**
* 是否为空列队
*
* @return
*/
public boolean isEmpty() {
return this.head == null && this.tail == null; //队头指向Null或队尾指向null,则空表
}
/**
* 返回队列的长度
*
* @return
*/
public int length() {
int len = 0;
Node<T> node = this.head; //获得首元结点(队头)
while(node != null){
node = node.next;
len++;
}
return len;
}
/**
* 进队,从队尾进队
*
* @param t
* @throws Exception
*/
public void enQueue(T t) throws Exception {
Node<T> node = this.tail; //获得尾结点(队尾)
if(this.head == null) { //如果是初次入队,队头更新,队尾更新
this.head = new Node<T>(t,null);
this.tail = this.head;
} else {
node.next = new Node<T>(t,null); //尾结点的指针域指向新入列结点
this.tail = node.next; //更新尾指针为新入列结点
}
}
/**
*
*
* 出列,从队头出列
*
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public T deQueue() throws Exception {
if(isEmpty())
throw new Exception("空队列,已无法出列");
Node<T> node = this.head; //获得首元结点(队头)
this.head = node.next; //更新头指针,头指针指向原首元结点的后继结点,既队头出列,其后继结点成为新的队头
return node.data; //返回出列的数据域
}
/**
* 重写toString方法
*/
public String toString(){
StringBuffer buffer = new StringBuffer("(");
Node<T> node = this.head; //获得首元结点,队头
while(node != null){ //队头不为空
buffer.append(node.data.toString());
if(node.next != null) //除尾结点外,其他都加,
buffer.append(",");
node = node.next;
}
buffer.append(")");
return buffer.toString();
}
public static void main(String[] args) throws Exception {
LinkedQueue<Integer> queue = new LinkedQueue<>();
queue.enQueue(1);
queue.enQueue(2);
queue.enQueue(3);
queue.enQueue(4);
queue.enQueue(5);
queue.enQueue(6);
System.out.println(queue);
System.out.println(queue.length());
queue.deQueue();
queue.deQueue();
queue.deQueue();
System.out.println(queue);
System.out.println(queue.length());
queue.enQueue(7);
queue.enQueue(8);
queue.enQueue(9);
queue.enQueue(10);
System.out.println(queue);
System.out.println(queue.length());
}
}
参考资料
首先强烈推荐学习数据结构的朋友,直接去看该网站学习,作者是@严长生。里面的资料是基于严蔚敏的《数据结构(C语言版)》,本文的概念知识都是基于该网站和《数据结构(C语言版)》这个书来分析的。
数据结构概述 - 作者:@严长生
代码实现部分,主要根据自己的实现再参考大佬,发现不足,加以修改的。(原作更好!!)
Github - doubleview/data-structure