开始
队列是一种特殊的线性表,其特殊之处在于,它只允许你在队列的头部删除元素,在队列的末尾添加新的元素。
左侧是队列的头部,右侧是队列的尾部,新的元素如果想要进入队列,只能从尾部进入,如果想要出队列,只能从队列的头部出去。
队列的实现
有了栈这个数据结构做铺垫,队列就容易学习了
数据存储
同栈一样,我们的队列也用数组来存储数据,定义一个简单的类Queue
。
function Queue(){
var items = [];
}
数据将存储在items
数组之中。
队列的方法
队列的方法如下
enqueue
从队列尾部添加一个元素dequeue
从队列头部删除一个元素head
返回头部的元素size
返回队列的大小clear
清空队列isEmpty
判断队列是否为空tail
返回队列尾节点
enqueue
function Queue() {
var items = []
this.enqueue = function () {
items.push(item)
}
}
dequeue
function Queue() {
var items = []
this.enqueue = function () {
items.push(item)
}
this.dequeue = function () {
return this.shift()
}
}
head
function Queue() {
var items = []
this.enqueue = function () {
items.push(item)
}
this.dequeue = function () {
return this.shift()
}
this.head = function () {
return items[0]
}
}
tail
function Queue() {
var items = []
this.enqueue = function () {
items.push(item)
}
this.dequeue = function () {
return this.shift()
}
this.head = function () {
return items[0]
}
this.tail = function () {
return items[items.length - 1]
}
}
size
function Queue() {
var items = []
this.enqueue = function () {
items.push(item)
}
this.dequeue = function () {
return this.shift()
}
this.head = function () {
return items[0]
}
this.tail = function () {
return items[items.length - 1]
}
this.size = function () {
return items.length
}
}
clear
function Queue() {
var items = []
this.enqueue = function () {
items.push(item)
}
this.dequeue = function () {
return this.shift()
}
this.head = function () {
return items[0]
}
this.tail = function () {
return items[items.length - 1]
}
this.size = function () {
return items.length
}
this.clear = function () {
items = []
}
}
isEmpty
function Queue() {
var items = []
this.enqueue = function () {
items.push(item)
}
this.dequeue = function () {
return this.shift()
}
this.head = function () {
return items[0]
}
this.tail = function () {
return items[items.length - 1]
}
this.size = function () {
return items.length
}
this.clear = function () {
items = []
}
this.isEmpty = function () {
return items.length == 0
}
}
队列应用练习
约瑟夫环
题目要求
有一个数组a[100]
,存放0~99,要求每隔两个数删掉一个数,到末尾时循环到开头,继续进行,求最后一个被删掉的数。
思路分析
如果我们直接操作数组,问题会变得比较麻烦,关键是到了末尾如何回到开头重新来一遍,还得考虑把删掉的元素从数组中删除。数组下标会发上变化
如果用队列就比较简单了,先将这100个数放入队列,使用while
循环,while
循环终止的条件是队列中只有一个元素,使用index
从零开始计数,算法步骤如下:
- 维护一个计数器
index
- 从队列头部弹出一个元素,
index + 1
- 如果
index % 3 == 0
则说明这个元素应该被删除,如果不等于零,就不是需要删除的元素,将它添加到队列的尾部。
不停的有元素被删除,当队列中只剩下一个元素时,循环终止,队列中所剩下的就是最后一个被删除的元素。
function del_ring(arr_list) {
var queue = new Queue()
for (var i = 0;i<arr_list.length;i++) {
queue.enqueue(arr_list[i])
}
var index = 0;
while (queue.size() != 1) {
var item = queue.dequeue()
index += 1
// 每隔两个就要删除一个,那么不是被删除的元素就放回队列尾部
if (index%3 != 0){
queue.enqueue(item)
}
}
return queue.head()
}
var arr_list = [];
for (var i = 0;i<100;i++) {
arr_list.push(i)
}
console.log(del_ring(arr_list))
斐波那契数列
斐波那契数列是一个非常经典的问题,有着各种各样的解法,比较常见的是递归做法,其实也可以使用队列实现
题目要求
使用队列计算斐波那契数列的第n项
思路分析
斐波那契数列的前两项是 1 1,此后的每一项都是前两项之和,即f(n) = f(n-1) + f(n-2)
现将两个1放入队列中,然后使用while
循环,用index
计数,循环终止的条件是index < n-2
- 使用
dequeue
方法从队列的头部删除一个元素,该元素为del_item
- 使用
head
方法获得队列头部的元素,该元素为head_item
del_item + head_item = next_item
,将next_item
放入队列,注意,只能从尾部添加元素index + 1
当循环结束时,队列里有两个元素,我们先用dequeue
删除头部元素,剩下的那个元素就是我们想要的答案。
示例代码
function fibonacci(n) {
var queue = new Queue()
var index = 0
queue.enqueue(1)
queue.enqueue(1)
while (index < n-2) {
// 出队列一个元素
var del_item = queue.dequeue()
// 取队列头部元素
var head_item = queue.head()
// 将计算结果放入队列
var next_item = del_item + head_item
queue.enqueue(next_item)
index += 1
}
queue.dequeue()
return queue.head()
}
小结
使用队列的例子还有很多,比如逐层打印一棵树上的节点,比如kafka
,rabbitmq
这类消息队列,其形式就是一种队列,消息生产者把消息放入队列中(尾部),消费者从队列里取出消息进行处理(头部),只不过背后的实现较为复杂。
在socket
中,当大量客户端向服务端发起连接,而服务器忙不过来的时候,就会把这些请求放入队列中,先来的先处理,后来的后处理,队列满时,新来的请求直接抛弃掉。
队列实战
用队列实现栈
题目要求
用两个队列实现一个栈
思路分析
队列是先进先出,而栈是先进后出,两者对数据的管理模式刚好是相反的,但是可以用两个队列实现一个栈。
两个队列分别命名为queue_1
,queue_2
,实现思路如下:
push
,实现该方法时,如果两个队列都为空,那么默认向queue_1
里添加数据,如果有一个不为空,则向这个不为空的队列里添加数据top
,两个队列,或者都为空,或者有一个不为空,只需要返回不为空的队列的尾部元素即可pop
,该方法比较复杂,需要删除的是栈顶,但是这个栈顶元素其实是队列的尾部元素,所以每次做pop
操作时,将不为空的队列里的元素依次删除并放入另一个队列中直到遇到队列中只剩下一个元素,删除这个元素,其余元素都转移到之前的空队列中了。
在具体的实现中,我们定义两个额外的变量,data_queue
和empty_queue
,data_queue
始终指向那个不为空的队列,empty_queue
始终指向那个为空的队列。
代码示例
function QueueStack() {
var queue_1 = new Queue()
var queue_2 = new Queue()
var data_queue = null
var empty_queue = null
var init_queue = function () {
// 都为空 返回 queue_1
if (queue_1.isEmpty() && queue_2.isEmpty()) {
data_queue = queue_1
empty_queue = queue_2
}else if (queue_1.isEmpty()) {
data_queue = queue_2
empty_queue = queue_1
}else {
data_queue = queue_1
empty_queue = queue_2
}
}
this.push = function (item) {
init_queue()
data_queue.enqueue(item)
}
this.top = function () {
init_queue()
return data_queue.tail()
}
/**
pop 方法要弹出栈顶元素,这个栈顶元素,其实就是queue的队列尾部元素
但是队列的尾部元素是不能删除的,我们可以把data_queue里的元素(除了队列尾部元素)都移动到empty_queue中
最后移除data_queue的队列尾部元素并返回
*/
this.pop = function () {
init_queue()
while (data_queue.size() > 1) {
empty_queue.enqueue(data_queue.dequeue());
}
return data_queue.dequeue()
}
}
打印杨辉三角
题目要求
使用队列打印出杨辉三角的前n行,n>=1
思路分析
杨辉三角示意图如下
杨辉三角的每一行,都依赖于上一行,假设在队列里存储第n-1
行的数据,输出第n
行时,只需要将队列里的数据依次出队列,进行计算得到下一行的值并将计算所得放入到队列中。
计算方式如下f[i][j] = f[i-1][j-1] + f[i-1][j]
,i
代表行数,j
代表一行的第几个数。如果j=0
或者j=i
,则f[i][j] = 1
。
但是将计算所得放入队列中,队列中保存的是两行数据,一部分是n-1
行。另一部分是刚刚计算出来的第n
行数据,需要有办法把这两行数据分隔开,方法有如下两种:
- 使用
for
循环,在输出第5行的时候,其实只有5个数据可以输出,那么就可以使用for
循环控制调用equeue
的次数,循环5次之后,队列里存储的就是计算好的第6行的数据。 - 每一行数据后面多存储一个0,使用0作为分界点,如果
equeue
返回的是0,就说明这一行已经全部输出,此时,将这个0追加到队列的末尾。
示例代码
使用for
循环控制
function print_yanghui (n) {
var queue = new Queue()
// 添加进去第一个
queue.enqueue(1)
// 第一层的for循环控制打印几层
for (var i = 1;i<=n;i++) {
var line = ''
// 设置一个变量 用以暂存上一层弹出的数
var pre = 0
// 第二层for循环控制打印第i层
for (var j = 0;j<i;j++) {
// 出 n-1 层的数
var item = queue.dequeue()
line += item + '';
// 计算下一行的内容
var value = item + pre;
pre = item
queue.enqueue(value)
}
// 每一层最后一个数字没有计算,需要添加进来
queue.enqueue(1)
console.log(line)
}
}
使用边界标记
function print_yanghui (n) {
var queue = new Queue()
// 添加进去第一个
queue.enqueue(1)
// 添加分界符
queue.enqueue(0)
for (var i = 1;i<=n;i++) {
var line = ''
var pre = 0
while (true) {
var item = queue.dequeue()
// 用一个0将每一行分隔开,遇到0不输出
if (item == 0) {
// 添加末尾的1 和分隔符
queue.enqueue(1)
queue.enqueue(0)
break
}else {
line += item + ''
var value = item + pre;
pre = item
queue.enqueue(value)
}
}
console.log(line)
}
}
拓展:用两个栈实现一个队列
思路分析
我们所学习的每一种数据结构,本质上都研究的是对数据如何存储和使用。那么在考虑如何实现这种方法时,只有先增加,才能继续研究如何删除修改。
在这道题目中,我们先考虑如何实现enqueue
方法,两个栈分别命名为stack_1
和stack_2
,似乎只能选择一个栈来存储数据了,那就选择stack_1
来存储数据。
接下来考虑dequeue
方法,队列的头,在stack_1
的底部,栈是先进后出,目前取不到,我们可以把stack_1
的元素都存储到stack_2
中,这样,队列的头就变成了stack_2
的栈顶,就可以执行stack_2.pop()
来删除。此时,队列的头正好是stack_2
的栈顶,恰好可以操作,队列的dequeue
方法借助栈的pop
方法完成,队列的head
方法借助栈的top
方法完成。
如果stack_2
是空的,那就把stack_1
的元素都倒入到stack_2
就可以,如果stack_1
也是空的,说明队列也是空的,返回null
。
equeue
始终操作stack_1
,dequeue
和head
方法始终操作stack_2
。
function StackQueue() {
var stack_1 = new Stack()
var stack_2 = new Stack()
// 总是把数据放入 stack_1 中
this.enqueue = function () {
stack_1.push(item)
}
this.head = function () {
// 如果两个栈都是空的
if (stack_1.isEmpty()&&stack_2.isEmpty()) {
return null
}
//如果stack_2是空的,那么stack_1一定不为空,把stack_1中的元素倒入stack_2
if (stack_2.isEmpty()) {
while (!stack_1.isEmpty()) {
stack_2.push(stack_1.pop())
}
}
return stack_2.top()
}
// 出队列
this.dequeue = function () {
if (stack_1.isEmpty() && stack_2.isEmpty()) {
while (!stack_1.isEmpty()) {
return null
}
}
if (stack_2.isEmpty()) {
while (!stack_1.isEmpty()) {
stack_2.push(stack_1.pop())
}
}
return stack_2.pop()
}
}