详解队列Queue

本文介绍了队列的基本概念和特性,包括先进先出(FIFO)原则。讨论了基于动态数组和静态数组的两种队列实现方式。动态数组实现的队列在出队时可能存在效率问题,而循环数组通过巧妙的头尾指针管理,实现了出队操作的时间复杂度降为O(1)。此外,文章还详细阐述了循环队列的扩容和缩容策略以及满队列的判断条件。
摘要由CSDN通过智能技术生成

同栈类似,队列也是一种受限的线性表。其中受限指队列的操作是线性表的子集,它要求所有元素必须从一端插入新元素,从另外一端删除元素。添加元素的一端叫队尾 ,删除元素的一端叫队首。

图1 队列逻辑模型

根据底层使用的数据容器的不同,队列可分为以下两大类

  • 顺序队列:使用数组作为数据容器
  • 链式队列:使用链表作为数据容器

队列的逻辑定义是其具备先进先出(first in first out,FIFO)的特点,这种特点决定了队列的应用场景。

队列接口

根据队列的描述,可知队列的操作包括出队、入队、查看队首、获取队列大小、判空操作。其接口设计如下。

/**
 * 队列接口,同java.util.Queue接口名称不同
 * @param <E>
 */
public interface Queue<E> {
     void enqueue(E e);
     E dequeue();
     E getFront();
     int getSize();
     boolean isEmpty();
}

基于动态数组的队列

当使用数组作为数据容器时,数组索引为0的元素是队首元素,数组最后一个有效值是队尾元素。本节使用动态数组作为队列底层存储数据的容器。其中,其中动态数组的实现参考实现java动态数组

/**
 * 基于动态数组实现队列
 * 数组的第一个元素arr[0]是队首,最后一个元素arr[size-1]是队尾
 * @param <E>
 */
public class ArrayQueue<E> implements Queue<E> {
    private Array<E> arr;
    public ArrayQueue() {
        arr = new Array<E>();
    }
    public ArrayQueue(int capacity){
        arr = new Array<>(10);
    }

    /**
     *入队,在动态数组尾部添加元素,可能触发数组扩容
     * 但考虑复杂度均摊,其最终复杂度为O(1)
     * @param o
     */
    @Override
    public void enqueue(E o) {
        arr.addLast(o);
    }

    /**
     * 出队,在动态数组首元素arr[0]移除元素,可能触发缩容操作
     * 同时,移出第一个元素,后续所有元素都要移动,
     * 因此其复杂度为O(n)
     * @return
     */
    @Override
    public E dequeue() {
        return arr.remove(0);
    }

    /**
     * 获取队首元素,不出队
     * 由于数组随机访问特点,
     * 其时间复杂度为O(1)
     * @return E
     */
    @Override
    public E getFront() {
        return arr.getFirst();
    }

    /**
     * 获取当前队列已有大小
     * 由于数组随机访问特点,
     * 其时间复杂度为O(1)
     * @return int
     */
    @Override
    public int getSize() {
        return arr.getSize();
    }

    /**
     * 判空
     * 由于数组随机访问特点,
     * 其时间复杂度为O(1)
     * @return boolean
     */
    @Override
    public boolean isEmpty() {
        return arr.isEmpty();
    }

    @Override
    public String toString() {
        StringBuilder stringBuild = new StringBuilder();
        stringBuild.append("arrayQueue top is [");
        for (int i = 0; i < arr.getSize(); i++) {
            stringBuild.append(arr.get(i));
            if(i != arr.getSize() -1){
                stringBuild.append(",");
            }
            if(i == arr.getSize() -1){
                stringBuild.append("] end");
            }
        }
        return stringBuild.toString();
    }
}

基于静态数组的循环队列

上一小节使用动态数组实现的队列在出队时,在动态数组移除首元素arr[0]时,存在两个问题

  1. 可能触发缩容操作。
  2. 移出第一个元素,数组后续所有元素都要移动重排,造成出队操作复杂度为O(n)

为解决基于动态数组的队列出队操作渐进时间复杂度O(n)的问题,本节介绍一种基于静态数组实现的队列。该队列同样使用数组索引为0的元素是队首元素,数组最后一个有效值是队尾元素,需要维护以下变量:

  • 静态数组data:用于存储队列元素
  • 头指针front:指向队首(已有元素位置)。指向已有元素的位置
  • 尾指针tail:指向队尾(下一个元素要存储的位置,第一个为空的位置)。指向没有元素的位置
  • 队列容量size: 表示当前队列已使用的容量,不是队列的总容量

在刚创建队列后,队列的头尾指针相等,都指向下标为0的位置。并且,头尾指针相等也是判断队列为空的唯一条件。即当且仅当队列为空,即数组为空时,头指针和尾指针指向同一个位置front == tail.

