队列的定义

队列是一种遵循 先进先出 原则的线性表。队列只允许在队列的尾部添加元素,在队列的头部删除元素。新添加的元素必须排在队列的末尾。

在现实生活中,最常见的队列的例子就是排队,例如超市里顾客在排队结账。
image.png

在计算机科学中,一个常见的例子就是打印队列。比如说我们需要打印五份文档。我们会打开每个文档,然后点击打印按钮。每个文档都会被发送至打印队列。第一个发送到打印队列的文档会首先被打印,以此类推,直到打印完所有的文档。

入队列

下面这张图展示了队列如何添加新的元素:
queue.jpg
左侧是队列的头部,右侧是队列的尾部,新的元素如果想进入队列,只能从尾部进入。1 从尾部进入队列后,2也只能从尾部进入队列,因此2只能跟在1后面,3只能跟在2后面。

出队列

如果队列中的元素想要出队列,只能从队列的头部出去,如下图:
queue (1).jpg
1 首先出队列,接着是2、3,它们出队列的顺序与入队的顺序是一致的。

基于对象的队列

相较于使用数组实现队列,使用Object对象来实现队列,在获取元素时更高效。因此我们使用 ES6 的类来创建一个基于Object 对象的队列:

  1. class Queue {
  2. constructor() {
  3. this.count = 0; // 队列的大小
  4. this.lowestCount = 0; // 追踪队列的头部元素,即头部元素的在对象中的 key
  5. this.items = {};
  6. }
  7. }

接下来,为队列声明一些队列可用的方法:

enqueue(element) 向队列尾部添加一个新的元素
dequeue() 移除队列头部的元素
head() 查看队列头部的元素(注意:不是删除,只是查看)
tail() 查看队列尾部元素(注意:不是删除,只是查看)
isEmpty() 判断队列是否为空
clear() 清空队列
size() 返回队列的大小
print() 打印队列

下面,我们逐一实现这些方法:

enqueue 向队列尾部添加元素

enqueue 方法负责向队列尾部添加新元素,注意,只能从尾部添加新元素

  1. // 向队列尾部添加新元素
  2. enqueue(element) {
  3. this.items[this.count] = element;
  4. this.count++;
  5. }

dequeue 移除队列头部元素

dequeue 方法负责移除队列头部的元素。由于队列遵循先进先出的原则,最先添加的元素也是最先被移除的。

  1. // 移除队列头部元素
  2. dequeue() {
  3. // 队列为空,返回 undefined
  4. if (this.isEmpty()) {
  5. return undefined;
  6. }
  7. // 缓存队列头部的元素,以便该元素被移除后将它返回
  8. const result = this.items[this.lowestCount];
  9. delete this.items[this.lowestCount];
  10. // lowestCount 属性加 1,即当前头部元素被移除后,下一个头部元素在对象中的 key
  11. this.lowestCount++;
  12. return result;
  13. }

head 查看队列头部元素

head 方法返回队列头部的元素,注意,这里只是查看,不是删除

  1. head() {
  2. // 队列为空,返回 undefined
  3. if (this.isEmpty()) {
  4. return undefined;
  5. }
  6. // lowestCount 作为键名来获取元素
  7. return this.items[this.lowestCount];
  8. }

tail 查看队列尾部元素

  1. tail() {
  2. if (this.isEmpty()) {
  3. return undefined;
  4. }
  5. return this.items[this.count - 1];
  6. }

isEmpty 判断队列是否为空

isEmpty 方法用来判断队列是否为空,如果队列为空,返回true,否则返回 false

  1. isEmpty() {
  2. return this.count - this.lowestCount === 0
  3. // return this.size() === 0
  4. }

clear 清空队列

clear 方法清空队列中的所有元素。我们可以调用 deqeue 方法直到它返回 undefined,也可以简单地将队列中的属性值重设为和构造函数中的一样。

  1. clear() {
  2. this.items = {};
  3. this.count = 0;
  4. this.lowestCount = 0;
  5. }

size 返回队列的大小

size 方法返回队列的大小,即返回队列中有多少个元素

  1. size() {
  2. // count 只会增加,不会减少,lowestCount 是元素在对象中的key
  3. // count 与 lowestCount 两者的差值就是队列中元素的个数
  4. return this.count - this.lowestCount;
  5. }

