zoukankan      html  css  js  c++  java
  • 数据结构(十)— 队列

    队列的定义

    不知道大家在用电脑时有没有经历过,机器有时会处于疑似死机的状态,鼠标点什么似乎都没用,双击任何快捷方式都不动弹。就当你失去耐心,打算reset时突然它像酒醒了一样,把你刚才点击的所有操作全部都按顺序执行了一遍。这其实是因为操作系统中的多个程序因需要通过一个通道输出,而按先后次序排队等待造成的。
    操作系统中,应用了一种数据结构来实现刚才提到的先进先出的排队功能,这就是队列。
    所以队列 ( queue ) 是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
    队列是一种先进先出 (First In First Out) 的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。假设队列是 q= ( a1 ,a2 , ……, an) ,那么 a1 就是队头元素,而 an 是队尾元素。这样我们就可以删除时, 总是从 a1 开始,而插人时,列在最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后,如图下图所示。
    在这里插入图片描述

    队列的顺序存储

    我们假设一个队列有 n 个元素,则顺序存储的队列需建立一个大于 n 的数组,并把队列的所有元素存储在数组的前 n 个单元,数组下标为 0 的一端即是队头。所谓的入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1)。
    在这里插入图片描述
    出队列的时候,与栈不同的是,队列元素的出列是在队头,即下标为 0 的位置,那也就意味着, 队列中的所有元素都得向前移动,以保证队列的队头,也就是下标为 0 的位置不为空,此时时间复杂度为 O(n)。
    在这里插入图片描述
    可有时想想,为什么出队列时一定要全部移动呢,如果不去限制队列的元素必须存储在数组的前 n 个单元这一条件,出队的性能就会大大增加。 也就是说,队头不需要一定在下标为 0 的位置,如下图所示:
    在这里插入图片描述
    为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针, front 指针指向队头元素 , rear 指针指向队尾元素的下一个位置,这样当front 等于 rear时,此队列不是还剩一个元素,而是空队列。

    但是这里会出现一个问题,数组毕竟是有限的,当数组出队了两个元素,front指针往后移动两个位置,然后在队伍后面添加数据,刚好这时超过了数组的长度,这个时候就会产生数组越界,但是数组前面明明有两个空的位置啊,我们把这种现象叫做"假溢出",如图所示:
    在这里插入图片描述

    循环队列

    所以解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
    刚才的例子继续,上图中的 rear 可以改为指向下标为 0 的位置,这样就不会造成指针指向不明的问题了,如图所示:
    在这里插入图片描述
    此时问题又出来了,我们刚才说,空队列时,front 等于rear,现在当队列满时,也是 front等于 rear,那么如何判断此时的队列究竟是空还是满呢?
    办法一是设置一个标志变量 flag, 当front ==rear,且 flag= 0时为队列空,当front == rear时,且flag =1时为队列满。
    办法二是当队列空时,条件就是 from = rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。如下图所示:
    在这里插入图片描述
    我们重点来讨论第二种方法,由于 rear 可能比 front大,也可能比 front 小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。 所以若队列的最大尺寸为 QueueSize,那么队列满的条件是 (rear+1) %QueueSlze == front。 (取模 “%” 的目的就是为了预防 rear 比 front 小的问题)。比如上面这个例子, QueueSize = 5,而 rear=4, (4+1) %5 = 0,所以此时队列满,再比如 front = 2 而 rear = 1。 (1 + 1) % 5 = 2 ,所以此时队列也是满的。而 front = 2 而 rear = 0 ,(0+1) %5 = 1 , 1不等于2,所以此时队列并没有满。
    另外,当 rear> front 时,即此时队列的长度为 rear - front。 但当 rear < front 时,队列长度分为两段, 一段是 QueueSize -front, 另一段是 0 + rear,加在一起,队列长度为 rear - front + QueueSize。因此通用的计算队列长度公式为:
    (rear- front + QueueSize) %QueueSize

    循环队列具体实现代码如下:

    public class SeqCycleQueue {
      private int front = 0;// 定义头队列指针
      private int rear = 0;// 定义尾队列指针
      private Object[] queueData;// 定义一个队列数组
    
      /**
       * 创建一个顺序循环队列的构造函数
       */
      public SeqCycleQueue(int size) {
        queueData = new Object[size];
      }
    
      /**
       * 队列添加数据
       * 
       * @throws Exception
       */
      public void offer(Object data) throws Exception {
        if ((rear + 1) % queueData.length == front) {// 判定队列是否已满
          throw new Exception("队列已满");
        } else {
          queueData[rear] = data;
          rear = (rear + 1) % queueData.length;// 修改队尾指针
        }
      }
    
      /**
       * 队列删除数据
       * 
       * @throws Exception
       */
      public void poll() throws Exception {
        if (isEmpty()) {
          throw new Exception("队列为空");
        } else {
          queueData[front] = null;
          front = (front + 1) % queueData.length;
        }
      }
    
      /**
       * 判断队列是否为空
       * 
       * @return
       */
      public boolean isEmpty() {
        return rear == front;
      }
    
      /**
       * 返回队列的长度
       * 
       * @return
       */
      public int length() {
        return (rear - front + queueData.length) % queueData.length;
      }
    
      /**
       * 获取队列头
       * 
       * @return
       */
      public Object getFront() {
        return queueData[front].toString();
      }
    
      /**
       * 获取队尾元素
       * 
       * @return
       */
      public Object getRear() {
        return queueData[rear - 1].toString();
      }
    
      /**
       * 测试代码
       * 
       * @param args
       * @throws Exception
       */
      public static void main(String args[]) throws Exception {
        SeqCycleQueue queue = new SeqCycleQueue(6);
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        queue.offer(5);
        System.out.println("对头元素:" + queue.getFront());
        System.out.println("队尾元素:" + queue.getRear());
        System.out.println("队列长度:" + queue.length());
        System.out.println("头指针位置:" + queue.front);
        System.out.println("尾指针位置:" + queue.rear);
    
        queue.poll();
        queue.poll();
        queue.poll();
        queue.offer(6);
        queue.offer(7);
        System.out.println("对头元素:" + queue.getFront());
        System.out.println("队尾元素:" + queue.getRear());
        System.out.println("队列长度:" + queue.length());
        System.out.println("头指针位置:" + queue.front);
        System.out.println("尾指针位置:" + queue.rear);
      }
    }
    

    结果:
    在这里插入图片描述

    队列的链式存储

    队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已, 我们把它简称为链队列。 为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点,如下图所示:
    在这里插入图片描述
    空队列时, front 和 rear 都指向头结点,如图所示;
    在这里插入图片描述
    链队列实现可以参考之前的单链表实现,唯一不同的就是只能在队尾插入数据,在对头删除数据。

    对于循环队列与链队列的比较,可以从两方面来考虑:
    从时间上,其实它们的基本操作都是常数时间,即都为 0(1)的,不过循环队列是预先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
    从空间上,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域, 会产生一些空间上的开销,但也可以接受。 所以在空间上,链队列更加灵活。
    总的来说,在可以确定队列长度最大值的情况下 ,建议用循环队列,如果你无法预估队列的长度时,则用链队列。

    总结回顾

    又到了总结回顾的时间。我们前面讲的主要是栈和队列,它们都是特殊的线性表, 只不过对插入和删除操作做了限制。
    栈 (stack) 是限定仅在表尾进行插入和删除操作的线性表。
    队列 (queue) 是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
    它们均可以用线性表的顺序存储结构来实现,但都存在着顺序存储的一些弊端。
    因此它们各自有各自的技巧来解决这个问题。
    对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。
    对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列 ,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是 O(n)的时间复杂度变成了O(1)。

    队列
    顺序栈:两栈共享空间 顺序队列:循环队列
    链栈 链队列
  • 相关阅读:
    JavaSE 基础 第51节 定义自己的异常
    JavaSE 基础 第50节 Java中的异常链
    JavaSE 基础 第49节 手动抛出异常
    JavaSE 基础 第48节 Java中的异常声明
    JavaSE 基础 第47节 获取异常信息
    JavaSE 基础 第46节 异常的分类
    JavaSE 基础 第45节Java异常快速入门
    JavaSE 基础 第44节 引用外部类的对象
    JavaSE 基础 第43节 静态内部类
    通用爬虫
  • 原文地址:https://www.cnblogs.com/ZWOLF/p/10859279.html
Copyright © 2011-2022 走看看