数据结构与算法|第六章:队列

数据结构与算法|第六章:队列

1.项目环境

2.什么是队列?

可以将它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进先出,这就是典型的 队列
队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EJ7PpbGI-1591167731826)(G:\workspace\csdn\learn-document\data-structure-algorithm\csdn\image-20200601215812324.png)]
队列的应用非常广泛,比如 Java 线程池中的等待队列,JUC 中的 AQS 队列,又或者是近几年非常火爆的各类消息中间,ActiveMq、RabbitMq、Kafka 等等。

3.顺序队列

队列用数组和链表都可以实现,其中使用数组实现队列叫 顺序队列,使用链表实现的队列叫 链式队列,我们使用数组进行实现

/**
 * 使用数组来实现队列
 */
public class ArrayQueue {

    private String[] items;// 队列存储的元素
    private int n;// 队列大小
    private int head;// 头节点下标
    private int tail;// 尾节点下标

    private static final String[] EMPTY_ELEMENTDATA = {};

    public ArrayQueue(int initialCapacity) {
        if (initialCapacity > 0) {
            this.items = new String[initialCapacity];
            n = initialCapacity;
        } else if (initialCapacity == 0) {
            this.items = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: " +
                    initialCapacity);
        }
    }

    /**
     * 从尾部入队
     *
     * @param item
     * @return
     */
    public boolean enqueue(String item) {
        if (tail == n) {
            if (head == 0) {//没有出队操作
                throw new RuntimeException("队列已满");
            } else {//有出队操作,表示队列头部有空余空间,可以进行数据迁移
                System.err.println("触发数据迁移");
                for (int i = 0; i < n; i++) {
                    if (i <= head) {
                        items[i] = items[i + head];
                    } else {
                        items[i] = null;
                    }
                }
                tail = head + 1;
                head = 0;
            }
        }
        items[tail] = item;
        tail++;
        return true;
    }

    /**
     * 从头部出队
     *
     * @return
     */
    public String dequeue() {
        if (tail == head) {
            throw new RuntimeException("空队列");
        }
        String res = items[head];
        items[head] = null;
        head++;
        return res;
    }

    @Override
    public String toString() {
        return "ArrayQueue{" +
                "items=" + Arrays.toString(items) +
                ", n=" + n +
                ", head=" + head +
                ", tail=" + tail +
                '}';
    }
}

测试:

public class ArrayQueueDemo {
    public static void main(String[] args) {
        // 入队异常
        enqueueException();
        // 入队和出队
        enqueueAndDequeue();
        // 交替进行
        alternatedenqueueAndDequeue();
    }

    private static void enqueueException() {
        ArrayQueue arrayQueue = new ArrayQueue(5);
        for (int i = 0; i < 5; i++) {
            arrayQueue.enqueue("元素" + i);
            System.err.println(arrayQueue.toString());
        }
    }

    private static void enqueueAndDequeue() {
        ArrayQueue arrayQueue = new ArrayQueue(5);
        for (int i = 0; i < 5; i++) {
            arrayQueue.enqueue("元素" + i);
            System.err.println(arrayQueue.toString());
        }
        for (int i = 0; i < 5; i++) {
            String item = arrayQueue.dequeue();
            System.err.println("出队元素:"+item);
            System.err.println(arrayQueue.toString());
        }
    }

    private static void alternatedenqueueAndDequeue() {
        ArrayQueue arrayQueue = new ArrayQueue(5);
        for (int i = 0; i < 5; i++) {
            arrayQueue.enqueue("元素" + i);
            System.err.println(arrayQueue.toString());
        }
        for (int i = 0; i < 2; i++) {
            String item = arrayQueue.dequeue();
            System.err.println("出队元素:"+item);
            System.err.println(arrayQueue.toString());
        }
        for (int i = 0; i < 2; i++) {
            arrayQueue.enqueue("再次入队元素" + i);
            System.err.println(arrayQueue.toString());
        }
    }
}