print 打印队列

print 方法负责以字符串的形式将队列打印出来

  1. print() {
  2. if (this.isEmpty()) {
  3. return '';
  4. }
  5. let objString = `${this.items[this.lowestCount]}`;
  6. for (let i = this.lowestCount + 1; i < this.count; i++) {
  7. objString = `${objString}, ${this.items[i]}`;
  8. }
  9. return objString;
  10. }

完整代码

  1. class Queue {
  2. constructor() {
  3. this.count = 0;
  4. this.lowestCount = 0;
  5. this.items = {};
  6. }
  7. enqueue(element) {
  8. this.items[this.count] = element;
  9. this.count++;
  10. }
  11. dequeue() {
  12. if (this.isEmpty()) {
  13. return undefined;
  14. }
  15. const result = this.items[this.lowestCount];
  16. delete this.items[this.lowestCount];
  17. this.lowestCount++;
  18. return result;
  19. }
  20. head() {
  21. if (this.isEmpty()) {
  22. return undefined;
  23. }
  24. // lowestCount 作为 键名来获取元素
  25. return this.items[this.lowestCount];
  26. }
  27. tail() {
  28. if (this.isEmpty()) {
  29. return undefined;
  30. }
  31. return this.items[this.count - 1];
  32. }
  33. isEmpty() {
  34. return this.count - this.lowestCount === 0;
  35. }
  36. clear() {
  37. this.items = {};
  38. this.count = 0;
  39. this.lowestCount = 0;
  40. }
  41. // count 只会增加,不会减少,lowestCount 是元素在对象中的 key
  42. // count 与 lowestCount 的差值就是队列中元素的个数
  43. size() {
  44. return this.count - this.lowestCount;
  45. }
  46. print() {
  47. if (this.isEmpty()) {
  48. return '';
  49. }
  50. let objString = `${this.items[this.lowestCount]}`;
  51. for (let i = this.lowestCount + 1; i < this.count; i++) {
  52. objString = `${objString}, ${this.items[i]}`;
  53. }
  54. return objString;
  55. }
  56. }

基于数组的队列

我们使用ES6的类来创建一个基于数组的队列:

  1. class Queue {
  2. constructor() {
  3. this.items = [];
  4. }
  5. }

同样,我们为队列声明以下队列可用的方法:

enqueue(element) 向队列尾部添加一个新的元素
dequeue() 移除队列头部的元素
head() 查看队列头部的元素(注意:不是删除,只是查看)
tail() 查看队列尾部的元素(注意:不是删除,只是查看)
isEmpty() 判断队列是否为空
clear() 清空队列
size() 返回队列的大小
print() 打印队列

enqueue 向队列尾部添加元素

队列是基于数组来实现的,因此可以使用数组的 push 方法向队列尾部添加新元素

  1. enqueue(element) {
  2. // 使用 数组的 push 方法向队列尾部添加新元素
  3. this.items.push(element);
  4. }

dequeue 移除队列头部元素

队列基于数组实现,我们同样可以使用数组的 shift 方法来移除队列头部的元素

  1. dequeue() {
  2. if (this.isEmpty()) {
  3. return undefined;
  4. }
  5. // 使用 数组 的 shift 方法移除队列头部元素
  6. return this.items.shift();
  7. }

head 查看队列头部元素

head 方法返回队列头部元素,即数组的第一个元素。注意:这里只是查看元素,不是删除

  1. head() {
  2. if (this.isEmpty()) {
  3. return undefined;
  4. }
  5. return this.items[0];
  6. }

tail() 查看队列尾部元素

  1. tail() {
  2. if (this.isEmpty()) {
  3. return undefined;
  4. }
  5. return this.items[this.items.length - 1]
  6. }

isEmpty 判断队列是否为空

isEmpty 方法判断队列是否为空,要判断队列是否为空,只需要判断数组的长度是否为 0 即可

  1. isEmpty() {
  2. return this.items.length === 0
  3. }

clear 清空队列

clear 方法用于清空队列,即将数组重置为一个空数组

  1. clear() {
  2. this.items = [];
  3. }

size 返回队列的大小

size 方法返回队列的大小,即返回的是数组的长度

  1. size() {
  2. return this.items.length;
  3. }

