数据结构 - 队列

目录

1、队列的分类

按照实现分为:顺序队列、链式队列

按照结构分为:普通队列、双端队列、环形队列(单向和双向)

按照特性组织:是否并发队列(是否线程安全)、是否阻塞队列、是否有界队列

其他特殊属性:优先队列(本身数据结构不是队列而是堆)、交换队列、延迟队列

2、队列的实现

1)、顺序单向队列

2)、环形顺序链表


    队列也是线性表的一种,其特点就是先进先出(FIFO),即从线性表的一端进从另一端出,所以也是一种操作受限的线性表。与 的数据结构一样,队列可以基于数组进行实现,称为顺序队列,也可以基于链表实现,称为链式队列。先进先出特点鲜明到就是在作排队等待,所以经常用于各种资源受限的情况下排队等待(比如线程池)。个人理解受限的数据结构和方法,限制本身也觉得了其特色的使用场景,根据队列的不同形态有 队列、双端队列和环形队列。入队和出队操作,只涉及到head、tail指针的跳动,所以时间复杂度为O(1)。

1、队列的分类

按照实现分为:顺序队列、链式队列

    数组在创建的时候就确定了大小,不能扩容,所以一般基于数组实现的顺序队列是有界队列;

    链表可以利用碎片的内存空间,使用指针(引用)链接下一个节点,本身就是动态扩容的,所以一般基于链表实现的链式队列无界队列。比如 Java juc中的 LinkedBlockQueue默认的大小为 Integer.MAX_VALUE,可以认为是无界队列,只是我们在创建对象时可以             指定队列的长度,变成有界队列。

按照结构分为:普通队列、双端队列、环形队列(单向和双向)

    

       根据数据结构分成明显,普通队列(单向队列)只能从一端进一端出,并且FIFO;

       双端队列在管道的两端都可以入队出队;

       环形队列则说明空间是一定的,当数据量无限制增大的时候不能进行扩容,则需要根据策略到达是覆盖数据还是丢弃数据。环形队列也分为单向环形队列和双向环形队列,如果队列和双端队列是使用类似管道的方式,根据可以从管道的一个或者两个方向出入;那么环形队列与head和tail指针有关,单端队列从tail处进从head处出,双端队列可以从head和tail两个点进出。 环形队列应用场景还是非常多的,比如Mysql的

按照特性组织:是否并发队列(是否线程安全)、是否阻塞队列、是否有界队列

    队列的场景主要用于有限的资源进行排序等场景,我们一般理解的队列数据结构是处理单机(同一个JVM)中时,此时可能需要协调多个线程协调等待有限资源(或者临界区),所以需要队列本身支持并发。在Java中最简单的支持并发队列,就是在队列的入队(enqueue)、出队(dequeue)两个方法上添加 synchronized关键字,当然也可以基于 Unfase类的CAS机制进行实现(这里涉及了大量的并发知识,可以关注高并发系列)。

    阻塞队列是指出队操作时,如果队列已经为空了,不能马上返回空,而是需要使用自旋等方式,知道队列中又添加了新的数据时,获取新的数据返回。阻塞队列主要用于生产者消费者模式。比如Java线程池中,当创建了一个线程(可能是线程池预热就创建了,也可能是基于线程池原理,添加任务时创建的),本身就是创建了一个Worker对象(该对象本身就是一个Runnable),操作系统调度会直接调用Worker的run方法,该方法会先处理因为我们添加任务时创建的任务,执行完成后就会去线程池队列中获取任务执行,而该队列我们一般使用LinkedBlockingQueue(其就是一个阻塞队列),当任务执行完队列为空时,一直阻塞知道有新的任务添加到队列。如果此时使用的不是阻塞队列,则马上返回结果,run方法执行完成后,就会进行销毁。具体可以参考ThreadPoolExecutor源码解析

    上面提到是否有界队列本身与队列的实现有关(链式队列还是顺序队列),是否有界主要表现在队列中的任务堆积时,不能在特定的时间内处理,或者造成饥饿。

 