三个方法需要分开执行

enqueueException 执行结果:

ArrayQueue{items=[元素0, null, null, null, null], n=5, head=0, tail=1}
ArrayQueue{items=[元素0, 元素1, null, null, null], n=5, head=0, tail=2}
ArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, head=0, tail=3}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, head=0, tail=5}
Exception in thread "main" java.lang.RuntimeException: 队列已满
	at com.huajie.chapter05.ArrayQueue.enqueue(ArrayQueue.java:38)
	at com.huajie.chapter05.ArrayQueueDemo.enqueueException(ArrayQueueDemo.java:19)
	at com.huajie.chapter05.ArrayQueueDemo.main(ArrayQueueDemo.java:9)

enqueueAndDequeue 执行结果:

ArrayQueue{items=[元素0, null, null, null, null], n=5, head=0, tail=1}
ArrayQueue{items=[元素0, 元素1, null, null, null], n=5, head=0, tail=2}
ArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, head=0, tail=3}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, head=0, tail=5}
出队元素:元素0
ArrayQueue{items=[null, 元素1, 元素2, 元素3, 元素4], n=5, head=1, tail=5}
出队元素:元素1
ArrayQueue{items=[null, null, 元素2, 元素3, 元素4], n=5, head=2, tail=5}
出队元素:元素2
ArrayQueue{items=[null, null, null, 元素3, 元素4], n=5, head=3, tail=5}
出队元素:元素3
ArrayQueue{items=[null, null, null, null, 元素4], n=5, head=4, tail=5}
出队元素:元素4
ArrayQueue{items=[null, null, null, null, null], n=5, head=5, tail=5}

alternatedenqueueAndDequeue 执行结果:

ArrayQueue{items=[元素0, null, null, null, null], n=5, head=0, tail=1}
ArrayQueue{items=[元素0, 元素1, null, null, null], n=5, head=0, tail=2}
ArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, head=0, tail=3}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, head=0, tail=5}
出队元素:元素0
ArrayQueue{items=[null, 元素1, 元素2, 元素3, 元素4], n=5, head=1, tail=5}
出队元素:元素1
ArrayQueue{items=[null, null, 元素2, 元素3, 元素4], n=5, head=2, tail=5}
触发数据迁移
ArrayQueue{items=[元素2, 元素3, 元素4, 再次入队元素0, null], n=5, head=0, tail=4}
ArrayQueue{items=[元素2, 元素3, 元素4, 再次入队元素0, 再次入队元素1], n=5, head=0, tail=5}

前面两种就不做说明了,就是正常的出队和入队操作,第三种触发了数据迁移的操作,我们需要画图来进行演示,有助于理解

第一步,5 个元素依次入队
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I7S6U6kI-1591167731828)(G:\workspace\csdn\learn-document\data-structure-algorithm\csdn\image-20200603095401377.png)]
第二步,2 个元素出队
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0NJPrEd-1591167731830)(G:\workspace\csdn\learn-document\data-structure-algorithm\csdn\image-20200603095423059.png)]
第三步,再次入队 2 个元素

由于队列尾部已经无法再插入,要满足先进先出的特点,所以在头部 1,2 两个位置也不能插入,触发数据迁移操作,将 3、4、5 三个元素迁移到数组下标 [1,2,3] 的位置上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yDuLqKVz-1591167731832)(G:\workspace\csdn\learn-document\data-structure-algorithm\csdn\image-20200603095552356.png)]
再正常入队两个元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WfCApF8B-1591167731833)(G:\workspace\csdn\learn-document\data-structure-algorithm\csdn\image-20200603095645385.png)]
复杂度分析:

  • 出队的时间复杂度 O(1)

  • 入队的时间复杂度

    • 在不触发数据迁移的情况下是 O(1)
    • 在触发数据迁移的情况下,均摊时间复杂度是 O(1),这种情况下使用 均摊时间复杂度 更为合理

4.链式队列

示例中的 toArray 方法主要是为了打印方便和队列操作逻辑无关

