上一篇系列文章《【資料結構基礎】棧簡介(使用ES6)》筆者介紹了什麼是資料結構和什麼是棧及相關程式碼實現,本篇文章筆者給大家介紹下什麼是佇列以及相關的程式碼實現。
本篇文章將從以下幾個方面進行介紹:
- 什麼是佇列
- 如何用程式碼實現佇列
- 什麼是雙端佇列
- 如何用程式碼實現雙端佇列
- 實際應用舉例
本篇文章閱讀時間預計10分鐘。
什麼是佇列
佇列是一個有序集合,遵循先進先出的原則(FIFO),與堆疊的原則恰恰相反。允許插入的一端稱為隊尾,允許刪除的一端稱為對頭。假設佇列是q=(a1,a2,......,an),那麼a1就是隊頭,an就是隊尾。我們刪除時,從a1開始刪除,而插入時,只能在an後插入。
佇列就好比我們生活中的排隊,比如我們去醫院掛號需要排隊,進電影院需要排隊進場,去超市買東西需要排隊結賬,打電話諮詢客服需要排隊接聽等等。
在計算機中最常見的例子就是印表機的列印佇列任務,假設我們要列印五分不同的文件,我們需要依次開啟每個文件,依次的單擊“列印按鈕”,每個列印指令都會送往列印佇列任務,最先按列印按鈕的文件最先被列印,直到所有文件被列印完成。
如何用程式碼實現佇列
首先我們先宣告建立一個初始化的queue類,實現程式碼如下:
class Queue {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
} 複製程式碼
首先我們建立了一個儲存佇列元素的資料結構,我們宣告瞭count變數,方便我們統計佇列大小,宣告lowestCount變數標記佇列的對頭,方便我們刪除元素。接下來我們要宣告如下方法,來實現一個完整的佇列:
- enqueue(element):此方法用於在隊尾新增元素。
- dequeue(): 此方法用於刪除佇列的隊頭元素。
- peek():此方法用於佇列的隊頭元素。
- isEmpty(): 此方法用於判斷佇列是否為空,是的話返回True,否的話返回False。
- size(): 此方法返回佇列的大小,類似陣列length屬性。
- clear():清空佇列所有元素。
- toString():列印佇列中的元素。
enqueue(element)
此方法主要實現了向佇列的隊尾新增新的元素,實現的關鍵就是“隊尾”新增元素,實現程式碼如下:
enqueue(element) {
this.items[this.count] = element;
this.count++;
} 複製程式碼
由於佇列的items屬性是物件,我們使用count作為物件的屬性,元素新增至佇列後,count的值遞增加1。
dequeue()
此方法主要用於刪除佇列元素,由於佇列遵循先進先出原則,我們需要將佇列的“隊頭”元素進行移除,程式碼實現如下:
dequeue() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}複製程式碼
首先我們需要驗證佇列是否為空,如果未空返回未定義。如果佇列不為空,我們首先獲取“隊頭”元素,然後使用delete方法進行刪除,同時標記對頭元素的變數lowestCount遞增加一,然後返回刪除的隊頭元素。
peek()
現在我們來實現一些輔助方法,比如我們想檢視“隊頭”元素,我們用peek()方法進行實現,使用lowestCount變數進行獲取,實現程式碼如下:
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.lowestCount];
} 複製程式碼
size()與isEmpty()
獲取佇列的長度,我們可以使用count變數與lowestCount相減即可,假如我們的count屬性為2,lowestCount為0,這意味著佇列有兩個元素。接下來我們從佇列裡中刪除一個元素,lowestCount的值更新為1,count的值不變,因此佇列的長度為1,依次類推。因此size()方法的實現程式碼如下:
size() {
return this.count - this.lowestCount;
} 複製程式碼
isEmpty()的實現方式更為簡單了,只需要判斷size()是否返回為0即可,實現程式碼如下:
isEmpty() {
return this.size() === 0;
}複製程式碼
clear()
要清空佇列元素,我們可以一直呼叫dequeue()方法,直至返回undefined即可或者將各變數重置為初始值即可,我們使用重置初始化的思路,程式碼如下:
clear() {
this.items = {};
this.count = 0;
this.lowestCount = 0;
}複製程式碼
toString()
接下來我們實現最後一個方法,列印輸出佇列所有的元素,示例程式碼如下:
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;
} 複製程式碼
最終完整的queue類
export default class Queue {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
enqueue(element) {
this.items[this.count] = element;
this.count++;
}
dequeue() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.lowestCount];
}
isEmpty() {
return this.size() === 0;
}
clear() {
this.items = {};
this.count = 0;
this.lowestCount = 0;
}
size() {
return this.count - this.lowestCount;
}
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;
}
}複製程式碼
如何使用Queue類
首先引入我們的Queue類,然後初始化建立我們的Queue類,驗證是否為空,然後進行新增刪除元素,示例程式碼如下:
const queue = new Queue();
console.log(queue.isEmpty()); // outputs true
queue.enqueue('John');
queue.enqueue('Jack');
console.log(queue.toString()); // John,Jack
queue.enqueue('Camila');
console.log(queue.toString()); // John,Jack,Camila
console.log(queue.size()); // outputs 3
console.log(queue.isEmpty()); // outputs false
queue.dequeue(); // remove John
queue.dequeue(); // remove Jack
console.log(queue.toString()); // Camila複製程式碼
如下圖所示演示了上述程式碼的執行效果:
什麼是雙端佇列
雙端佇列是一個特殊的更靈活的佇列,我們可以在佇列的“隊頭”或“隊尾”新增和刪除元素。由於雙端佇列是實現了FIFO和LIFO這兩個原則,也可以說是佇列和堆疊結構的合體結構。
在我們生活中,比如排隊買票,有的人著急或特殊情況,直接來到隊伍的最前面,有的人因為其他的事情,等不了太長時間,從隊尾離開了。
如何用程式碼實現雙端佇列
首先我們宣告初始化一個雙端佇列,程式碼和佇列的結構類似,如下段程式碼所示:
class Deque {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
}複製程式碼
由於雙端佇列的結構和佇列的結構類似,只是插入和刪除更靈活而已,isEmpty(), clear(), size()和toString()相關方法保持一致,還需要增加以下相關的方法:
- addFront(element):此方法用於在雙端佇列的“隊頭”新增元素。
- addBack(element):此方法用於在雙端佇列的“隊尾”新增元素。
- removeFront():此方法用於刪除雙端佇列的“隊頭”元素。
- removeBack():此方法用於刪除雙端佇列的“隊尾”元素。
- peekFront():此方法用於返回雙端佇列的“隊頭”元素
- peekBack():此方法用於返回雙端佇列的“隊尾”元素
addFront(element)
由於從雙端佇列的的“隊頭”新增元素,稍微複雜些,實現程式碼如下:
addFront(element) {
if (this.isEmpty()) {
this.addBack(element);
} else if (this.lowestCount > 0) {
this.lowestCount--;
this.items[this.lowestCount] = element;
} else {
for (let i = this.count; i > 0; i--) {
this.items[i] = this.items[i - 1];
}
this.count++;
this.lowestCount = 0;
this.items[0] = element;
}
}複製程式碼
從上述程式碼我們可以看出,如果雙端佇列為空,我們複用了addBack()方法,避免書寫重複的程式碼;如果隊頭元素lowestCount的變數大於0,我們將變數遞減,將新新增的元素賦值給隊頭元素;如果lowestCount的變數為0,為了避免負值的出現,我們將佇列元素整體往後移動1位,進行重新賦值,將隊頭索引為0的位置留給新新增的元素。
最終實現的Deque類
由於文章篇幅有限,其他的方法又很類似,不再一一介紹,完整的程式碼如下:
export default class Deque {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
addFront(element) {
if (this.isEmpty()) {
this.addBack(element);
} else if (this.lowestCount > 0) {
this.lowestCount--;
this.items[this.lowestCount] = element;
} else {
for (let i = this.count; i > 0; i--) {
this.items[i] = this.items[i - 1];
}
this.count++;
this.items[0] = element;
}
}
addBack(element) {
this.items[this.count] = element;
this.count++;
}
removeFront() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
removeBack() {
if (this.isEmpty()) {
return undefined;
}
this.count--;
const result = this.items[this.count];
delete this.items[this.count];
return result;
}
peekFront() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.lowestCount];
}
peekBack() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.count - 1];
}
isEmpty() {
return this.size() === 0;
}
clear() {
this.items = {};
this.count = 0;
this.lowestCount = 0;
}
size() {
return this.count - this.lowestCount;
}
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;
}
}複製程式碼
如何使用Deque類
接下來我們來驗證下我們的Deque類,首先引入Deque類的檔案,程式碼如下:
const deque = new Deque();
console.log(deque.isEmpty()); // outputs true
deque.addBack('John');
deque.addBack('Jack');
console.log(deque.toString()); // John,Jack
deque.addBack('Camila');
console.log(deque.toString()); // John,Jack,Camila
console.log(deque.size()); // outputs 3
console.log(deque.isEmpty()); // outputs false
deque.removeFront(); // remove John
console.log(deque.toString()); // Jack,Camila
deque.removeBack(); // Camila decides to leave
console.log(deque.toString()); // Jack
deque.addFront('John'); // John comes back for information
console.log(deque.toString()); // John,Jack”複製程式碼
實際應用舉例1:擊鼓傳花
不知道大家玩過擊鼓傳花嗎,筆者最怕玩這個,不知道是點背還在咋地,這個花球總和我有緣,本身就五音不全還要表演,人可丟大了。什麼是擊鼓傳花,在這裡給沒玩過的朋友解釋下:數人或幾十人圍成圓圈坐下,其中一人拿花(或一小物件);另有一人揹著大家或矇眼擊鼓(桌子、黑板或其他能發出聲音的物體),鼓響時眾人開始依次傳花,至鼓停止為止。此時花在誰手中(或其座位前),誰就上臺表演節目(多是唱歌、跳舞、說笑話;或回答問題、猜謎、按紙條規定行事等);偶然如果花在兩人手中,則兩人可通過猜拳或其它方式決定負者。
今天我們要用佇列實現這個遊戲,稍微不同的是,拿到花球的人需要出列,直到最後一個拿到花球的人獲勝。假設告訴敲鼓的人一個數字(從0開始),按照數字迴圈在場的人,到達這個數字停止敲鼓,直到最後一個人為止。
大家是不是迫不及待的想知道程式碼如何實現?程式碼如下所示:
function hotFlower(elementsList, num) {
const queue = new Queue();
const elimitatedList = [];
for (let i = 0; i < elementsList.length; i++) {
queue.enqueue(elementsList[i]);
}
while (queue.size() > 1) {
for (let i = 0; i < num; i++) {
queue.enqueue(queue.dequeue());
}
elimitatedList.push(queue.dequeue());
}
return {
eliminated: elimitatedList,
winner: queue.dequeue()
};
}複製程式碼
從上述程式碼我們可以看出:
- 我們宣告瞭一個佇列queue和陣列elimitatedList(出局者資訊)。
- 通過迭代的方式填充給定的物件elementsList並賦值給queue。
- 然後在給定的變數num之下,不斷的刪除佇列的頭元素,並插入到隊尾,相當保持佇列數目不變,迴圈依次移動佇列;(迴圈佇列)
- 到達給定數字num,刪除當前佇列“隊頭”元素,並將隊頭“出局者”資訊,新增至陣列elimitatedList。
- 直到佇列的元素為1時,函式輸出elimitatedList(出局者資訊)和獲勝者資訊winner。
接下來我們來驗證下,此演算法是否正確,驗證程式碼如下:
const names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl'];
const result = hotFlower(names, 7);
result.eliminated.forEach(name => {
console.log(`${name} was eliminated from the Hot Flower game.`);
});
console.log(`The winner is: ${result.winner}`);複製程式碼
上述程式碼將會輸出:
Camila was eliminated from the Hot Flower game.
Jack was eliminated from the Hot Flower game.
Carl was eliminated from the Hot Flower game.
Ingrid was eliminated from the Hot Flower game.
The winner is: John複製程式碼
程式碼執行時,佇列的變化示意圖如下:
實際應用舉例2:驗證英語迴文
許多英語單詞無論是順讀還是倒讀,其詞形和詞義完全一樣,如dad(爸爸)、noon(中午)、level(水平)等。最簡單的方法就是反轉字串與原始字串進行比較是否相等。從資料結構的角度我們可以運用堆疊的結構進行實現,然而用雙端佇列的結構實現起來也非常簡單,示例程式碼如下:
function palindromeChecker(aString) {
if (aString === undefined || aString === null ||
(aString !== null && aString.length === 0)) {
return false;
}
const deque = new Deque();
const lowerString = aString.toLocaleLowerCase().split(' ').join('');
let isEqual = true;
let firstChar, lastChar;
for (let i = 0; i < lowerString.length; i++) {
deque.addBack(lowerString.charAt(i));
}
while (deque.size() > 1 && isEqual) {
firstChar = deque.removeFront();
lastChar = deque.removeBack();
if (firstChar !== lastChar) {
isEqual = false;
}
}
return isEqual;
}複製程式碼
從上述程式碼我們可以看出:
- 首先我們需要驗證輸入的字串是否有效。
- 宣告例項化一個雙端佇列。
- 針對輸入的字串進行轉成小寫、刪除空格的處理。
- 然後將字串拆成字元新增至雙端佇列
- 然後通過removeFront()和deque.removeBack()這兩個出列方法進行比較,只要不相等就返回fasle跳出方法。
- 返回布林變數isEqual,True為迴文,fasle
接下來我們來驗證下我們的演算法是否正確:
console.log('a', palindromeChecker('a'));
console.log('aa', palindromeChecker('aa'));
console.log('kayak', palindromeChecker('kayak'));
console.log('level', palindromeChecker('level'));
console.log('Was it a car or a cat I saw', palindromeChecker('Was it a car or a cat I saw'));
console.log('Step on no pets', palindromeChecker('Step on no pets'));複製程式碼
上述程式碼的執行結果都返回為true。
小節
今天關於佇列的介紹就到這裡,我們一起學習了什麼是佇列和雙端佇列,以及如何進行程式碼實現。並且運用迴圈佇列的機制實現了擊鼓傳花的遊戲,同時又運用雙端佇列的結構實現了迴文的驗證。其實佇列在我們的實際業務場景中運用還是蠻多的,比如我們要實現一個佇列的訊息推送機制,我們JS的event loop的時間迴圈機制,瀏覽器的頁面渲染機制等等。希望本篇的內容對大家有所幫助,在實踐中運用多了才能運用佇列的機制解決更多的實際問題。