玩转数据结构之循环队列

0. 序言

循环队列是为了解决上一节中从队列队首取出数据的时间复杂度是O(n)的问题。我们再看下这个问题:


删除队首元素

搬移数据

由于删除数组队首的数据,需要搬移数据,所以时间复杂度为O(n),而从队列的队首取出数据本身就很频繁,导致性能低下。

除了介绍循环队列,还会用模拟代码来对比循环队列和数组队列关于运行时间方面的差异。

1. 解决方案

循环队列

假如取出队首的数据后,不搬移数据,而是记录下队首的元素,这样性能自然就提高了。这里front指的是队首元素,而tail指的是下次从队尾添加元素所指向的位置。

2. 实现原理

其实有出队和没有出队的情况下,原理是一样的,但是明白这个原理,需要把两种情况都说清楚,不然你可能一头雾水。

  • 第一种情况:有出队的操作


    无数据时
  1. 当无数据时,即数组为空的时候,front和tail都指向数组的第一个元素,即索引为0的位置。所以当front==tail时表明数组为空。


    第一个数据入队

    第二个数据入队
  2. 当数据入队以后,tail往后移动一位。当又有数据入队以后,tail再往后移动一位。以此类推。


    队首元素出队
  3. 假如队首元素出队,front只需要往后移动一位即可。此时取出队首元素的操作的时间复杂度为O(1)


    当最后一个索引有元素时
  4. 当元素插入最后一个索引的时候,由于front前面有可利用的两个空间,所以此时tail指向索引为0的位置.


    最后一个索引已经有元素时,再次放入元素
  5. 再次放入元素,此时元素会放在tail之前指向的索引为0的位置。tail往后移动一位,指向索引为1的地方.


    此时数组满

当(tail + 1 ) % capacity == front 时,队列满。

注意这个时候浪费了一个空间,即这个数组还剩下一个空间的时候进行扩容。为什么要浪费这样一个空间呢?因为此时如果再添加元素,插入到索引为1的地方,那么tail就要指向索引为2的地方,由于front此时也指向索引为2的地方,且front == tail的时候我们判定数组为空,所以浪费一个空间,是为了把数组满和数组空的情况区别开来。

而这个浪费的空间,我们在初始化数组的时候,把容量设置为需求的最大容量+1即可,这样既然满足需求,又能很好的利用这个额外的1个空间,达到循环队列的目的。

扩容_有出队

① 原数组data,新数组newData
② front == tail 队列为空 (tail+1)% capacity == front 队列满
③ size 是原数组的元素个数 = 7 需求容量7 数组容量8(浪费一个空间)
④ 扩容:原数组的元素从队首front指向的元素开始到索引不等于tail的元素复制到新的数组中,此时front = 0 tail = size;扩容容量 = 需求容量7 ×2 +1 = 15 。

  • 没有出队的操作


    扩容_无出队

    与有出队不同的是,data中的tail指向的是最后一个索引。虽然如此,公式front == tail 以及(tail + 1)% capacity == front 队列满 未发生变化。

3. 代码

因为要实现循环队列,而队列的底层我们用的数组,所以不能使用之前的动态数组类Array,我们要用一个循环数组来实现循环队列LoopQueue:

  • 定义队列接口类Queue
public interface Queue<E> {
    void enqueue(E e);
    E dequeue();
    E getFront();
    int getSize();
    boolean isEmpty();
}
  • 定义实现类LoopQueue:
public class LoopQueue<E> implements Queue<E> {

    private E[] data;
    private int front,tail;
    private int size;

    public LoopQueue(int capacity) {
        // 1:创建的数组的大小 = 调用者需要的数组的容量+1
        data = (E[]) new Object[capacity+1];
        front = 0;
        tail = 0;
        size = 0;
    }

    public LoopQueue(){
        this(10);
    }

    // 2:能容纳数据的数量 = 数组容量 -1;
    public int getCapacity(){
        return data.length - 1;
    }