/**
 * 使用链表来实现队列
 */
public class LinkedListQueue {

    private Node head;// 头节点
    private Node tail;// 尾节点
    private int size;

    public LinkedListQueue() {
    }

    /**
     * 从尾部入队
     *
     * @param element
     * @return
     */
    public boolean enqueue(String element) {
        if (head == null) {
            head = tail = new Node(element, null);
        } else {
            Node node = new Node(element, null);
            tail.next = node;
            tail = tail.next;
        }
        size++;
        return true;
    }

    /**
     * 从头部出队
     *
     * @return
     */
    public String dequeue() {
        String res = head.item;
        head = head.next;
        size--;
        return res;
    }

    private static class Node {
        String item;
        Node next;

        Node(String element, Node next) {
            this.item = element;
            this.next = next;
        }
    }

    public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node x = head; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }

    @Override
    public String toString() {
        return "LinkedListQueue{" +
                "元素集合=" + Arrays.toString(toArray()) +
                '}';
    }
}

测试

public class LinkedListQueueDemo {
    public static void main(String[] args) {
        LinkedListQueue linkedListQueue = new LinkedListQueue();
        for (int i = 0; i < 5; i++) {
            linkedListQueue.enqueue("元素" + i);
            System.out.println(linkedListQueue.toString());
        }
        System.out.println("====出队两个元素====");
        for (int i = 0; i < 2; i++) {
            linkedListQueue.dequeue();
            System.out.println(linkedListQueue.toString());
        }
        System.out.println("====再次入队三个元素====");
        for (int i = 0; i < 3; i++) {
            linkedListQueue.enqueue("再出入队元素" + i);
            System.out.println(linkedListQueue.toString());
        }
    }
}

执行结果:

LinkedListQueue{元素集合=[元素0]}
LinkedListQueue{元素集合=[元素0, 元素1]}
LinkedListQueue{元素集合=[元素0, 元素1, 元素2]}
LinkedListQueue{元素集合=[元素0, 元素1, 元素2, 元素3]}
LinkedListQueue{元素集合=[元素0, 元素1, 元素2, 元素3, 元素4]}
====出队两个元素====
LinkedListQueue{元素集合=[元素1, 元素2, 元素3, 元素4]}
LinkedListQueue{元素集合=[元素2, 元素3, 元素4]}
====再次入队三个元素====
LinkedListQueue{元素集合=[元素2, 元素3, 元素4, 再出入队元素0]}
LinkedListQueue{元素集合=[元素2, 元素3, 元素4, 再出入队元素0, 再出入队元素1]}
LinkedListQueue{元素集合=[元素2, 元素3, 元素4, 再出入队元素0, 再出入队元素1, 再出入队元素2]}

链表的操作相对数组更加简单,只需要操作头节点和尾节点即可,而且链表没有大小限制,不需要扩容。

复杂度分析:

  • 出队的时间复杂度 O(1)

  • 入队的时间复杂度 O(1)

5.循环队列

上面顺序队列的实现,如果不采用数据迁移的操作,也可以使用循环队列的方式解决,同样实现队列先进先出的效果。

假设,此时数组位置 [1,2] 的元素已经出队,如果元素 1 入队,按原逻辑,tail++,但是 tail 已经和数组长度 n 相等,无法再加 1 了。

我们可以设置,tail 位置为空闲的位置 1,并将元素 1 放到数组角标 0 的位置

如果元素 2 入队,设置 tail 位置为空闲的位置 2,并将元素 2 放到数组角标 1 的位置

出队的时候,同样还是出队 head 头部的元素,这就是循环队列的思想。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ICnldfX-1591167731834)(G:\workspace\csdn\learn-document\data-structure-algorithm\csdn\image-20200603105801611.png)]
代码实现:

  • 难点1:需要判断在队列满时,如果存在空闲位置,tail 由 5 变成 1 的逻辑(循环思想)
  • 难点2:同样 head 如何实现循环效果

