《数据结构与算法之美》学习笔记五之队列

前情提要:上一章学习了栈相关的知识,主要有下面的内容:
  • 栈操作的时间复杂度,对于顺序栈,入栈时如果栈的空间不够涉及到数据搬移,此时使用摊还分析法,将数据搬移的耗时均摊到不需要搬移数据的入栈操作中,均摊时间复杂度等于最好情况时间复杂度 O(1)
  • 栈在函数调用中的应用,内存给每一个线程都分配了一块独立的内存空间,这些内存空间被组织成“栈”的形式,用来存储函数调用时的临时变量,当进入一个函数时,会将这个函数作为一个栈帧入栈,当函数执行完毕时,会将对应的栈帧出栈。
  • 栈在表达式中的应用,对于加减乘除等等数学表达式的运算,计算机理解起来是很困难的,需要使用栈来辅助。需要一个运算符栈和一个操作数栈,遍历表达式,当遇到操作数,就压入操作数栈,当遇到运算符,则需要和运算符栈的栈顶运算符比较优先级,如果栈顶元素优先级高,如果当前运算符优先级更高,就压入运算运算符栈,继续下次对比;
  • 如何使用栈实现浏览器的前进后退功能?和计算表达式的值有点类似,也是需要两个栈,当访问新页面的时候,把页面压入栈A;当点击后退时,取出栈A的栈顶元素,压入栈B;当点击前进时,从栈B中取出栈顶元素,压入栈A;如果要访问新页面,就需要清除栈B

这一章继续来学习队列
队列比栈复杂那么一丢丢

(一)队列基本概念

队列的基本概念很好理解,就类似于买票排队,先来的先买,后来的排到队尾,也就是咱们经常听到的“先进先出”
队列与栈类似,也只支持两种操作:“入队” 和 “出队”。“入队” 就是新增一个元素到队列的末尾,“出队” 就是从队列的头部取出一个元素。
在这里插入图片描述
所以队列和栈一样,是一种操作受限的线性表数据结构。

(二)顺序队列和链式队列

跟栈一样,队列也可以用数组和链表实现。用数组实现的叫顺序队列,用链表实现的叫链式队列
下面是使用 Java 语言写的队列的数组实现

// 用数组实现的队列
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的位置。
当我们调用了两次出队操作之后,head指针往后移动两位,指向索引为2的位置,tail指针还是指向索引为4的位置
在这里插入图片描述
随着不停地入队、出队操作,head 指针和 tail 指针都会往后移动,如果这两个指针重合,即使数组中还有位置,也没办法再进行入队操作了。
回想一下数组那一章节,删除一个元素,造成数组空间不连续,后续再往数组中增加元素的时候,这个空出来的空间是没有办法利用的。此时我们怎么解决数据不连续的问题呢?对,就是使用数据搬移
每次的出队操作都相当于删除了索引为0的元素。但是我们并不需要在每次出队的时候都进行数据搬移,这样子会导致出队的时间复杂度变为 O(n),只需要在入队时,发现没有空闲空间的时候,进行数据搬移。
这样子的话,队列的出队函数并不需要修改,入队函数需要修改一下下

// 入队操作,将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 指针移动到最右边后,再往队列中添加元素时,就会将 head 指针到 tail 指针之间的数据往前搬移,从索引为0开始,到 tail - head 结束
在这里插入图片描述
在这种实现思路下,出队操作的时间复杂度仍然是 O(1)。入队操作,我们之前在数组那一章节分析过,均摊时间复杂度就等于最好情况时间复杂度,也是 O(1)。
接下来我们再来看一下基于链表的队列实现方式。
基于链表的实现,我们同样需要两个指针,head 指针指向链表的第一个结点,tail 指针指向链表的最后一个结点。如下图所示,
入队时,tail.next = newNode;tail = tail.next
出队时,head = head.next

在这里插入图片描述

(三)循环队列

循环队列,顾名思义,就是原来的拉直的队列首尾相连成一个圈圈
在这里插入图片描述
我们可以发现,图中的队列大小为8,head指针指向 4 的位置,tail 指针指向 7 的位置。
当有一个新元素入队的时候,新元素会放在 7 的位置,tail 指针并不需要更新为 8 ,而是需要到 0 的位置,同样的,再次新增时,tail 指针继续往前移动,到 1 的位置。新增两个元素的状态如下图所示:
在这里插入图片描述
通过这种方式,在队列空间满之前,都不需要进行数据搬移操作。
但是循环队列的实现,相比于非循环队列,会复杂一些。关键在于两个状态的确定,一个是队列为空的状态,一个是队列满员的状态。
在非循环的队列中,队列为空的判断标准是 head == tail ,队列满员的判断标准是 tail == n
在循环队列中,队列为空的判断条件依然是 head == tail,队列满员的状态如下图所示
在这里插入图片描述
队列满员的判断条件,可以通过多🖼几次图总结出来,是 (tail+1)%n=head。当队列满的时候,tail 的位置是没有存储数据的,这会造成一丢丢内存的浪费。

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

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

  // 入队
  public boolean enqueue(String item) {
    // 队列满了
    if ((tail + 1) % n == head) return false;
    items[tail] = item;
    tail = (tail + 1) % n;
    return true;
  }

  // 出队
  public String dequeue() {
    // 如果head == tail 表示队列为空
    if (head == tail) return null;
    // 取出头部
    String ret = items[head];
    // 更新head到前一个位置
    head = (head + 1) % n;
    return ret;
  }
}

(四)阻塞队列

阻塞队列其实就是在队列的基础上增加了阻塞操作。当队列为空的时候,从对头取数据的操作就会被阻塞,等到队列中有数据的时候,在从队头取出数据并返回;入队操作也一样,当队列满的时候,入队操作会被阻塞,等到队列中有空位的时候才会执行插入操作,并返回。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值