    // 3: 入队
    @Override
    public void enqueue(E e) {
        // 4:队列已经满的情况下
        if ((tail +1) % data.length == front)
            resize(getCapacity() * 2);
        // 7:原队列没有满的情况下和原队列满了且扩容后:
        data[tail] = e;
        tail = (tail + 1) % data.length;
        size++;
    }

    private void resize(int newCapacity){
        // 5:每次创建数组都要比最大容量 + 1
        E[] newData = (E[]) new Object[newCapacity+1];
        for (int i = 0; i < size ; i++)
            // 6: newData[0] = data[front] front可能为0也可能不为0
            // newData[1] = data[front +1] newData[2] = data[front +2]
            // 假设数组data.length = 8 索引0~7 共有元素size = 3 此时front = 6
            // 此时从队首到队尾的元素在数组中的索引是 6和7和0,而这个0 = (2+6)%8 此时2真的是这里的i
            newData[i] = data[(i + front) % data.length];
        data = newData;
        front = 0;
        tail = size;
    }

    @Override
    public E dequeue() {
        if (isEmpty())
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        E ret = data[front];
        data[front] = null;
        // 8 front往后移动一位(当有出队,插入元素的位置的索引小于front之前指向的索引时,需要 % 
        // data.length
        front = (front + 1) % data.length;
        size --;
        if (size == getCapacity() /4 && getCapacity()/2!=0)
            resize(getCapacity()/2);
        return ret;
    }


    @Override
    public E getFront() {
        if (isEmpty())
            throw new IllegalArgumentException("Queue is empty.");
        return data[front];
    }

    // 3:定义的是数组中元素的数量
    @Override
    public int getSize() {
        return size;
    }

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

    @Override
    public String toString() {
        StringBuffer res = new StringBuffer();
        res.append(String.format("Queue: size = %d,capacity = %d\n", size, getCapacity()));
        res.append("front [");
        // 9: 也可以用resize中的遍历方式
        for (int i = front; i !=tail; i = (i +1) % data.length) {
            res.append(data[i]);
            if ((i + 1) % data.length != tail)
                res.append(", ");
        }
        res.append("] tail");
        return res.toString();

    }
}

① 代码1:
创建的数组的大小 = 需求容量+1 (这个1是需要浪费的空间);数组为空的时候front = 0,tail = 0,size = 0。
② 代码2:
查询数组最大容量 = 数组容量 - 1(这个1不能算是有效容量);注意,这里的capacity和实现原理图中的容量capacity不一样,这里指的是有效容量,这里的data.length == 实现原理图中的容量。
③ 代码3:
定义的是数组中元素的数量。
④ 代码4:
数组满的时候,如果还想插入元素,先扩容。
⑤ 代码5:
扩容:数组的容量(data.length) = 原数组有效容量(getCapacity()) × 2 + 1
缩容:数组的容量(data.length) = 原数组有效容量(getCapacity()) / 2 + 1
⑥ 代码6:
newData[0] = data[front] (front可能为0也可能不为0)
newData[1] = data[front +1]
newData[2] = data[front +2]
假设data.length = 8 索引:0~7 共有元素size = 3 此时front = 6
此时从队首到队尾的元素位于数组中的索引是 6 和 7 和 0 , 而这个0 = ( 2 + 6 ) % 8 此时2就是for循环中的 “ i " 。
⑦ 代码7:
处理原队列没有满的情况下和原队列满了且扩容后,新元素的插入操作。这两种情况执行的操作其实也一样,都是往tail指针指向的索引位置。
⑧ 代码8:
front往后移动一位(当有出队,插入元素的位置的索引小于front之前指向的索引时,需要 % data.length):


出队

⑨ 代码9:
也可以使用resize中的for循环替代。其实意思一样。这里的front就是索引值,所以这里的i也是索引,起始索引为front,当i == tail的停止循环,每次 i 变化遵循 “i = (i +1) % data.length"变化趋势。