其他特殊属性:优先队列(本身数据结构不是队列而是堆)、交换队列、延迟队列

    优先队列本身是基于堆的数据结构实现,后面专门分析,是树的一种结构。而交换队列(Java中包含Transfer字样的队列),延迟队列直接与业务相关。

 

    所以一个队列本身可能集上面的多个特点,这就需要我们根据业务进行选择,比如 LinkedBlockingDeque = 链式队列 + 双端队列 + 阻塞队列 + 默认无界队列 + 并发队列;队列本身比较复杂,用于各种场景需求,这里就拿Java中的队列进行映射(只是Java中没有环形队列,因为使用场景比较特殊),java中是队列分类特性:

  1. 队列名称中含有 Linked字样的是基于链表实现的链式队列,名称包含 Array字样的是基于数组实现的 顺序队列
  2. Dqueue接口继承自Queue接口,带有Dqueue字样,获取该接口的子类的,都是双端队列;否则是单向队列
  3. 基于BlockingQueue字样或者该接口的子类,都是阻塞队列,否则是非阻塞队列
  4. BlockingDqueue接口的子类,即是阻塞队列又是双端队列

 

2、队列的实现

1)、顺序单向队列

    基于数组实现的顺序队列本身比较简单,只是需要注意队空的判断条件为 head == tail,队满的判断条件为 tail == array.length。如果要实现支持动态扩容的队列,则需要与ArrayList一样,在添加元素时判断,如果tail == array.length则要新增一个更大的数组,并将原数据拷贝过去。之前在大O复杂度表示法(最后部分)只有均摊的时间复杂度分析方法,分析了其新增的时间复杂度也为O(1)。

public class DynamicArrayQueue {
  // 数组:items,数组大小:n
  private String[] items;
  private int n = 0;
  // head表示队头下标,tail表示队尾下标
  private int head = 0;
  private int tail = 0;

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

  // 入队操作,将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;
  }

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

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

2)、环形顺序链表

    环形顺序队列也是面试高频,所以需要自己能抓住重要的特点(下面的实现方式,会浪费一个数组空间,否则没有条件判断队满或者队空):

1、有两个head、tail下标分别指向队头和队尾,队尾与入队(enqueue)方法关联,队头head与出队(dequeue方法关联);

2、判断队列空的条件比较简单,head == tail;

3、判断队列满的条件是 (tail + 1)% n == head;// n为队列长度,也就是存储队列数据的数组的长度

4、入队和出队方法,先判断完是否队满或者队空后;直接根据下标获取或者存储数据;再将下标值加1,而加1操作可能使数组下标越界此时需要让其数组头。比较巧妙的技巧是使用数组长度取余,或者判断下标已经与数组长度相同则置为0

    tail = (tail + 1) % queueLength;tail = tail + 1 == queueLength ? 0 : tail + 1;

public class CircularQueue<T> {

    /** 队列数据 */
    private Object[] data;
    /** 队列的长度 */
    private int queueLength;
    /** 队头的位置 */
    private int head;
    /** 队尾的位置 */
    private int tail;

    public static void main(String[] args) {
        // 取出元素1
        // 存储第四个元素返回false,因为不空一个位置的话,没有条件判断队空队满
        CircularQueue<Integer> circularQueue = new CircularQueue<>(4);
        // head = 0, tail = 0;

        circularQueue.enqueue(1);
        circularQueue.enqueue(2);
        Integer dequeue = circularQueue.dequeue();
        System.out.println("取出元素" + dequeue);
        circularQueue.enqueue(3);
        circularQueue.enqueue(4);
        Boolean enqueue = circularQueue.enqueue(5);
        System.out.println("存储第四个元素返回" + enqueue + ",因为不空一个位置的话,没有条件判断队空队满");
    }

    public CircularQueue(int queueLength) {
        data = new Object[queueLength];
        this.queueLength = queueLength;
    }

    public Boolean enqueue(T t) {
        if ((tail + 1) % queueLength == head) {
            return false;
        }

        data[tail] = t;
//        tail = (tail + 1) % queueLength;
        tail = tail + 1 == queueLength ? 0 : tail + 1;

        return true;
    }

    public T dequeue() {
        if (head == tail) {
            return null;
        }

        T result = (T)data[head];
//        head = (head + 1) % queueLength;
        head = head + 1 == queueLength ? 0 : head + 1;

        return result;
    }

}

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值