1、什麼是佇列?
佇列和棧有著明顯的區別,佇列是一種特殊的線性表有著先進先出的特點。它只允許在表頭進行刪除操作,在表尾進行新增操作。
入佇列示意圖
出佇列示意圖
佇列有許多的應用,比如javascript
的事件迴圈機制,就是通過事件佇列
來儲存非同步操作的回撥函式。
比如逐層列印一顆樹上的節點。像kafka,rabbitmq這類訊息佇列,其形式就是一種佇列,訊息生產者把訊息放入佇列中(尾部),消費者從佇列裡取出訊息進行處理(頭部),只不過背後的實現更為複雜。
如果你瞭解一點socket,那麼你應該知道當大量客戶端向服務端發起連線,而服務端忙不過來的時候,就會把這些請求放入到佇列中,先來的先處理,後來的後處理,佇列滿時,新來的請求直接拋棄掉。
資料結構在系統設計中的應用非常廣泛,只是我們水平達不到那個級別,知道的太少,但如果能理解並掌握這些資料結構,那麼就有機會在工作中使用它們並解決一些具體的問題,當我們手裡除了錘子還有電鋸時,那麼我們的眼裡就不只是釘子,解決問題的思路也會更加開闊
2、佇列的實現
首先先定義一些常用的方法:
- enqueue 從佇列尾部新增一個元素
- dequeue 從佇列頭部刪除一個元素
- head 返回頭部的元素,注意,不是刪除
- size 返回佇列大小
- clear 清空佇列
- isEmpty 判斷佇列是否為空
- tail 返回佇列尾節點
然後我們逐一實現
let Queue = (function () {
let items = new WeakMap()
// WeakMap結構與Map結構基本類似。區別是它只接受物件作為鍵名,
// 不接受其他型別的值作為鍵名。鍵名是物件的弱引用,當物件被回收後,
// WeakMap自動移除對應的鍵值對,WeakMap結構有助於防止記憶體洩漏。
class Queue {
constructor() {
items.set(this, [])
}
// 入佇列
enqueue(item) {
let queue = items.get(this)
queue.push(item)
}
// 出佇列
dequeue() {
return items.get(this).shift()
}
// 返回佇列頭
head() {
let queue = items.get(this)
return queue[0]
}
// 返回佇列大小
size() {
let queue = items.get(this)
return queue.length
}
// 清空佇列
clear() {
items.set(this, [])
}
// 判斷佇列是否為空
isEmpty() {
let queue = items.get(this)
return queue.length === 0
}
// 返回隊尾
tail() {
let queue = items.get(this)
return queue[queue.length - 1]
}
}
return Queue
})
複製程式碼
3、佇列常見的應用
3.1、約瑟夫環
有一個陣列a[100]存放0--99;要求每隔兩個數刪掉一個數,到末尾時迴圈至開頭繼續進行,求最後一個被刪掉的數。比如:前10個數是 0 1 2 3 4 5 6 7 8 9 10,所謂每隔兩個數刪掉一個數,其實就是把 2 5 8 刪除掉。
思路分析
- 從佇列頭部刪除一個元素,index+1
- 如果index%3 == 0,就說明這個元素是需要刪除的元素,如果不等於0,就不是需要被刪除的元素,則把它新增到佇列的尾部
程式碼實現
// 先初始化一個資料
var arr_list = [];
for(var i=0;i< 100;i++){
arr_list.push(i);
}
// 定義功能函式
function del(arr) {
let queue = new Queue()
// 將所有元素入佇列
for(var i=0;i< arr_list.length;i++){
queue.enqueue(arr_list[i]);
}
let index = 0
while(queue.size() != 1) {
index ++
index%3 === 0 ? queue.enqueue(queue.dequeue()) : queue.dequeue()
}
return queue.head() // 返回佇列中唯一的元素
}
// 呼叫
del(arr_list)
複製程式碼
3.2、用佇列來實現一個棧
思路分析
-
push, 實現push方法時,如果兩個佇列都為空,那麼預設向queue_1裡新增資料,如果有一個不為空,則向這個不為空的佇列裡新增資料
-
top,兩個佇列,或者都為空,或者有一個不為空,只需要返回不為空的佇列的尾部元素即可
-
pop,pop方法是比較複雜,pop方法要刪除的是棧頂,但這個棧頂元素其實是佇列的尾部元素。每一次做pop操作時,將不為空的佇列裡的元素一次刪除並放入到另一個佇列中直到遇到佇列中只剩下一個元素,刪除這個元素,其餘的元素都跑到之前為空的佇列中了。
程式碼實現
function queueToStack() {
let queue_1 = new Queue()
let queue_2 = new Queue()
let data_queue = null
let empty_queue = null
// 確認每個佇列的用途
let initQueue = () => {
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 = (item) => {
initQueue()
data_queue.enqueue(item)
}
this.top = () => {
initQueue()
return data_queue.tail()
}
this.pop = () => {
initQueue()
while (data_queue.size() > 1) {
empty_queue.enqueue(data_queue.dequeue())
}
return data_queue.dequeue()
}
}
複製程式碼
佇列還有其他很多在面試中可能會問道的面試題比如:列印楊輝三角以及迷宮問題,這些用佇列來實現可能會更加的方便。
4、優先佇列
function PriorityQueue() {
let items = []
function QueueElement(element, priority) {
this.element = element
this.priority = priority // 設定優先順序別
}
this.enqueue = function(element, priority) {
let queueElement = new QueueElement(element, priority)
let add = false
for(let i = 0; i < items.length; i++) { // 遍歷佇列找到優先順序比它小的元素
if(queueElement.priority > items[i].priority) {
items.splice(i, 0, queueElement) // 然後新增到該佇列
add = true
break
}
}
if(!add) { // 如果沒有找到直接新增到佇列末尾
items.push(queueElement)
}
}
}
複製程式碼
5、迴圈佇列
function hotPotato(nameList, num) {
let queue = new Queue()
for(let i = 0; i < nameList.length; i++) {
queue.enqueue(nameList[i])
}
let eliminated = ''
while(queue.size() > 1) {
for(let i = 0; i < num; i++) {
queue.enqueue(queue.dequeue)
}
eliminated = queue.dequeue()
console.log(eliminates + 'out')
}
return queue.dequeue()
}
複製程式碼
6、總結
在學習了佇列之後與棧對比進一步瞭解了各個資料結構的用法,以及使用的方便之處。當然也為面試打下了基礎。