print 打印队列

print 方法以字符串的形式将队列中的元素打印出来

  1. pring() {
  2. return this.items.toString();
  3. }

完整代码

  1. class Queue {
  2. constructor() {
  3. this.items = []
  4. }
  5. enqueue(element) {
  6. this.items.push(element);
  7. }
  8. dequeue() {
  9. if (this.isEmpty()) {
  10. return undefined;
  11. }
  12. return this.items.shift();
  13. }
  14. head() {
  15. if (this.isEmpty()) {
  16. return undefined;
  17. }
  18. return this.items[0];
  19. }
  20. tail() {
  21. if (this.isEmpty()) {
  22. return undefined;
  23. }
  24. return this.items[this.items.length - 1]
  25. }
  26. isEmpty() {
  27. return this.items.length === 0;
  28. }
  29. clear() {
  30. this.items = [];
  31. }
  32. size() {
  33. return this.items.length;
  34. }
  35. print() {
  36. return this.items.toString();
  37. }
  38. }

基于链表实现队列

队列的方法与基于Object对象和基于数组实现的队列一致,我们不再赘述。

  1. class Queue {
  2. const linkList = new LinkList();
  3. // 入队列
  4. enqueue(element) {
  5. // 链表的 append 方法是向链表尾部添加元素
  6. // 对应于队列,就是在队列尾部添加元素
  7. return linkList.append(element);
  8. }
  9. // 出队列
  10. dequeue() {
  11. // 链表的 removeHead 方法是移除链表头部节点
  12. // 对应于队列,就是在队列头部移除元素
  13. return linkList.removeHead();
  14. }
  15. // 返回队首
  16. head() {
  17. // 链表的 head 方法返回的是链表的头节点
  18. // 对应于队列,就是返回队列的头部元素
  19. return linkList.head();
  20. }
  21. // 返回队尾
  22. tail() {
  23. // 链表的 tail 方法返回的是链表尾部的节点
  24. // 对应于队列,就是返回队列尾部的元素
  25. return linkList.tail();
  26. }
  27. // 判断队列是否为空
  28. isEmpty() {
  29. // 链表的 isEmpty 方法判断链表是否为空
  30. // 对应于队列,就是判读队列是否为空
  31. return link.isEmpty();
  32. }
  33. // 清空队列
  34. clear() {
  35. // 链表的 clear 方法清空链表
  36. // 对应于队列,就是清空队列
  37. linkList.clear();
  38. }
  39. // 队列长度
  40. size() {
  41. // 链表的 size 方法返回的是链表的长度
  42. // 对应于队列,就是返回队列的长度
  43. return linkList.size()
  44. }
  45. // 打印队列
  46. print() {
  47. // 链表的 toString 方法返回链表的字符串形式
  48. // 对应于队列,就是返回队列的字符串形式
  49. return linkList.toString()
  50. }
  51. }

队列的应用

约瑟夫环

有一个长度为 a 的数组,存放 0~99 的数字,要求每隔两个数删掉一个数,到末尾时循环至开头继续进行删除操作,求最后一个被删掉的数

思路分析

前10个数是 0 1 2 3 4 5 6 7 8 9,所谓每隔两个数删掉一个数,其实就是把 2、5、8 删除,也就是每 3 个数中删除最后一个数。

我们使用队列来解决这个问题,先把这 100 个数放入队列中,然后使用 while 循环,当队列里只剩一个元素时,就终止 while 循环。我们定义一个 index 变量从 0 开始计数,算法步骤如下:

  1. 从队列头部删除一个元素,然后 index 变量加 1
  2. 判断 index % 3 的结果是否为 0,如果为 0,就说明这个元素是需要删除的元素,如果不等于0,就不是需要被删除的元素,则把它添加到队列的尾部

不停的有元素被删除,最终队列里只有一个元素,此时 while 循环终止,队列里剩下的元素就是最后一个被删除的元素