以下实现可能有 bug,只在本测试用例中没有问题,主要是思路

/**
 * 使用数组来实现循环队列
 */
public class CycleArrayQueue {

    private String[] items;// 队列存储的元素
    private int n;// 队列大小
    private int count;// 实际元素个数
    private int head;// 头节点下标
    private int tail;// 尾节点下标

    private static final String[] EMPTY_ELEMENTDATA = {};

    public CycleArrayQueue(int initialCapacity) {
        if (initialCapacity > 0) {
            this.items = new String[initialCapacity];
            n = initialCapacity;
        } else if (initialCapacity == 0) {
            this.items = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: " +
                    initialCapacity);
        }
    }

    /**
     * 从尾部入队
     *
     * @param item
     * @return
     */
    public boolean enqueue(String item) {
        if (count == n) {
            throw new RuntimeException("队列已满");
        }
        if (tail == n) {// 需要将新加入的元素添加到前面空闲节点
            tail = 1;
            items[0] = item;
        } else {
            items[tail] = item;
            tail++;
        }
        count++;
        return true;
    }

    /**
     * 从头部出队
     *
     * @return
     */
    public String dequeue() {
        if (count == 0) {
            throw new RuntimeException("空队列");
        }
        String res = items[head];
        items[head] =null;
        head = (head + 1) % n;
        count--;
        return res;
    }

    @Override
    public String toString() {
        return "CycleArrayQueue{" +
                "items=" + Arrays.toString(items) +
                ", n=" + n +
                ", count=" + count +
                ", head=" + head +
                ", tail=" + tail +
                '}';
    }
}

测试

public class CycleArrayQueueDemo {
    public static void main(String[] args) {
        alternatedenqueueAndDequeue();
    }

    private static void alternatedenqueueAndDequeue() {
        CycleArrayQueue arrayQueue = new CycleArrayQueue(5);
        for (int i = 0; i < 5; i++) {
            arrayQueue.enqueue("元素" + i);
            System.err.println(arrayQueue.toString());
        }
        for (int i = 0; i < 3; i++) {
            String item = arrayQueue.dequeue();
            System.err.println("出队元素:"+item);
            System.err.println(arrayQueue.toString());
        }
        for (int i = 0; i < 3; i++) {
            arrayQueue.enqueue("第二次入队元素" + i);
            System.err.println(arrayQueue.toString());
        }
        System.err.println("===================");
        for (int i = 0; i < 5; i++) {
            String item = arrayQueue.dequeue();
            System.err.println("第二次出队元素:"+item);
            System.err.println(arrayQueue.toString());
        }
        for (int i = 0; i < 5; i++) {
            arrayQueue.enqueue("第三次入队元素" + i);
            System.err.println(arrayQueue.toString());
        }
    }

}

执行结果:

