重温:数据结构与算法 - 07队列

xwzz.jpg

重温:数据结构与算法 - 开篇
重温:数据结构与算法 - 复杂度分析(一)
重温:数据结构与算法 - 复杂度分析(二)
重温:数据结构与算法 - 数组
重温:数据结构与算法 - 链表(一)
重温:数据结构与算法 - 链表(二)
重温:数据结构与算法 - 栈

叨叨两句

最近提了离职,给公司找交接人也面试了不少人,上次作为面试官还是两年前事情;
最大感受大环境还是太过浮躁,五年前如此,今亦是如此,福报996,中年危机,卷王争霸,各种负面信息缠绕在周围;
作为个人,没资格去评论他人的职业规划,给他人灌输鸡汤,更是害人不浅。仅告诫自己:静心、学习、自省、前行。

什么是队列

言归正传,本章介绍另一种“操作受限”的线性表数据结构:队列

结构相同,它仅支持添加和删除操作:

  • 添加操作(入队 - enqueue)
  • 删除操作(出队 - dequeue)

不同点在于与栈的先进后出相反,它具有"先进先出"特性:

排队.png

图中,排队买票就是典型的队列结构,先排队的人优先买到票。

队列结构.png

我们把入队操作称之"enqueue()",出队操作则为"dequeue()",一个简单的队列操作约束大致如下:

// 队列基本操作约束接口
public interface Queue<T> {

    // 入队
    boolean enqueue(T item);

    // 出队
    T dequeue();

    // 是否为空
    boolean isEmpty();

    // 队中数据的数量
    int size();

    // 清空
    void clear();
}

一个满足先进先出的队列容器,其关键在于enqueue()dequeue()函数的实现上,而基础容器的选择同上篇的栈结构一样,使用数组或者链表都可以,使用数组实现队列称之:顺序队列,使用链表实现则为:链式队列
对于资源限制严谨的场景下推荐使用顺序队列,而对资源限制没有要求,仅需先进后出特性的场景可使用链式队列。

顺序队列(数组)

以下是以数组实现顺序队列的思路,大家不妨看看:

顺序队列:

  • 1、定义一个items[]数组来存储数据;

  • 2、定义一个head指针和tail指针,分别指向当前队列的首下标和尾下标;

  • 3、入队操作enqueue()中,校验队列是否已满:

    • 已满 ,返回false,入队失败;
    • 未满,将数据存储到队尾(tail指针位置),且tail指针后移一位 ;
  • 4、出队操作dequeue()中,校验队列是否已空:

    • 已空:返回null
    • 非空:返回队首数据(head指针位置),且将head指针后移一位。

按照这个思路,我把实现代码贴在了下方:

public class ArrayQueue<T> implements Queue<T> {

    private static final int DEFAULT_SIZE = 10;

    // 队列中数据数量
    private int count;

    private Object[] items;

    // 当前队列能存储的最大容量
    private int maxSize;

    // head指向队首下标 , tail指向队尾下标
    private int head = 0, tail = 0;

    public ArrayQueue() {
        this(DEFAULT_SIZE);
    }

    public ArrayQueue(int intSize) {
        items = new Object[intSize];
        this.maxSize = intSize;
    }

