要点:基本队列、优先队列、双端队列、循环队列、对应练习
先知:队列(Queue)是一种运算受限的线性表,特点:先进先出 。(FIFO:First In First Out)
一、认识队列
1.1 定义
队列(Queue)是一种运算受限的线性表,特点:先进先出。(FIFO:First In First Out)
受限之处:
只允许在表的前端(front)进行删除操作。
只允许在表的后端(rear)进行插入操作。
生活中类似队列结构的场景:
排队,比如在电影院,商场,甚至是厕所排队。
优先排队的人,优先处理。 (买票、结账、WC)。
1.2 队列图解
1.3 队列在程序中的应用
打印队列:计算机打印多个文件的时候,需要排队打印。
线程队列:当开启多线程时,当新开启的线程所需的资源不足时就先放入线程队列,等待 CPU 处理。
我们在做性能优化的时候,很多时候会提到的一点就是“HTTP 1.1 的队头阻塞问题”,具体来说就是 HTTP2 解决了 HTTP1.1 中的队头阻塞问题,但是为什么 HTTP1.1 有队头阻塞问题,HTTP2 究竟怎么解决的这个问题,很多人都不清楚。
其实队头阻塞
是一个专有名词,不仅仅在 HTTP 有,交换器等其他地方也都涉及到了这个问题。实际上引起这个问题的根本原因是使用了队列
这种数据结构。
协议规定, 对于同一个 tcp 连接,所有的 http1.0 请求放入队列中,只有前一个请求的响应
收到了,才能发送下一个请求,这个时候就发生了阻塞,并且这个阻塞主要发生在客户端。
这就好像我们在等红绿灯,即使旁边绿灯亮了,你的这个车道是红灯,你还是不能走,还是要等着。
HTTP/1.0 和 HTTP/1.1
在HTTP/1.0
中每一次请求都需要建立一个 TCP 连接,请求结束后立即断开连接。
在HTTP/1.1
中,每一个连接都默认是长连接 (persistent connection)。对于同一个 tcp 连接,允许一次发送多个 http1.1 请求,也就是说,不必等前一个响应收到,就可以发送下一个请求。这样就解决了 http1.0 的客户端的队头阻塞 ,而这也就是HTTP/1.1
中管道 (Pipeline)
的概念了。
但是,http1.1 规定,服务器端的响应的发送要根据请求被接收的顺序排队
,也就是说,先接收到的请求的响应也要先发送。这样造成的问题是,如果最先收到的请求的处理时间长的话,响应生成也慢,就会阻塞已经生成了的响应的发送,这也会造成队头阻塞。可见,http1.1 的队首阻塞是发生在服务器端。
HTTP/2 和 HTTP/1.1
为了解决HTTP/1.1
中的服务端队首阻塞,HTTP/2
采用了二进制分帧
和 多路复用
等方法。
帧是HTTP/2
数据通信的最小单位。在 HTTP/1.1
中数据包是文本格式,而 HTTP/2
的数据包是二进制格式的,也就是二进制帧。
采用帧的传输方式可以将请求和响应的数据分割得更小,且二进制协议可以被高效解析。HTTP/2
中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。
多路复用
用以替代原来的序列和拥塞机制。在HTTP/1.1
中,并发多个请求需要多个 TCP 链接,且单个域名有 6-8 个 TCP 链接请求限制(这个限制是浏览器限制的,不同的浏览器也不一定一样)。在HTTP/2
中,同一域名下的所有通信在单个链接完成,仅占用一个 TCP 链接,且在这一个链接上可以并行请求和响应,互不干扰。
此网站 可以直观感受 HTTP/1.1
和 HTTP/2
的性能对比。
二、队列的实现
队列的实现和栈一样,有两种方案:
2.1 队列常见的操作
enqueue(element)
向队列尾部添加一个(或多个)新的项。
dequeue()
移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。
front()
返回队列中的第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息与 Map 类的 peek 方法非常类似)。
isEmpty()
如果队列中不包含任何元素,返回 true,否则返回 false。
size()
返回队列包含的元素个数,与数组的 length 属性类似。
toString()
将队列中的内容,转成字符串形式。
2.2 代码实现
顺序队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Queue { constructor ( ) { this .items = []; } enqueue (item ) { this .items.push(item); } dequeue ( ) { return this .items.shift(); } front ( ) { return this .items[0 ]; } isEmpty ( ) { return this .items.length === 0 ; } size ( ) { return this .items.length; } toString ( ) { let result = "" ; for (let item of this .items) { result += item + " " ; } return result; } }
链式队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 function LinkedQueue ( ) { let Node = function (ele ) { this .ele = ele; this .next = null ; } let length = 0 , front, rear; this .push = function (ele ) { let node = new Node(ele), temp; if (length == 0 ) { front = node; } else { temp = rear; temp.next = node; } rear = node; length++; return true ; } this .pop = function ( ) { let temp = front; front = front.next length--; temp.next = null return temp; } this .size = function ( ) { return length; } this .getFront = function ( ) { return front; } this .getRear = function ( ) { return rear; } this .toString = function ( ) { let string = '' , temp = front; while (temp) { string += temp.ele + ' ' ; temp = temp.next; } return string; } this .clear = function ( ) { front = null ; rear = null ; length = 0 ; return true ; } } let myQueue = new LinkedQueue();myQueue.push(1 ) myQueue.push(2 ) myQueue.push(3 ) myQueue.push(4 ) console .log(myQueue.toString()) console .log(myQueue.pop()) console .log(myQueue.toString())
2.3 测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const queue = new Queue();queue.enqueue("a" ); queue.enqueue("b" ); queue.enqueue("c" ); queue.enqueue("d" ); console .log(queue.items); queue.dequeue(); queue.dequeue(); console .log(queue.items); console .log(queue.front()); console .log(queue.isEmpty()); console .log(queue.size()); console .log(queue.toString());
三、优先队列
3.1 定义
生活中类似优先队列 的场景:
优先排队的人,优先处理。 (买票、结账、WC)。
排队中,有紧急情况(特殊情况)的人可优先处理。
所谓优先队列,顾名思义,就是说插入到队列中的元素可以根据优先级设置先后顺序。优先级越高位置越靠前,优先级越低位置越靠后。假设优先级用数字来表示,如果数字越小表示的优先级越高,形成的队列就称之为最小优先队列,反之则称之为最大优先队列。
3.2 代码实现
优先级队列主要考虑的问题:
每个元素不再只是一个数据,还包含优先级。
在添加元素过程中,根据优先级放入到正确位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 class QueueElement { constructor (element, priority ) { this .element = element; this .priority = priority; } } export class PriorityQueue extends Queue { constructor ( ) { super (); } enqueue (element, priority ) { const queueElement = new QueueElement(element, priority); if (this .isEmpty()) { this .items.push(queueElement); } else { let added = false ; for (let i = 0 ; i < this .items.length; i++) { if (queueElement.priority < this .items[i].priority) { this .items.splice(i, 0 , queueElement); added = true ; break ; } } if (!added) { this .items.push(queueElement); } } } dequeue ( ) { return super .dequeue(); } front ( ) { return super .front(); } isEmpty ( ) { return super .isEmpty(); } size ( ) { return super .size(); } toString ( ) { let result = "" ; for (let item of this .items) { result += item.element + "-" + item.priority + " " ; } return result; } }
可以看到,唯一有区别的只有enqueue
方法。我们规定所有添加到优先队列的元素都必须满足{element, priority}这种JSON格式,以保证队列中的每一个元素都有一个priority属性来表示优先级。如果要添加的元素的优先级和队列中已有元素的优先级相同,仍然遵循队列的先进先出原则。如果队列中所有元素的优先级比要添加的元素的优先级都高,则将元素添加到队列的末尾。
3.3 测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const priorityQueue = new PriorityQueue();priorityQueue.enqueue("A" , 10 ); priorityQueue.enqueue("B" , 15 ); priorityQueue.enqueue("C" , 11 ); priorityQueue.enqueue("D" , 20 ); priorityQueue.enqueue("E" , 18 ); console .log(priorityQueue.items);priorityQueue.dequeue(); priorityQueue.dequeue(); console .log(priorityQueue.items);console .log(priorityQueue.isEmpty()); console .log(priorityQueue.size()); console .log(priorityQueue.toString());
四、双端队列
4.1 定义
允许从前端(front)和后端(rear)添加元素, 遵循的原则先进先出或后进先出.
双端队列可以理解为就是栈(后进先出)和队列(先进先出)的一种结合体. 既然是结合那么相应的操作也支持队列,栈的操作。
4.2 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 constructor ( ) { this .items = {} this .count = 0 this .lowestCount = 0 } addFront (ele ) { if (this .isEmpty()) { this .items[this .count] = ele } else if (this .lowestCount > 0 ) { this .lowestCount -= 1 this .items[this .lowestCount] = ele } else { for (let i = this .count; i > 0 ; i--) { this .items[i] = this .items[i - 1 ] } this .items[0 ] = ele } this .count++ return ele } removeFront ( ) { if (this .isEmpty()) { return } const delEle = this .items[this .lowestCount] delete this .items[this .lowestCount] this .lowestCount++ return delEle } addBack (ele ) { this .items[this .count] = ele this .count++ } removeBack ( ) { if (this .isEmpty()) { return } const delEle = this .items[this .count - 1 ] delete this .items[this .count - 1 ] this .count-- return delEle } peekFront ( ) { if (this .isEmpty()) { return } return this .items[this .lowestCount] } peekBack ( ) { if (this .isEmpty()) { return } return this .items[this .count - 1 ] } size ( ) { return this .count - this .lowestCount } isEmpty ( ) { return this .size() === 0 } clear ( ) { this .items = {} this .count = 0 this .lowestCount = 0 } toString ( ) { if (this .isEmpty()) { return '' } let objString = `${this .items[this .lowestCount]} ` for (let i = this .lowestCount + 1 ; i < this .count; i++){ objString = `${objString} , ${this .items[i]} ` } return objString } }
五、循环队列
5.1 定义
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。 循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。 你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
想要实现上面的功能,其实主要就是判断循环队列是否为空isEmpty
,是否已满isFull
,这里我们使用两个指针来表示队首(head
)和队尾的指针(tail
)。假设初始化的时候两个指针的值都是-1
。那么显然是否为空的判断条件就是两个指针都是-1
的时候。
5.2 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 var MyCircularQueue = function (k ) { this .size = k this .head = -1 this .tail = -1 this .data = [] }; MyCircularQueue.prototype.enQueue = function (value ) { if (this .isFull()) { return false } if (this .isEmpty()) { this .head = 0 } this .tail = (this .tail + 1 )%this .size this .data[this .tail] = value return true }; MyCircularQueue.prototype.deQueue = function ( ) { if (!this .isEmpty()) { if (this .tail === this .head) { this .tail = -1 this .head = -1 } else { this .head = (this .head + 1 )%this .size } return true } return false }; MyCircularQueue.prototype.Front = function ( ) { return this .head === -1 ? -1 : this .data[this .head] }; MyCircularQueue.prototype.Rear = function ( ) { return this .tail === -1 ? -1 : this .data[this .tail] }; MyCircularQueue.prototype.isEmpty = function ( ) { return this .tail === -1 && this .head === -1 }; MyCircularQueue.prototype.isFull = function ( ) { return (this .tail + 1 )%this .size === this .head }; MyCircularQueue.createNew = function (k ) { return new MyCircularQueue(k) };
六、队列的应用
6.1 击鼓传花-循环队列
使用队列实现小游戏:击鼓传花。
分析:现实例子就是击鼓传花(Hot Potato),在游戏中,孩子们围着圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈结束游戏。重复这个过程,直到只剩一个孩子(胜利)。
传入一组数据集合和设定的数字 number,循环遍历数组内元素,遍历到的元素为指定数字 number 时将该元素删除,直至数组剩下一个元素。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function passGame (nameList, number ) { const queue = new Queue(); for (const name of nameList) { queue.enqueue(name); } while (queue.size() > 1 ) { for (let i = 0 ; i < number - 1 ; i++) { queue.enqueue(queue.dequeue()); } queue.dequeue(); } const endName = queue.front(); return nameList.indexOf(endName); }
测试代码
1 2 3 4 const names = ["lily" , "lucy" , "tom" , "tony" , "jack" ];const targetIndex = passGame(names, 4 );console .log("击鼓传花" , names[targetIndex]);
6.2 基数排序
使用队列实现基数排序
队列不仅用于执行现实生活中关于队列相关的操作,还可以用于对数据进行排序。计算机刚刚出现的时候,程序是通过穿孔输入主机的,每一张卡包含一条程序语句。这些穿孔卡装在一个盒子里面,经过一个机械装置进行排序。我们可以用一组队列来模拟这个过程。这种排序技术 叫做基数排序 。
对于0~99的数字,基数排序将数据集扫描两次。第一次按照个位上的数字进行排序,第二次按照十位上的数字进行排序。每个数组根据对应位上的数字被分配在不同的盒子里。
举例如下:假如有数字 91,46,85,15,92,35,31,22
经过基数排序第一次扫描之后按照个位数的大小排序 ,数字被分配大如下的盒子中:
第0个盒子:
第1个盒子:91,31
第2个盒子:92,22
第3个盒子:
第4个盒子:
第5个盒子:85,15,35
第6个盒子:46
第7个盒子:
第8个盒子:
第9个盒子:
根据盒子的顺序,对数字经行第一次排序的结果如下:
91,31,92,22,85,15,35,46
然后根据十位上的数值再将上次排序结果分配到不同的盒子里
第0个盒子:
第1个盒子:15
第2个盒子:22
第3个盒子:31,35
第4个盒子:46
第5个盒子:
第6个盒子:
第7个盒子:
第8个盒子:85
第9个盒子:92,92
最后将盒子里的数字取出,组成一个新的列表,该列表即为排好顺序的数字:
15,22,31,35,46,85,91,92
使用队列代表盒子,可以实现这个算法,我们需要9个队列,每个对应一个数字。将所有队列保存在一个数组中,使用取余和出发操作决定各位和十位。算法的剩余部分将数字加入对应的队列,根据个位数值重新排序,然后再根据十位数值经行排序,结果即为排好顺序的数字。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 function Queue ( ) { this .dataStore = []; this .enqueue = enqueue; this .dequeue = dequeue; this .front = front; this .back = back; this .toString = toString; this .empty = empty; } function enqueue (element ) { this .dataStore.push(element); } function dequeue ( ) { return this .dataStore.shift(); } function front ( ) { return this .dataStore[0 ]; } function back ( ) { return this .dataStore[this .dataStore.length-1 ]; } function toString ( ) { var retStr = "" ; for (var i=0 ; i<this .dataStore.length; ++i) { retStr += this .dataStore[i] + " " } return retStr; } function empty ( ) { if (this .dataStore.length == 0 ){ return true ; }else { return false ; } } function count ( ) { return this .dataStore.length; } function distribute (nums,queues,n,digit ) { for (var i=0 ; i<n; ++i) { if (digit == 1 ){ queues[nums[i]%10 ].enqueue(nums[i]); }else { queues[Math .floor(nums[i] / 10 )].enqueue(nums[i]); } } } function collect (queues,nums ) { var i=0 ; for (var digit=0 ; digit<10 ; ++digit) { while (!queues[digit].empty()){ nums[i++] = queues[digit].dequeue(); } } } function dispArray (arr ) { for (var i=0 ; i<arr.length; ++i) { document .write(arr[i] + " " ); } } var queues = [];for (var i = 0 ; i < 10 ; i++) { queues[i] = new Queue(); } var nums = [];for (var i=0 ; i<10 ; ++i) { nums[i] = Math .floor(Math .random()*100 ); } document .write('排序之前' );document .write('<br>' );dispArray(nums); document .write('<br>' );distribute(nums, queues, 10 , 1 ); collect(queues, nums); distribute(nums, queues, 10 , 10 ); collect(queues, nums); document .write("排序之后" );document .write('<br>' );dispArray(nums);
6.3 银行排队
实现思路:第一个客户到达的时刻为0,之后每个客户到达的时刻在前一个客户到达时设定随机值,因此在客户到达时需要产生两个随机数,一个是客户办理业务耗时durtime,一个是下一客户到达时间间隔intertime,假设当前时间为occurtime,则下一客户到达时为occurtime+intertime。
刚到达的客户应该插入到当前含元素最少的队列中。
在JavaScript的运用中,通常使用队列来进行任务的排序。而任务队列的任务是按进入队列的顺序延迟执行(解决状态一致性)的,即当前一个任务完成后,后面的任务才被执行,如果当前没有任务,则入队列的任务立即执行
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function taskQueue ( ) { taskList = []; var isRun = false ; this .addTask = function (task ) { taskList.push(task); }; setInterval (function ( ) { if (taskList.length > 0 && !isRun) { isRun = true ; taskList.shift(); isRun = false ; } }, 100 ); } function show ( ) { alert(taskList); } taskQueue(); addTask(1 ); addTask(2 ); addTask(3 ); setTimeout ('show()' ,99 );setTimeout ('show()' ,101 );setTimeout ('show()' ,400 );
6.4 判断回文
上一篇栈 (使用栈判断回文 )中也有涉及回文的实现, 下面我们通过双端队列 来实现同样的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function palindromeChecker (aString ) { if (!aString || typeof aString !== 'string' || !aString.trim().length) { return false } const deque = new Deque() const lowerString = aString.toLowerCase().split(' ' ).join('' ) for (let i = 0 ; i < lowerString.length; i++) { deque.addBack(lowerString[i]) } let isEqual = true let firstChar = '' let lastChar = '' while (deque.size() > 1 && isEqual) { firstChar = deque.removeFront() lastChar = deque.removeBack() if (firstChar != lastChar) { isEqual = false } } return isEqual }
图解
6.5 生成 1 到 n 的二进制数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function generatePrintBinary (n ) { var q = new Queue() q.enqueue('1' ) while (n-- > 0 ) { var s1 = q.peek() q.dequeue() console .log(s1) var s2 = s1 q.enqueue(s1 + '0' ) q.enqueue(s2 + '1' ) } } generatePrintBinary(5 )
回顾
Tips:
Please indicate the source and original author when reprinting or quoting this article.