(i + 1) % data.length != tail 指这个条件下索引i里面的元素不是队尾元素。

  • 测试
public class Test_LoopQueue {
    public static void main(String[] args) {
        LoopQueue<Integer> queue = new LoopQueue<>();
        for (int i = 0; i < 10; i++) {
            queue.enqueue(i);
            System.out.println(queue);

            if (i % 3 ==2){
                queue.dequeue();
                System.out.println(queue);
                System.out.println("---------------------------------");
            }
        }
    }
}
Queue: size = 1,capacity = 10
front [0] tail
Queue: size = 2,capacity = 10
front [0, 1] tail
Queue: size = 3,capacity = 10
front [0, 1, 2] tail
Queue: size = 2,capacity = 5
front [1, 2] tail
---------------------------------
Queue: size = 3,capacity = 5
front [1, 2, 3] tail
Queue: size = 4,capacity = 5
front [1, 2, 3, 4] tail
Queue: size = 5,capacity = 5
front [1, 2, 3, 4, 5] tail
Queue: size = 4,capacity = 5
front [2, 3, 4, 5] tail
---------------------------------
Queue: size = 5,capacity = 5
front [2, 3, 4, 5, 6] tail
Queue: size = 6,capacity = 10
front [2, 3, 4, 5, 6, 7] tail
Queue: size = 7,capacity = 10
front [2, 3, 4, 5, 6, 7, 8] tail
Queue: size = 6,capacity = 10
front [3, 4, 5, 6, 7, 8] tail
---------------------------------
Queue: size = 7,capacity = 10
front [3, 4, 5, 6, 7, 8, 9] tail

针对这个测试就不再赘述。至此,我们实现了出队的时间复杂度为O(1)。那循环数组出队的时间复杂度O(1)比数组队列出队的时间复杂度O(n)到底性能上高出多少,下一篇文章讲述。

4. 对比数组队列

public class Test_Loop2Array {

    // 测试queue分别运行count个enqueue和dequeue操作所需要的时间,单位:秒
    private static double testQueue(Queue<Integer> queue, int count) {

        long startTime = System.nanoTime();

        Random random = new Random();
        for (int i = 0; i < count; i++) {
            queue.enqueue(random.nextInt(Integer.MAX_VALUE));
        }
        for (int i = 0; i < count; i++) {
            queue.dequeue();
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;
    }

    public static void main(String[] args) {
        int count = 100000;

        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        double time1 = testQueue(arrayQueue, count);
        System.out.println("ArrayQueue,time: " + time1 + " s");

        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double time2 = testQueue(loopQueue, count);
        System.out.println("LoopQueue,time: " + time2 + " s");
    }
}
ArrayQueue,time: 2.985780695 s
LoopQueue,time: 0.011671535 s

你会发现循环队列在运行时间方面比数组队列所耗费的时间要少很多,足见其优异性。

5. 后续

如果大家喜欢这篇文章,欢迎点赞!
如果想看更多 数据结构 方面的文章,欢迎关注!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,735评论 0 13
  • 本文内容取自于小甲鱼的数据结构与算法。http://www.jianshu.com/p/230e6fde9c75 ...
    阿阿阿阿毛阅读 2,884评论 0 7
  • 前言: 数据结构是计算机相关专业的基础课程,不管学什么编程语言,都要学习数据结构。接下来就一起来了解一下吧。 欢迎...
    贪挽懒月阅读 709评论 2 17
  • 1. 阴天 窗外 雪 2. 这样景象的天空 灰蒙蒙的 遮住你所有的心思 像过去的一切不曾发生 又似一切就发生在昨天...
    大小简阅读 106评论 0 0
  • 车入南站已夜半, 汗出薄衫却晕眩, 本来约在大厅里, 谁知她到楼外边。 北门许进不许出, 拖箱小跑绕大圈。 上下九...
    云逸1108阅读 51评论 0 1