算法笔记-队列

队列在线程池等有限资源池中的应用

本文创作灵感来源于 极客时间 王争老师的《数据结构与算法之美》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

是什么

什么是队列
  • 队列也是一种操作受限的线性数据结构,只提供入列和出列两种操作,遵循先进先出原则:先入列的元素优先出列
  • 队列中又区分了普通队列,循环队列,阻塞队列以及并发队列。不同的类型有着不同的实际应用,尤其是在底层系统,框架和中间件中发挥着巨大作用。

    为什么

    为什么有队列这种数据结构
  • 特定的数据结构是为了解决特定的某一类问题,而队列这种特殊的线性数据结构也有着其特殊的作用。正如栈遵循的先入先出原则一样,现实生活中的排队现象就是队列的一个很好体现。先排队的人,优先享受服务,后排队的人,要等前面的人都被服务过之后,才轮到自己。
  • 怎么办

    怎么学习队列这种数据结构
  • 队列根据其实现方式的不同又区分了顺序队列和链式队列,而从功能上又区分普通队列,循环队列,阻塞队列和并发队列。
    顺序队列
  • 顺序队列:队列实现的方式是数组。简单实现代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    class  ArrayQueue{
    private String[] queueList;//队列
    private int first;//队头元素下标
    private int last;//队尾元素下标
    private int n;//队列大小

    //初始化队列
    public ArrayQueue(int length){
    this.queueList = new String[length];
    this.first = 0;
    this.last = 0;
    this.n = length;
    }

    //入队
    public boolean enQueue(String item){
    //队尾元素下标等于队列长度,说明队列末尾不能入队了
    if(last == n){
    /**
    * 队头元素下标不为0,说明当前数组前面有空余位置,
    * 这个时候需要对队列中的元素搬移位置,使得队列中的
    * 元素都从数组的第一个位置排列
    */
    if(first != 0){
    for(int i = first;i < last; i++){
    queueList[i-first] = queueList[i];
    }
    last = last - first;
    first = 0;
    queueList[last] = item;
    last++;
    return true;
    }else{
    return false;
    }
    }else {
    return false;
    }
    }

    //出列
    public String deQueue(){
    //判断队列是否为空
    if(first == last){
    return null;
    }else{
    String item = queueList[first];
    first++;
    return item;
    }
    }
    }

用数组实现的队列,每次出列的时候,取出数组中下标为 first 的元素,然后 first 自增一。每次入列的时候,数组下标为 last 的位置存储元素,last 自增一。出列的情况,我们只需判断当时的队列是否为空队列就够了。但是,入列的时候,由于出列导致 first 值一直在自增,入列导致 last 值也在自增,那么数组势必会存储到最后一个元素,而数组的前面由于出列,空余出了一些位置。所以,这个时候我们就需要把数组中的元素整体搬移到数组的最前面,为后续入列提供空间。数据搬移的时候,我们要考虑好边界条件:first 值不等于 0 且 last 值等于数组的长度

链式队列
  • 链式队列:队列实现的方式是链表。简单实现代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    class LinkQueue{
    class Node{
    String data;//数值域
    Node next;//指针域
    }

    private Node first;//队列头结点
    private Node last;//队列尾节点

    //初始化队列
    public LinkQueue(){
    this.first.next = null;
    this.last.next = null;
    }

    //入列
    public boolean enQueue(Node node){
    //空队列
    if(first.next == null && last.next == null){
    first.next = node;
    last.next = node;
    return true;
    }else{
    last.next.next = node;
    last.next = node;
    return true;
    }
    }

    //出列
    public Node deQueue(){
    //判断是否为空列
    if(first.next == null && last.next == null){
    return null;
    }else{
    Node node = first.next;
    node.next = null;//释放内存
    first.next = first.next.next;
    return node;
    }
    }
    }

链式队列中,我们需要额外创建两个节点,一个是头节点一个是尾节点,分别指向第一个元素和最后一个元素。队列为空的条件就是两个指示节点都指向 null 。由于链表可以自由的申请内存空间,所以链式队列理论上是可以无限大的。

循环队列
  • 循环队列顾名思义就是一个环形的队列,首尾相连。相对于普通的数组实现的队列来说,我们不用在做数据搬移的工作了,只要队列未满,我们就可以直径二入队。简单实现代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class CircularQueue{
    private String[] queueList;//队列
    private int first;//队头元素下标
    private int last;//队尾元素下标
    private int n;//队列大小

    //初始化队列
    public CircularQueue(int length){
    this.queueList = new String[length];
    this.first = 0;
    this.last = 0;
    this.n = length;
    }

    //入队
    public boolean enQueue(String item){
    if((last + 1) % n == first){
    return false;
    }else {
    queueList[last] = item;
    last = (last + 1) % n;
    return true;
    }
    }

    //出列
    public String deQueue(){
    if(first == last){
    return null;
    }else{
    String item = queueList[first];
    first = (first + 1) % n;
    return item;
    }
    }
    }

从代码中我们发现两个公式 (last + 1) % n 和 (first + 1) % n。通过对比发现,当队列满的时候:(last + 1) % n = first。从而得出上述两个出列与入列时候的标识位获取新值的公式。注:上述循环链表中的最后一个位置是不能存储元素的,因为一旦入队,last == first 就成立了,队列就是一个空列,但是实际上是已经存储满了。

阻塞队列和并发队列
  • 阻塞队列就是对队列的操作添加了受限条件:当队列为空的时候,出队操作就会被阻塞;如果队列满了,那么入列操作就会阻塞。这正好是一个消费者与生产者的模型。
  • 并发队列就是线程安全的队列,简单的操作就是在入队出队的操作上加锁。王争老是提到基于数组的循环队列,利用CAS原子操作,可以实现非常搞笑的并发队列。这个概念我还是第一次听过,接下来我会好好查找资料,研究一番,后续单独在写一篇文章记录。
    队列在线程池等有限资源池中的应用
  • 非阻塞方式,直接拒绝请求
  • 阻塞方式,将请求排队,依次分配资源。
  • 对大部分有限资源的请求使用,当没有资源的时候,都是基于队列的形式实现排队请求的。
  • 链表实现了无界队列,但是肯能会导致过多的请求排队,导致请求时间过长,对于敏感系统来说弊端很大。
  • 数组实现了有界队列,当请求数量超过队列拥有资源的数量的时候,新的i请求会被拒绝。但是,如何恰当的分配队列数组的大小,也是一个很复杂的实现。

    总结

    初入算法学习,必是步履蹒跚,一路磕磕绊绊跌跌撞撞。看不懂别慌,也别忙着总结,先读五遍文章先,无他,唯手熟尔~
    与诸君共勉
-------------本文结束感谢您的阅读-------------