美文网首页转载部分
数据结构与算法之美-队列

数据结构与算法之美-队列

作者: 沉江小鱼 | 来源:发表于2021-04-16 20:46 被阅读0次

前言:本篇文章只是记录王争的数据结构与算法之美的学习笔记,写下来能强迫自己系统的再过一遍,加深理解。这门课以实际开发中遇到的问题为例,引入解决问题涉及到的的数据结构和算法,但不会讲的太细,最好结合一本实体书进行学习。

CPU 资源是有限的,任务的处理速度和线程个数也不是线性正相关的,相反,过多的线程反而会导致CPU 频繁切换,处理性能下降。当我们向固定大小的线程池中请求一个线程时,如果请求非常多,线程池没有空闲资源,如何处理这个请求?就需要用到队列了。

1. 队列

队列很好理解,可以理解为排队买票,先来的买完票就走了,后来的在最后排队。

跟栈不同,先进者先出,后进者后出,这就是典型的"队列",跟栈相同的是队列也是一种操作受限的线性表数据结构,栈只支持两个基本操作:入栈 Push出栈 Pop,队列也支持两个基本操作:入队 enqueue出队 dequeue,如下图所示:

image.jpeg

2. 队列的实现

从队列的概念中,我们知道,队列和栈一样,都是抽象的数据结构,具有先进先出,后进后出的特性。

我们有两种实现方式:

  • 用数组实现,则为顺序队列
  • 用链表实现,则为链式队列
2.1 数组实现队列
// 用数组实现的队列
public class ArrayQueue {
  // 数组:items,数组大小:n
  private String[] items;
  private int n = 0;
  // head表示队头下标,tail表示队尾下标
  private int head = 0;
  private int tail = 0;

  // 申请一个大小为capacity的数组
  public ArrayQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入队
  public boolean enqueue(String item) {
    // 如果tail == n 表示队列已经满了
    if (tail == n) return false;
    items[tail] = item;
    ++tail;
    return true;
  }

  // 出队
  public String dequeue() {
    // 如果head == tail 表示队列为空
    if (head == tail) return null;
    // 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
    String ret = items[head];
    ++head;
    return ret;
  }
}

队列的数组实现也挺简单,对于栈来说需要一个栈顶指针就行,因为栈的入栈和出栈都是在一个方向进行。队列则是不同的方向入队和出队,所以需要两个指针:head 指针,指向队头tail 指针,指向队尾

如下图,当a b c d 依次入队之后,队列的 head 指针指向下标为 0 的位置,tail 指向下标为 4 的位置:


image.png

当我们调用两次出队操作之后,head 指向下标为 2 的位置,tail 仍然指向下标为 4 的位置:


image.png

如果用数组实现队列的话,会遇到一个问题:插入很多元素之后,队列的空间不够了。
我们可以跟数组一样,用数据搬移,出队一个,就搬移一次,但是这样很耗费时间,想象一下,出队从下标为 0 的位置开始,相当于删除下标为 0 的数据,这样每出队一个数据,就需要搬移整个队列的数据,当然也可以稍微优化一下,就是当 tail 指针指向最后一个位置时,将所有数据都移到下标为 0 开始的位置上,整体进行搬移一次,代码如下:


   // 入队操作,将item放入队尾
  public boolean enqueue(String item) {
    // tail == n表示队列末尾没有空间了
    if (tail == n) {
      // tail ==n && head==0,表示整个队列都占满了
      if (head == 0) return false;
      // 数据搬移
      for (int i = head; i < tail; ++i) {
        items[i-head] = items[I];
      }
      // 搬移完之后重新更新head和tail
      tail -= head;
      head = 0;
    }
    
    items[tail] = item;
    ++tail;
    return true;
  }

从代码中看到,当队列的 tail移动到数组最右边时,如果有新的数据入队,可以将 headtail 之间的数据,整体搬移到数组 0 到 tail-head的位置:

image.png
2.2 链表实现队列

基于链表实现,head 指针指向链表的第一个节点tail 指向链表的最后一个节点
当入队时,我们需要:

tail->next = new_node;
tail = tail->next;

出队时:

head = head->next
image.png

3. 循环队列

顾名思义,就是一个首位相连的队列,在上面用数组实现的队列时,当 tail==n 时,需要对数据做搬移操作,这样性能会受到影响,我们可以让队列的首尾相连,当tail==n的时候,又开始从下标为 0 的地方入队,这就是循环队列,就是一个环了:

image.png

从上图中可以看到,队列大小为 8,目前 head = 4tail = 7,按照上面数组的实现,我们需要做数据搬移操作了。但是用循环队列的话,我们可以将新的数据 a 放到下标为 7 的位置,将新的数据 b 放到下标为 0 的位置,这样 tail 就指向下标为 1 的位置,如下图:

`

这样我们避免了数据搬移操作,但是判断队列为空,和队列满了的条件时就比较困难了:

  • 数组实现的非循环队列

    • 队空条件:tail == n
    • 队满条件:head == tail
  • 数组实现的循环队列

    • 队空条件:tail == n
    • 队满条件:(tail + 1) %n = head (需要自己画图理解一下)
image.png
像我图中画的队满的情况,tail=3,head=4,n=8,所以总结一下规律就是:(3+1)%8=4。多画几张队满的图,你就会发现,当队满时,(tail+1)%n=head

并且当队列满时,tail 指向的位置其实是空的,所以循环队列会浪费一个数组的存储空间。

4. 阻塞队列

阻塞队列就是在队列为空的时候,从队头取数据会被阻塞,因为队列没数据啊,一直等到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回:


image.png

其实上面的定义就是一个"生产者 - 消费者模型"。基于阻塞队列实现的"生产者 - 消费者模型",可以有效的协调生产和消费的速度。可以多配置几个“消费者”:


image.png

5. 对于线程资源的处理策略

  • 非阻塞处理:直接拒绝任务请求

  • 阻塞处理:将请求排队,使用队列方式存储,直到有空闲线程时,取出队首位置的任务执行

  • 基于链表的实现:
    可以实现一个支持无线排队的无界队列,但是可能会导致过多的请求无法处理,请求处理的时间较长,所以针对响应时间比较敏感的系统,基于链表实现的无线排队的线程池是不合适的

  • 基于数组的实现:
    首先为有界队列,即队列的大小有限,当排队请求超过队列大小时,可以直接拒绝这个请求,对于响应时间敏感的系统来说,相对合理

6. 总结&练习

队列可以用数组实现(顺序队列),可以用链表实现(链式队列),都是先进先出,在数组实现队列的时候,可以实现循环队列,这样就不用数据搬移了。

  • 数组实现队列
  • 链表实现队列
  • 实现循环队列

相关文章

网友评论

    本文标题:数据结构与算法之美-队列

    本文链接:https://www.haomeiwen.com/subject/awfylltx.html