算法实现

  1. const delRing = (arrList) => {
  2. const queue = new Queue();
  3. // 将数组里的元素放入队列中
  4. for (let i = 0; i < arrList.length; i++) {
  5. queue.enqueue(arrList[i]);
  6. }
  7. // index 变量用来计数
  8. let index = 0;
  9. while (queue.size() !== 1) {
  10. // 从队列中弹出一个元素,判断是否需要删除
  11. const item = queue.dequeue();
  12. index += 1;
  13. // 每隔两个就要删除一个,那么不是被删除的元素就放回到队列尾部
  14. if (index % 3 != 0) {
  15. queue.enqueue(item);
  16. }
  17. }
  18. return queue.head()
  19. }

测试结果:

  1. const arrList = []
  2. for (let i = 0; i < 100; i++) {
  3. arrList.push(i)
  4. }
  5. // 最后一个被删除的元素是 90
  6. delRing(arrList) // 90

斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……

斐波那契数列通常用 F(n) 表示,该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1

斐波那契数列是一个非常经典的问题,有着各种各样的解法,比较常见的是递归算法,其实也可以使用队列来实现。

思路分析:
先将 0 和 1 添加到队列中,然后使用 while 循环,使用 index 变量来计数,当 index < n - 2 时终止循环:

  • 使用 dequeue 方法从队列头部删除一个元素,,该元素为 del_item
  • 使用 head 方法获得队列头部的元素,该元素为 head_item
  • del_item + head_item = next_item,将 next_item 放入队列,注意:只能从队列尾部添加元素
  • 然后将变量 index 加 1

当循环结束时,队列里面会有两个元素,先使用 dequeue 方法删除头部元素,剩下的那个元素就是我们要计算的第 n 项的结果

算法实现:

  1. const fibonacci = (n) => {
  2. queue = new Queue();
  3. // index 变量用来计数
  4. let index = 0;
  5. // 先将 斐波那契数列 的前两个数 0 和 1 放入队列中
  6. queue.enqueue(0);
  7. queue.enqueue(1);
  8. while(index < n - 2) {
  9. // 从队列中删除头部元素
  10. const del_item = queue.dequeue();
  11. // 获取队列中的头部元素
  12. const head_item = queue.head();
  13. // 计算两个元素相加的结果
  14. const next_item = del_item + head_item;
  15. // 将计算结果放入队列中
  16. queue.enqueue(next_item);
  17. index += 1;
  18. }
  19. queue.dequeue();
  20. return queue.head()
  21. }
  22. console.log(fibonacci(8)) // 13

用队列实现栈

在 数据结构与算法学习之 栈 一文中,我们提到栈可以使用 JavaScript 中的数组和 Object对象来实现,其实栈也可以使用 队列 来实现。

队列的特点是 先进先出,而栈的特点是先进后出,两者对数据的管理模式刚好是相反的,但是却可以使用两个队列来实现一个栈。

思路分析:
我们将两个队列分别命名为 queue_1、queue_2:

  • push 实现 push 方法时,如果两个队列都为空,那么默认向 queue_1 里添加元素;如果有一个队列不为空,则向这个不为空的队列里添加元素;
  • top 实现 top 方法时,两个队列,或者都为空,或者有一个不为空,只需要返回不为空的队列的尾部元素即可;
  • pop pop 方法的实现是比较复杂的,pop 方法要删除的是栈顶元素,但这个栈顶元素其实是队列的尾部元素。每一次 pop 操作时,将不为空的队列里的元素依次删除并放入到另一个队列中,直到这个不为空的队列里只剩下一个元素,删除这个元素,其余的元素就都放到之前为空的队列中了。

在具体的实现中,我们额外定义两个变量 data_queue 和 empty_queue,data_queue 始终指向那个不为空的队列,empty_queue 始终指向那个为空的队列。

