队列3:LeetCode622:设计循环队列
Posted 纵横千里,捭阖四方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了队列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的队列。
以上是关于队列3:LeetCode622:设计循环队列的主要内容,如果未能解决你的问题,请参考以下文章