    @Override
    public boolean enqueue(T item) {
        // 队列满了,存储失败
        if (tail == maxSize) return false;

        // 存储,队尾下标后移一位
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

    @Override
    public T dequeue() {
        // 队空,返回null
        if (isEmpty()) return null;

        // 取出数据,队首下标后移一位
        Object item = items[head];
        ++head;
        --count;
        return (T) item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

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

    @Override
    public void clear() {
        if (isEmpty()) return;
        for (int i = 0; i < size(); i++) {
            items[i] = null;
        }
        count = 0;
    }


    public void print() {
        for (int i = head; i < tail; i++) {
            System.out.print(items[i] + "\t");
        }
        System.out.println();
    }
}

测试一下:

ArrayQueue<String> queue = new ArrayQueue<>(3);
queue.enqueue("1");
queue.enqueue("2");
queue.enqueue("3");
queue.enqueue("4");
queue.enqueue("5");
queue.print();
String data1 = queue.dequeue();
System.out.println("出队:" + data1);
String data2 = queue.dequeue();
System.out.println("出队:" + data2);
queue.print();
System.out.println("大小:" + queue.size());

// 问题:队列有空闲空间却无法添加数据
System.out.println(queue.enqueue("6"));
queue.print();
System.out.println("大小:" + queue.size());

输出:

1   2   3   
出队:1
出队:2
3   
大小:1

false
3   
大小:1

通过测试,我们创建了一个大小为3的队列,并一口气增加了5个元素,因队列只能容纳3个元素,后续添加的4、5元素都失败了;紧接出队两个元素1、2,队列最终只剩下元素3,满足队列的先进先出原则。

然而,当我想尝试继续添加元素6时,却失败了!队列当前大小依旧是1,但我们知道队列总大小明明是3,却不能继续向队列添加数据,显然这并不是我想要的结果,通过下图我们可以看到原因:

入队失败.png

当我们在第一步入队操作后,tail指针就已到队尾,无论第二步出队多少个元素,tail指针始终没有被重置,显然这是导致此问题的根本原因。

如何解决?
数据搬移!没错,在数组章节提到过插入一个数据进数组中,需要将插入位置及后面所有的数据都向后搬移一位;同理,当队列中队首元素出队时,将队首之后的所有元素向前搬移一位,并将tail--,这样就可以保证tail指针始终指向的是队内实际尾元素下标,而不是队列实际大小的尾下标。

按照上面思路,的确解决了问题,不过每次出队都需要进行一次数据搬移,这使得dequeue()函数时间复杂度由原先的 O(1) --》O(n).

有没有更好的方案?
换个角度,来看看enqueue()入队函数,还是上面问题,当我们执行第3步,入队元素6时,由于tail指针指向了队尾,入队失败。但通过上图可以看到head指针前是存在空闲空间的,此时我们进行数据搬移可否?

已上图为例,当执行第三步时,tail指针指向队尾,但head指针并不在队首,那么将3搬移至下标为0的位置,重置head、tail指针位置,再入队元素6,按照这个思路修改enqueue()函数如下:

    public boolean enqueue(T item) {
        // 队列到达末尾
        if (tail == maxSize) {
            // 队列满了,存储失败
            if (head == 0) return false;

            // 队列头部有空闲空间,进行数据搬移
            for (int i = head; i < tail; i++) {
                items[i - head] = items[i];
            }
            // 修正head、tail下标
            tail = tail - head;
            head = 0;
        }

        // 存储,队尾下标向后移动一位
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

修改后的enqueue()函数时间复杂度,又是多少呢?分析如下:

  • 当tail未达尾部时,可直接入队,时间复杂度:O(1)
  • 当tail达到尾部时,队首有空余空间,进行数据搬移,时间复杂度:O(n)
  • 每次入队操作,tail可能在0n任意位置,0n-1位置的时间复杂度为O(1),在n位置的时间复杂度为O(n),可将这n次搬移操作平摊给前n-1次操作,所以最终平均时间复杂度为:O(1)

链式队列(链表)

OK,接下来学习链式队列:
链式队列相较顺序队列简单很多,不需关心存储空间不够,或数据搬移问题。

链式队列:

  • 1、定义一个head指针和tail指针,用于指向当前队列的首结点和尾结点;
  • 2、入队操作enqueue()时,只需tail.next -> newNode , tail -> tail.next
  • 3、出队操作dequeue()时,head -> head.next
public class LinkedQueue<T> implements Queue<T> {

    private Node<T> head, tail;
    private int count = 0;

    @Override
    public boolean enqueue(T item) {
        Node<T> newNode = new Node<>(item);
        if (head == null) {
            head = tail = newNode;
        } else {
            tail.next = newNode;
            tail = tail.next;
        }
        count++;
        return true;
    }

    @Override
    public T dequeue() {
        if (head == null) return null;
        T item = head.data;
        head = head.next;
        count--;
        return item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

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

    @Override
    public void clear() {
        head = null;
    }

    public void print() {
        Node<T> temp = head;
        while (temp != null) {
            System.out.print(temp.data + "\t");
            temp = temp.next;
        }
        System.out.println();
    }

    static class Node<T> {

        public T data;          // 存储数据
        public Node<T> next;    // 下一个结点

        public Node(T data) {
            this.data = data;
        }

    }
}

测试:

        LinkedQueue<String> queue = new LinkedQueue<>();
        queue.enqueue("1");
        queue.enqueue("2");
        queue.enqueue("3");
        queue.enqueue("4");
        queue.enqueue("5");

        queue.print();

        System.out.println("出队:" + queue.dequeue());
        System.out.println("出队:" + queue.dequeue());
        System.out.println("出队:" + queue.dequeue());

        queue.print();

        queue.enqueue("6");

        queue.print();

输出:

出队:1
出队:2
出队:3
4   5   
4   5   6   

可以看到链式队列,无论是入队enqueue()还是出队dequeue(),都可通过指针直接访问到对应结点,所以链式队列的出队入队时间复杂度都是:O(1)

进阶

循环队列

前面提到,在内存资源有限的场景下,推荐使用顺序队列,反之可使用链式队列。如果在有限资源下,又希望能拥有像链式队列一样高效的入队操作,这样的队列能否实现?

循环队列.png

如图所示,我们将数组的首尾相连:

  • 入队时,当数据填充到尾部且继续填充时,重置tail指针指向下标为0的内存空间,诺该空间处在空闲状态则入队成功,否则已满入队失败;
  • 出队时,通过公式head = ( head + 1 ) % maxSize 来计算实际的head指针位置。
/**
 * 循环队列
 */
public class CircularQueue<T> implements Queue<T> {

    private static final int DEFAULT_SIZE = 10;

    // 队列中数据数量
    private int count;

    private Object[] items;

    // 当前队列能存储的最大容量
    private int maxSize;

    // head指向队列头部下标 , tail指向尾部下标
    private int head = 0, tail = 0;

    public CircularQueue() {
        this(DEFAULT_SIZE);
    }

    public CircularQueue(int intSize) {
        items = new Object[intSize];
        this.maxSize = intSize;
    }

    @Override
    public boolean enqueue(T item) {
        // 队满
        if (count == maxSize) return false;
        // tail到达队尾,
        if (tail == maxSize) tail = 0;
        // 存储,队尾下标向后移动一位
        items[tail] = item;
        ++tail;
        ++count;
        return true;
    }

    @Override
    public T dequeue() {
        // 队空,返回null
        if (isEmpty()) return null;
        // 取出数据,队头下标向后移动一位
        Object item = items[head];
        head = (head + 1) % maxSize;
        --count;
        return (T) item;
    }

    @Override
    public boolean isEmpty() {
        return count == 0;
    }

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

    @Override
    public void clear() {
        if (isEmpty()) return;
        for (int i = 0; i < size(); i++) {
            items[i] = null;
        }
        count = 0;
    }


    public void print() {
        for (int i = head, j = 0; j < count; i = (i + 1) % maxSize, j++) {
            System.out.print(items[i] + "\t");
        }
        System.out.println();
    }
}

测试:

        CircularQueue<String> queue = new CircularQueue<>(5);
        queue.enqueue("1");
        queue.enqueue("2");
        queue.enqueue("3");
        queue.enqueue("4");
        queue.enqueue("5");
        queue.enqueue("6");

        queue.print();

        System.out.println("出队:" + queue.dequeue());
        System.out.println("出队:" + queue.dequeue());

        queue.print();

        queue.enqueue("7");

        queue.print();

输出:

1   2   3   4   5   
出队:1
出队:2
3   4   5   
3   4   5   7   

由于入队enqueue()、出队dequeue()分别对tail指针和head指针进行了纠正,不再会出现数据搬移的情况,其时间复杂度都为:O(1).

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

推荐阅读更多精彩内容