队列3:LeetCode622:设计循环队列

​我们在一维数组中就提到,设计题虽然不烧脑,但是非常考验我们的基本功,我们应该注意一下相关练习,不应该觉得简单,但是最后只写出了学生级别的demo。今天就再来练习一下。

LeetCode622的题意也不复杂:

设计你的循环队列实现。循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。

  • Front: 从队首获取元素。如果队列为空,返回 -1 。

  • Rear: 获取队尾元素。如果队列为空,返回 -1 。

  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。

  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。

  • isEmpty(): 检查循环队列是否为空。

  • isFull(): 检查循环队列是否已满。

1.分析

当面试官将这个题目和你说完之后,该怎么一步步将其落地?上来就写代码吗?不是!而应该先继续和面试官沟通:

求职者:循环队列和链表都能实现循环队列,请问你想让我用哪种方式进行?

面试官:说数组或者链表。

求职者:如果是数组,那接下来应和面试官聊数组如何确定队空和队满。如果是链表,则应该讨论如何通过确定队尾和队头,也就是链表结构该如何设计。

如果这些基础问题都说完了,你很自信能写好,不妨再来一招反击:如何实现线性安全的队列。

我们逐步来分析。

2.数组实现

根据问题描述,该问题使用的数据结构应该是首尾相连的环。

任何数据结构中都不存在环形结构,但是可以使用一维 数组 模拟,通过操作数组的索引构建一个 虚拟 的环。很多复杂数据结构都可以通过数组实现。

对于一个固定大小的数组,任何位置都可以是队首,只要知道队列长度,就可以根据下面公式计算出队尾位置:

tailIndex=(headIndex+count−1)mod capacity

其中 capacity 是数组长度,count 是队列长度,headIndex 和 tailIndex 分别是队首 head 和队尾 tail 索引。下图展示了使用数组实现循环的队列的例子。

设计数据结构的关键是如何设计 属性,好的设计属性数量更少。属性数量少说明属性之间冗余更低。属性冗余度越低,操作逻辑越简单,发生错误的可能性更低。属性数量少,使用的空间也少,操作性能更高。

但是,也不建议使用最少的属性数量,这会导致代码的可读性很差,使用者会很费解。因此一定的冗余可以降低操作的时间复杂度,达到时间复杂度和空间复杂度的相对平衡。

那怎么保持平衡呢,假如这是要给其他人调用的基础代码,该怎么定义和实现呢?说人话, 写能看懂的代码就行了。

根据以上原则,列举循环队列的每个属性,并解释其含义。

queue:一个固定大小的数组,用于保存循环队列的元素。

headIndex:一个整数,保存队首 head 的索引。

count:循环队列当前的长度,即循环队列中的元素数量。使用 hadIndex 和 count 可以计算出队尾元素的索引,因此不需要队尾属性。

capacity:循环队列的容量,即队列中最多可以容纳的元素数量。该属性不是必需的,因为队列容量可以通过数组属性得到,但是由于该属性经常使用,所以我们选择保留它。这样可以不用在 Python 中每次调用 len(queue) 中获取容量。但是在 Java 中通过 queue.length 获取容量更加高效。为了保持一致性,在两种方案中都保留该属性。

class MyCircularQueue {  private int[] queue;  private int headIndex;  private int count;  private int capacity;  public MyCircularQueue(int k) {    this.capacity = k;    this.queue = new int[k];    this.headIndex = 0;    this.count = 0;  }   public boolean enQueue(int value) {    if (this.count == this.capacity)      return false;    this.queue[(this.headIndex + this.count) % this.capacity] = value;    this.count += 1;    return true;  }    public boolean deQueue() {    if (this.count == 0)      return false;    this.headIndex = (this.headIndex + 1) % this.capacity;    this.count -= 1;    return true;  }   public int Front() {    if (this.count == 0)      return -1;    return this.queue[this.headIndex];  }   public int Rear() {    if (this.count == 0)      return -1;    int tailIndex = (this.headIndex + this.count - 1) % this.capacity;    return this.queue[tailIndex];  }   public boolean isEmpty() {    return (this.count == 0);  }   public boolean isFull() {    return (this.count == this.capacity);  }}

3.单链表实现

单链表 和数组都是很常用的数据结构。

与固定大小的数组相比,单链表不会为未使用的容量预分配内存,因此它的内存效率更高。

单链表与数组实现方法的时间和空间复杂度相同,但是单链表的效率更高,因为这种方法不会预分配内存。

下图展示了单链表实现下的 enQueue() 和 deQueue() 操作。

列举循环队列中用到的所有属性,并解释其含义。

capacity:循环队列可容纳的最大元素数量。

head:队首元素索引。

count:当前队列长度。该属性很重要,可以用来做边界检查。

tail:队尾元素索引。与数组实现方式相比,如果不保存队尾索引,则需要花费 O(N) 时间找到队尾元素。

class Node {  public int value;  public Node nextNode;  public Node(int value) {    this.value = value;    this.nextNode = null;  }}class MyCircularQueue {  private Node head, tail;  private int count;  private int capacity;  public MyCircularQueue(int k) {    this.capacity = k;  }   public boolean enQueue(int value) {    if (this.count == this.capacity)      return false;    Node newNode = new Node(value);    if (this.count == 0) {      head = tail = newNode;    } else {      tail.nextNode = newNode;      tail = newNode;    }    this.count += 1;    return true;  }  public boolean deQueue() {    if (this.count == 0)      return false;    this.head = this.head.nextNode;    this.count -= 1;    return true;  }  public int Front() {    if (this.count == 0)      return -1;    else      return this.head.value;  }  public int Rear() {    if (this.count == 0)      return -1;    else      return this.tail.value;  }  public boolean isEmpty() {    return (this.count == 0);  }  public boolean isFull() {    return (this.count == this.capacity);  }}

4.如何设计线性安全的队列

如果将上面两种的一种顺利实现了,那我们不妨再来提升一下:如何设计线性安全的队列。如果你能想到这一点,还能写好,自然会给面试官不错的映像。

上面实现满足所有的要求,但是可能存在一些风险。

从并发性来看,该循环队列是线程不安全的。

例如:下图的执行序列超出了队列的设计容量,会覆盖队尾元素。

这种情况称为竞态条件。并发安全的解决方案很多,我们这里使用重入锁。

以方法 enQueue(int value) 为例,说明该方法的并发安全实现。

class MyCircularQueue {  private Node head, tail;  private int count;  private int capacity;  private ReentrantLock queueLock = new ReentrantLock(); public MyCircularQueue(int k) {    this.capacity = k;  }  public boolean enQueue(int value) {     queueLock.lock();    try {      if (this.count == this.capacity)        return false;      Node newNode = new Node(value);      if (this.count == 0) {        head = tail = newNode;      } else {        tail.nextNode = newNode;        tail = newNode;      }      this.count += 1;    } finally {      queueLock.unlock();    }    return true;  }}

我们可能习惯使用synchronized来加锁,但是这个锁太重了,使用重入锁ReentrantLock就可以。

题外话:你知道重入锁ReentrantLock的底层是什么吗?其实也是一个牛B的队列。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值