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

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


  • 队列的基本概念
    • 什么是队列?
    • 队列的先进先出原则
    • 队里的实现方式
  • 顺序队列的概念及Java实现
    • 什么是顺序队列
    • 顺序队列的初级实现
    • 顺序队列的升级版实现
  • 链式队列的概念及Java实现
    • 什么是链式队列?
    • 链式队列的实现


队列的基本概念


什么是队列?

什么是队列?队列也是线性表的一种。队列就是数据与数据之间至少逻辑上相邻,整体呈线性关系,但又遵循先进先出原则的线性表。


队列的先进先出原则(FIFO)

什么是先进先出原则?
数据从某个数据结构的一端存入,另一端取出的原则称为“先进先出”原则。(first in first out,简称“FIFO”

队列的FIFO:


这里写图片描述

如上图,使用队列时,数据只能从队列的队尾插入,从队头取出
就如日常生活中排队买票,先排队(入队列),等自己前面的人逐个买完票,逐个出队列之后,才轮到你买票。买完之后,你也出队列。先进入队列的人先买票并先出队列(不存在插队)。


队列的队尾和队头

队尾:
队尾就是插入数据的一端。就如我们排队时,是队伍的最后面,后面来的人要站在队尾

队头:
队头是数据出列的一端。就如我们排队时,队头就是队伍的最前面,最早完事,结束排队,离开队伍


队里的实现方式

队列跟栈一样,都是线性表的一种,都可以按照线性表的两种存储方式去实现:

  • 顺序列队
    采用顺序存储的且遵循先进先出原则的线性表就是顺序列队
  • 链式列队
    采取链式存储的且遵循先进先出原则的线性表就是链式列队


扫描二维码关注公众号,回复: 1839299 查看本文章

顺序队列的概念及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

猜你喜欢

转载自blog.csdn.net/snailmann/article/details/80549382