周末和小伙伴玩马里奥派对贼有意思,安利
正题
设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k)
: 构造器,设置队列长度为 k 。Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。
解析:
依题目,乍一看需要实现6个方法以及一个构造器。假设我们不考虑循环队列,那么应该如何实现以上6个方法和构造器呢?
依照题意,实现的无非就是 取首,取尾,插,删,判空,判满 而已,如果单纯通过数组去实现也是非常简单的。
-
构造器:
构造器的入参为队列长度,那么我们可以定义队列长度变量(判满使用),以及队列实体Array
/**
* @param {number} k
*/
var MyCircularQueue = function(k) {
this.length = k //队列长度
this.stack = [] // 队列
};
-
取首
若队列为空,那么
return -1
, 否则return {数组第一个元素}
/**
* @return {number}
*/
MyCircularQueue.prototype.Front = function() {
if (this.stack.length === 0) {
return -1
} else {
return this.stack[0]
}
};
-
取尾
若队列为空,那么
return -1
, 否则return {数组第最后元素}
/**
* @return {number}
*/
MyCircularQueue.prototype.Rear = function() {
return this.stack.length ? this.stack[this.stack.length - 1] : -1
};
-
插入
若队列不满,则
push
并且return true
, 否则return false
MyCircularQueue.prototype.enQueue = function(value) {
if (this.stack.length < this.length) {
this.stack.push(value)
return true
} else {
return false
}
};
-
删除
若队列为空则
return false
, 否则根据先进先出原则unshift
弹出第一个元素, 并且return true
if (this.stack.length > 0) {
this.stack.shift()
return true
} else {
return false
}
- 判空
return this.stack.length === 0
- 判满
return this.stack.length === this.length
提交!
提交通过了,结果看起来很满意!但是这真的是实现了循环队列吗??真的就符合题意吗?
在回答这个问题之前,我们现了解一下循环队列!
什么是循环队列?
百度给出的解释: 为充分利用向量空间,克服"假溢出"现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
可以很快提取出两个关键词,一是循环,而是队列(先进先出)。即使我们使用 push(进) shift(出)也仅满足了先进先出,并表现出循环的特点(首尾相接成环),所以以上的实现虽然通过了程序的判定,但实际上并不是循环队列的实现!
从实现构造器开始
构造器的入参是 队列长度,那么我们队列就应该持有队列长度变量,通常用 capacity
表示,其次需要实现首尾相接,也就必须要有首尾指针,分别为 front
和 rear
,另外需要一个饿存放数据的空间,可用数组来代替,综上所述,那么我们的构造器雏形基本就有了。
/**
* @param {number} k
*/
var MyCircularQueue = function(k) {
this.front = 0
this.rear = 0
this.capacity = k
this.queue = new Array(k)
};
如何判断空?
从简单到难,假定一个循环队列是空的,那么他会长什么样子呢?一图说明!
这是一个长度为7的队列,它里面什么都没有,那么我们带入构造函数中的变量看看
图中看出,首尾都在第0节点,长度为7的空队列。可以提取出以下几点信息:
front
和rear
在同一节点front
节点为空
ok,那么判断循环列表为空的条件也就具备了
/**
* @return {boolean}
*/
MyCircularQueue.prototype.isEmpty = function() {
return this.rear === this.front && !this.queue[this.front]
};
插入队列是如何进行的?
在队列中插入元素,实际上可以理解为,队列尾指针 +1
光看到这里我们会在实现队列插入的时候写出这样的代码:
/**
* @param {number} value
* @return {boolean}
*/
MyCircularQueue.prototype.enQueue = function(value) {
this.queue[this.rear] = value // 放入队列
this.rear = this.rear + 1 // rear 指针右移
};
实际上是不够全面的,没有考虑到边界,即:当列表满时,会出现什么情况
所以,我们让插入继续走下去,我们继续插入:
由于队列是循环的,那么我们可以看到当队列被塞满的时候, rear就会被重置到首位。如果按照上面的写法this.rear = this.rear + 1
, rear
就会被一直增加,不仅超过了队列的最大长度,也没有实现真正的循环。那么应该去如何定义 rear
的表达式呢?
可以理解为,当 rear 达到7的时候,就会从1开始,像极了取余数,所以 this.rear = (this.rear + 1) % this.capacity
,理解不过来的同学可以实际代入一下就可以理解了。
最终可将代码修改为:
/**
* @param {number} value
* @return {boolean}
*/
MyCircularQueue.prototype.enQueue = function(value) {
if (this.isFull()) {
return false // 队列满了
} else {
this.queue[this.rear] = value
this.rear = (this.rear + 1) % this.capacity
return true
}
};
如何判断队列满了呢?
在添加队列的同时,有限制条件,即:队列满时,队列将不会被添加。并且 return false
,那么重要的条件就是如何判断队列满了呢?那么接下来就实现 isFull()
方法。
其实在上图中已经表现出:
不难发现, rear
指针回到了首位,并且和 front 重合。所以有
this.rear === this.front
但是需要区分空队列的情况:
这种情况也是 this.rear === this.front
,唯一不同的就是队列中没有任何元素,所以我们要判断是否是因为队列满了而达到重复的情况,可以得到:
this.rear === this.front
!!this.queue[this.front]
实现 isFull()
/**
* @return {boolean}
*/
MyCircularQueue.prototype.isFull = function() {
return this.rear === this.front && !!this.queue[this.front]
};
开始删除吧
增加实现完了,开始实现删除。首先了解删除节点的操作:
由上图可知删除节点的操作仅仅对 front进行操作,即
- 删除
front
指针对应的元素 - front 指针右移
- 当队列为空时,不进行删除操作
return false
ps: 考虑边界问题, front右移应采用 取余数的方法,表达循环链表的特点
实现:
/**
* @return {boolean}
*/
MyCircularQueue.prototype.deQueue = function() {
if (this.isEmpty()) {
return false
} else {
this.queue[this.front] = null
this.front = (this.front + 1) % this.capacity
return true
}
};
取出首元素
取出首元素,即找到 front 指针所对应的节点
/**
* @return {number}
*/
MyCircularQueue.prototype.Front = function() {
return this.isEmpty() ? -1 : this.queue[this.front]
};
仅仅要考虑如果队列为空则 return -1
即可
取出尾元素
这个相对首元素来说稍微复杂一点,因为 rear
指向的是尾元素下一个节点的指针,所以应该取 rear - 1
指针所对应的节点。另外,考虑 rear
是循环过来的第一个节点,那么就取出最后一个节点即可
/**
* @return {number}
*/
MyCircularQueue.prototype.Rear = function() {
let rearIndex = this.rear === 0 ? this.capacity - 1 : this.rear - 1
return this.isEmpty() ? -1 : this.queue[rearIndex]
};
至此,上述应题要求的所有方法均已实现,提交!
结果是通过的,并且也是完全实现了循环队列的所有特性!这样才是应题的解答!
完整代码:
/**
* @param {number} k
*/
var MyCircularQueue = function(k) {
this.front = 0
this.rear = 0
this.capacity = k
this.queue = new Array(k)
};
/**
* @param {number} value
* @return {boolean}
*/
MyCircularQueue.prototype.enQueue = function(value) {
if (this.isFull()) {
return false
} else {
this.queue[this.rear] = value
this.rear = (this.rear + 1) % this.capacity
return true
}
};
/**
* @return {boolean}
*/
MyCircularQueue.prototype.deQueue = function() {
if (this.isEmpty()) {
return false
} else {
this.queue[this.front] = null
this.front = (this.front + 1) % this.capacity
return true
}
};
/**
* @return {number}
*/
MyCircularQueue.prototype.Front = function() {
return this.isEmpty() ? -1 : this.queue[this.front]
};
/**
* @return {number}
*/
MyCircularQueue.prototype.Rear = function() {
let rearIndex = this.rear === 0 ? this.capacity - 1 : this.rear - 1
return this.isEmpty() ? -1 : this.queue[rearIndex]
};
/**
* @return {boolean}
*/
MyCircularQueue.prototype.isEmpty = function() {
return this.rear === this.front && !this.queue[this.front]
};
/**
* @return {boolean}
*/
MyCircularQueue.prototype.isFull = function() {
return this.rear === this.front && !!this.queue[this.front]
};
/**
* Your MyCircularQueue object will be instantiated and called as such:
* var obj = new MyCircularQueue(k)
* var param_1 = obj.enQueue(value)
* var param_2 = obj.deQueue()
* var param_3 = obj.Front()
* var param_4 = obj.Rear()
* var param_5 = obj.isEmpty()
* var param_6 = obj.isFull()
*/
通过这次的题解,是否更进一步了解了设么是循环队列了呢?循环队列的优点也可以一目了然了,空间还是队列的最大空间,但是可以实现增删取,达到了空间利用的最大化。
那么什么事百度词条中解释的: 假溢出
?
通过增加队列节点,可以看出,当队列还没有满时,但rear指针可能已经到了最后节点,那么 rear 节点右移可能会导致溢出,但实际上因为循环队列的特性,rear
节点会重新回到首个节点的位置,并不会造成溢出。