算法实现:

  1. class QueueStack {
  2. constructor() {
  3. this.queue_1 = new Queue();
  4. this.queue_2 = new Queue();
  5. this.data_queue = null; // 放数据的队列
  6. this.empty_queue = null; // 空队列,备份使用
  7. }
  8. init_queue = () => {
  9. if (this.queue_1.isEmpty() && this.queue_2.isEmpty()) {
  10. this.data_queue = this.queue_1;
  11. this.empty_queue = this.queue_2;
  12. } else if (this.queue_1.isEmpty()) {
  13. this.data_queue = this.queue_2;
  14. this.empty_queue = this.queue_1;
  15. } else {
  16. this.data_queue = this.queue_1;
  17. this.empty_queue = this.queue_2;
  18. }
  19. }
  20. push(item) {
  21. this.init_queue();
  22. this.data_queue.enqueue(item);
  23. }
  24. // top 方法返回栈顶元素,实质返回的是队列的尾部元素
  25. top() {
  26. this.init_queue();
  27. return this.data_queue.tail()
  28. }
  29. pop() {
  30. this.init_queue();
  31. // 将 不为空队列 data_queue 中的元素依次出队列并放入为空的 empty_queue 队列中
  32. while (this.data_queue.size() > 1) {
  33. const del_item = this.data_queue.dequeue()
  34. this.empty_queue.enqueue(del_item)
  35. }
  36. // 当 data_queue.size() 为 1 时,data_queue 中只剩一个元素,也就是队列的尾部元素
  37. // 将队列的尾部元素返回,即为栈顶元素
  38. return this.data_queue.dequeue()
  39. }
  40. }
  41. var q_stack = new QueueStack();
  42. q_stack.push(1);
  43. q_stack.push(2);
  44. q_stack.push(4);
  45. console.log(q_stack.top()); // 栈顶是 4
  46. console.log(q_stack.pop()); // 移除 4
  47. console.log(q_stack.top()); // 栈顶变成 2
  48. console.log(q_stack.pop()); // 移除 2
  49. console.log(q_stack.pop()); // 移除 2

打印杨辉三角

杨辉三角形,又称帕斯卡三角形贾宪三角形海亚姆三角形巴斯卡三角形,是二项式系数的一种写法,形似三角形,在中国首现于南宋杨辉的《详解九章算法》得名,书中杨辉说明是引自贾宪的《释锁算书》,故又名贾宪三角形。前 9 行写出来如下:
image.png
使用队列打印出杨辉三角的前 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 循环控制调用 enqueue 的次数,循环 5 次后,队列里存储的就是计算好的第 6 行的数据

第二种方法是每一行的数据后面多存储一个 0 ,使用这个 0 作为分界点,如果 dequeue 返回的是 0 ,就说明这一行已经全部输出,此时,将这个 0 追加到队列的尾部。

算法实现

实现方法一**
使用两层for 循环,第一层 for 循环控制打印的层数,第二层 for 循环控制打印每层的元素

  1. function print_yanghui_triangle(n) {
  2. const queue = new Queue();
  3. queue.enqueue(1);
  4. // 第一层 for 循环控制打印几层
  5. for (let i = 1; i <= n; i++) {
  6. const space = Math.floor((n - i))
  7. let line = new Array(space).fill(" ").toString().replace(/,/g, " ");
  8. let pre = 0;
  9. // 第二层 for 循环控制打印第 i 层的元素
  10. for (let j = 0; j < i; j++) {
  11. const item = queue.dequeue();
  12. line += item + " ";
  13. // 计算下一行的 值
  14. const value = item + pre;
  15. pre = item;
  16. queue.enqueue(value);
  17. // 每一次的最后一个数字是 1
  18. if (j === i - 1) {
  19. queue.enqueue(1);
  20. }
  21. }
  22. // 每一层的最后一个数字是 1,上面的 for 循环没有计算最后一个数
  23. // queue.enqueue(1);
  24. console.log(line)
  25. }
  26. }

实现方法二
每一行的数据后面多存储一个 0 ,使用这个 0 作为分界点

  1. function print_yanghui_triangle2(n) {
  2. const queue = new Queue();
  3. queue.enqueue(1);
  4. // 0 作为每一行数据的分节点
  5. queue.enqueue(0);
  6. for (let i = 1; i <= n; i++) {
  7. const space = Math.floor((n - i))
  8. let line = new Array(space).fill(" ").toString().replace(/,/g, " ");
  9. let pre = 0;
  10. while (true) {
  11. const item = queue.dequeue();
  12. // 使用 0 将每一行的数据分隔开,遇到 0 不输出
  13. if (item === 0) {
  14. queue.enqueue(1);
  15. queue.enqueue(0);
  16. break
  17. } else {
  18. // 计算下一行的值
  19. line += item + " ";
  20. const value = item + pre;
  21. pre = item;
  22. queue.enqueue(value)
  23. }
  24. }
  25. console.log(line)
  26. }
  27. }

参考资料
书籍:《JavaScript数据结构与算法》