【数据结构】初入数据结构的队列(Queue)及其Java实现

初入数据结构的队列(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

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值