2.3 循环队列

       队列的特征是先进先出,有个缩写FIFO,是first input first output的缩写。在实际开发过程中,有许多队列的例子。因为先进先出,就像现实中的排队,所以广泛用于多线程中,用于保证多线程中各个线程的公平性。

       队列的实现有多种形式,比如可以直接使用链表实现队列。其实数组也可以实现,但是数组列表不合适。因为前文介绍过,数组列表从头部移除,性能十分低下。Java在1.2就提供了数组列表的实现ArrayList,但直到JDK 1.6才提供了ArrayDeque以解决头部插入和移除性能低下的问题。所以这里简单介绍下数组队列的实现,让童鞋们明白为什么ArrayDeque是怎么高效地从头部插入和移除数据的。

       前面讲过数组列表使用一个尾部索引来表示尾部位置。那么我们加上一个头部位置呢?那样就可以不频繁挪动数组元素啦,性能将极大地提高。不过这样对数组的利用就不够充分。为什么这样说呢?

       假设这种场景,头部插入异常多,而尾部插入非常少,那么就会导致首尾的不平衡,往头部插入新元素还是会频繁移动数组元素,如以下场景:

       那么怎么办呢?

       我们可以可以把数组的首尾连接起来,形成一个环,如下图所示:

       这就形成了循环队列了。需要注意的是循环队列不仅可以当作队列使用,因为两端都可以增加删除,所以还可以当作栈、环形缓冲(RingBuffer)使用。

       下面简单分析下

       从头部插入的时候,不断减少head,到了0时,就移动到最后一个。比如上图到了0,再往头部插入数据,head就移动到索引11。直到head到了tail+1的位置。

       从尾部插入的时候,不断增加tail,同理,如果tail增加到了11的话,在增加就到了索引0。直到到了head-1的位置。

       因为是个循环,所以要使用模加法/模减法。这里有个细节哈哈,JDK8的循环队列,使用的是位运算,而JDK11使用的是模加法/模减法。

       这里就需要注意边界值的问题。如果head和tail都指向空闲位置。在初始化时,head和tail都指向0。那么插入一个元素之后,需要移动两个指针,程序的判断就复杂了。所以最好的方案是对头尾进行不同的处理,tail指向空闲位置,而head指向已经有元素的位置。

       而删除方法就比较容易了,和数组列表的处理方式一样。下面是我用java写的比较简单的循环队列实现:

package com.youngthing.list.cycle;

import java.util.Arrays;

/**
 * 循环队列
 * created at 11/01/2022
 *
 * @author 俞建波
 * <a href="mailto://yujianbo@chtwm.com">yujianbo@chtwm.com</a>
 * @since 1.0.0
 */
public class CycleList<T> {
    private Object[] data;
    private int head;
    private int tail;

    public CycleList() {
        data = new Object[4];
    }

    public static int decrease(final int i, int modules) {
        int j = i - 1;
        if (j < 0) {
            return j + modules;
        }
        return j;
    }

    public static int increase(final int i, int modules) {
        int j = i + 1;
        if (j >= modules) {
            return j - modules;
        }
        return j;
    }


    public int size() {
        int l = tail - head;
        if (l < 0) {
            l += data.length;
        }
        return l;
    }

    public void addFirst(T e) {
        head = decrease(head, data.length);
        data[head] = e;
        if (head == tail) {
            ensureCapacity();
        }
    }

    public void addLast(T e) {
        data[tail] = e;
        tail = increase(tail, data.length);
        if (head == tail) {
            ensureCapacity();
        }
    }

    public T removeFirst() {
        if (head == tail) {
            throw new IllegalArgumentException("队列为空");
        }
        T e = (T) data[head];
        head = increase(head, data.length);
        return e;
    }

    public T removeLast() {
        if (head == tail) {
            throw new IllegalArgumentException("队列为空");
        }
        T e = (T) data[tail - 1];
        tail = decrease(tail, data.length);
        return e;
    }

    private void ensureCapacity() {
        Object[] array = new Object[(data.length + 1) << 1];
        System.arraycopy(data, 0, array, 0, head);
        int lengthDiff = array.length - data.length;
        System.arraycopy(data, head, array, head + lengthDiff, data.length - head);
        head = head + lengthDiff;
        data = array;
    }
}

       对于循环队列,不建议重复造轮子,以java为例子,Java自带的ArrayDeque代码就相当完善。面试时会问到JDK8与JDK11ArrayDeque的区别,本文不讨论具体语言的细节。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒过来摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值