CycleArrayQueue{items=[元素0, null, null, null, null], n=5, count=1, head=0, tail=1}
CycleArrayQueue{items=[元素0, 元素1, null, null, null], n=5, count=2, head=0, tail=2}
CycleArrayQueue{items=[元素0, 元素1, 元素2, null, null], n=5, count=3, head=0, tail=3}
CycleArrayQueue{items=[元素0, 元素1, 元素2, 元素3, null], n=5, count=4, head=0, tail=4}
CycleArrayQueue{items=[元素0, 元素1, 元素2, 元素3, 元素4], n=5, count=5, head=0, tail=5}
出队元素:元素0
CycleArrayQueue{items=[null, 元素1, 元素2, 元素3, 元素4], n=5, count=4, head=1, tail=5}
出队元素:元素1
CycleArrayQueue{items=[null, null, 元素2, 元素3, 元素4], n=5, count=3, head=2, tail=5}
出队元素:元素2
CycleArrayQueue{items=[null, null, null, 元素3, 元素4], n=5, count=2, head=3, tail=5}
CycleArrayQueue{items=[第二次入队元素0, null, null, 元素3, 元素4], n=5, count=3, head=3, tail=1}
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, null, 元素3, 元素4], n=5, count=4, head=3, tail=2}
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, 第二次入队元素2, 元素3, 元素4], n=5, count=5, head=3, tail=3}
===================
第二次出队元素:元素3
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, 第二次入队元素2, null, 元素4], n=5, count=4, head=4, tail=3}
第二次出队元素:元素4
CycleArrayQueue{items=[第二次入队元素0, 第二次入队元素1, 第二次入队元素2, null, null], n=5, count=3, head=0, tail=3}
第二次出队元素:第二次入队元素0
CycleArrayQueue{items=[null, 第二次入队元素1, 第二次入队元素2, null, null], n=5, count=2, head=1, tail=3}
第二次出队元素:第二次入队元素1
CycleArrayQueue{items=[null, null, 第二次入队元素2, null, null], n=5, count=1, head=2, tail=3}
第二次出队元素:第二次入队元素2
CycleArrayQueue{items=[null, null, null, null, null], n=5, count=0, head=3, tail=3}
CycleArrayQueue{items=[null, null, null, 第三次入队元素0, null], n=5, count=1, head=3, tail=4}
CycleArrayQueue{items=[null, null, null, 第三次入队元素0, 第三次入队元素1], n=5, count=2, head=3, tail=5}
CycleArrayQueue{items=[第三次入队元素2, null, null, 第三次入队元素0, 第三次入队元素1], n=5, count=3, head=3, tail=1}
CycleArrayQueue{items=[第三次入队元素2, 第三次入队元素3, null, 第三次入队元素0, 第三次入队元素1], n=5, count=4, head=3, tail=2}
CycleArrayQueue{items=[第三次入队元素2, 第三次入队元素3, 第三次入队元素4, 第三次入队元素0, 第三次入队元素1], n=5, count=5, head=3, tail=3}

6.阻塞队列

阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

ArrayBlockingQueue 示例

  • 定义一个长度为 10 的阻塞队列

  • 往队列中添加(入队) 100 个元素

  • 每 1 秒取出(出队)一个元素

public class BlockingDemo {

    ArrayBlockingQueue<String> ab = new ArrayBlockingQueue(10);

    {
        init();
    }

    public void init() {
        new Thread(() -> {
            while (true) {
                try {
                    String data = ab.take();
                    System.out.println("receive:" + data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public void addData(String data) throws InterruptedException {
        ab.add(data);
        System.out.println("send:" + data);
        Thread.sleep(1000);
    }

    public static void main(String[] args) throws InterruptedException {

        BlockingDemo demo = new BlockingDemo();
        for (int i = 0; i < 100; i++) {
            demo.addData("haha" + i);
        }
    }

}

执行结果:

send:haha0
receive:haha0
send:haha1
receive:haha1
send:haha2
receive:haha2
send:haha3
receive:haha3
...

入队操作相关源码

    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;// 重入锁
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                // 入队
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

这里使用了 ReentrantLock 进行加锁,Condition 进行线程阻塞和唤醒,enqueue 就是典型的入队操作,Java 相关的细节我们就不展开了。

出队相关源码

  • notEmpty.await(); 表示通过 take() 获取队列元素时,如果 count 为 0 表示队列为空,那么通过 notEmpty.await(); 进行阻塞,notEmpty 是 Condition 类的实例
  • 如果 count 不为 0,则通过dequeue 出队操作
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();// 可以被中断的锁
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

7.小结

队列最大的特点就是先进先出(FIFO),主要的两个操作是入队和出队。既可以用数组来实现,也可以用链表来实现;用数组实现的叫 顺序队列,用链表实现的叫 链式队列。循环队列主要是学习思路,加深队列的理解和相关操作;最后简单的了解 Java 中基于数组实现阻塞队列的使用,其实 Java JUC 中还有其他几种队列的实现,有兴趣可以了解。

8.参考

  • 极客时间 -《数据结构与算法之美》王争
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值