图2 队列初始状态图

对于入队操作,即数组添加元素时,尾指针后移一位。

图3 入队示意图

对于出队操作,即数组删除元素时,头指针后移一位。这种出队方式只需移动一下头指针,使出队操作的时间复杂度由O(n)降为O(1),解决了基于动态数组的队列出队操作渐进时间复杂度O(n)的问题。

图4 出队示意图

循环的来由: 当尾指针指向数组最后一个元素,且头指针指向的数组位置索引大于0(此时数组的剩余容量>1)时,当一个元素入队时,只能进入尾指针位置。入队之后,尾指针重新指向数组第一个元素的位置。需要特别指出的是,尾指针下一个指向的数组位置的计算方式为 (i + 1) % arr.length。

图5 尾指针循环示意图

假设循环队列一直入队,出现以下情况。则再入队一个元素,就会出现front ==tail,即可队列为空的判断条件一致,此时就无法区分队列时空还是满,因此当只剩一个元素容量时(浪费一个空间),不再入队,表示队列已满,即tail +1 = front表示队列已满,此时需要触发扩容操作。由于循环队列,因此队列已满的通用表达式为 (tail +1)% capacity = front

图6 循环队列已满示意图

循环队列代码实现

循环队列的代码实现如下

/**
 * 基于数组实现的循环队列
 */
public class LoopQueue<E> implements Queue<E> {
    //不能使用动态数组,因为动态数组只有一个游标,这个游标相当于循环队列的尾指针
    //逻辑上这是一种普通队列,并没有实现循环
    //private Array<E> arr;

    private E[] data ;
    //头指针 头指针front指向队首(已有元素位置);
    private int front;
    //尾指针.尾指针tail指向队尾(下一个元素要存储的位置,第一个为空的位置,没有元素的位置)。
    private int tail ;
    //当前已有容量
    private int size;

    public LoopQueue(){
        this(10);
    }

    public LoopQueue(int capacity){
        //capacity是用户传递进来的目标容量,而循环队列已满时,会浪费一个容量
        //因此想存储capacity个元素需要capacity + 1个空间
        // 因此构造的数组容量 + 1
        //2、涉及了创建泛型数组的变相方式
        data = (E[])new Object[capacity + 1];
        front = 0;
        tail = 0;
        size = 0;
    }

    @Override
    public void enqueue(E e) {
        if((tail + 1) % data.length == front){
            resize(getCapacity() * 2);
        }
        data[tail] = e;
        tail = (tail + 1) % data.length;
        size++;
    }

    /**
     * 扩容或缩容
     * @param newCapacity
     */
    private void resize(int newCapacity){
        E[] newData = (E[])new Object[newCapacity + 1];
        for (int i = 0; i < size; i++) {
            //front +i有意思
            //当数组循环起点不是从0开始时,可将其拆分为 basePoint + i(基本坐标 + 步长)
            newData[i] = data[(front + i) % data.length];
        }
        data = newData;
        front = 0;
        tail = size;

    }
    @Override
    public E dequeue() {
        if(isEmpty()){
            throw  new IllegalArgumentException("数组为空");
        }
        E e = data[front];
        //置空 使其被gc回收
        data[front] = null;
        front = (front + 1) % data.length;
        size--;
        //缩容
        if(size == getCapacity() / 4 && getCapacity() / 2 != 0){
            resize(getCapacity() / 2);
        }
        return e;
    }

    @Override
    public E getFront() {
        if(isEmpty()){
            throw  new IllegalArgumentException("数组为空");
        }
        return data[front];
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return  front == tail;
    }

    public int getCapacity(){
        //逻辑设计上要浪费一个数组空间
        // 减1操作是因为在定义队列时,底层数组的长度已加1
        return data.length - 1;
    }
    @Override
    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("loopqueue top is [");
        for (int i = 0; i < size ; i++) {
            //此处不能使用getCapacity()作为数组的长度,这个方法是暴露给外部用户使用的,
            //其值比数组长度少1,而此处的取模操作是基于真实的数组长度
            //getCapacity()是上层接口,逻辑层面的。
            // 而data.length是内部实现,底层数据容器支撑。两者不在一个层面
            //stringBuilder.append(data[(front + i) % getCapacity()]);
            stringBuilder.append(data[(front + i) % data.length]);
            //与上一行同样的道理
            //if((front + i + 1) % getCapacity() != tail) {
            if((front + i + 1) % data.length != tail) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("]end");
        return  stringBuilder.toString();
    }
}

延申

  • 在有限空间,出现循环多使用取模操作。取模结果可能等于0,这一点似乎也呼应数组从0计数

参考资料

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值