你們好,我是前端圖圖,已經有段時間沒有寫文章了😅。回家過年以後就沒有什麼心思了,只想多陪陪家人。致使假期回來才慢慢找回感受😅。好啦!下面廢話很少說,就來聊聊數據結構隊列。前端
隊列和棧類似,可是使用和棧不一樣的原則。雙端隊列是隊列和棧的原則混合在一塊兒的數據結構。算法
隊列是遵循先進先出(FIFO,也就是先進來的先出去的意思)原則的一組有序的項。隊列是從尾部添加新元素,並從頭部移除元素,最新添加元素必須排在隊列的末尾。後端
在生活中有不少例子,好比超市的收銀臺,你們都會排隊,而排在第一位的人先接收服務。數組
在計算機中,一個常見的例子是打印文件,好比說要打印五份文件。在點擊打印的時候,每一個文件都會被髮送到打印隊列。第一個發送到打印隊列的文檔會先被打印,以此類推,知道打印完全部文件。markdown
下面就來建立一個表示隊列的類。前端工程師
class Queue {
constructor() {
this.count = 0; // 隊列元素總數
this.lowestCount = 0; // 跟蹤隊列第一個元素的值
this.items = {};
}
}
複製代碼
首先用一個存儲隊列的數據結構,能夠是數組,也能夠是對象。items
就是用來存儲元素的。看起來是否是和棧很是類似?只是添加和刪除的原則不同而已。數據結構
count
屬性是用來控制隊列大小的。而lowestCount
屬性是用來在刪除隊列前面的元素時,追蹤第一個元素。運維
下面是要聲明一些隊列的方法。函數
enqueue
:向隊列的尾部添加一個元素。dequeue
:移除隊列的第一個元素而且返回該元素。peek
:返回隊列中最早添加的元素,也是最早被移除的元素。isEmpty
:校驗該隊列是否爲空隊列。size
:返回隊列中的元素個數,和數組的length
相似。首先實現的是enqueue
方法,該方法用於向隊列尾部添加元素。你們要記住!新添加的元素只能在隊列的末尾添加,這個方法和棧的push
方法同樣。測試
enqueue(ele) {
this.items[ele] = ele;
this.count++;
}
複製代碼
接下來就是dequeue
方法,用於移除隊列中的元素。隊列遵循先進先出的原則,最早添加的元素最早被移除。
dequeue() {
if (this.isEmpty()) {
return undefined;
}
// 暫存頭部元素
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
// 刪除以後將lowestCount遞增
this.lowestCount++;
return result;
}
複製代碼
有了這兩個方法,Queue
類就遵循先進先出的原則了。
peek
方法用於查看隊列頭部的元素。把lowestCount
做爲鍵名來獲取元素值。
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.lowestCount];
}
複製代碼
isEmpty
方法和棧的isEmpty
方法同樣,只不過這裏用的是count
和lowestCount
之間的差值計算而已。
isEmpty() {
return this.count - this.lowestCount === 0;
}
複製代碼
size
方法也是用count
和lowestCount
之間的差值計算。而後返回計算後的差值便可。
size() {
return this.count - this.lowestCount;
}
複製代碼
清空隊列中的全部元素,直接把隊列裏面的全部屬性值都重置爲構造函數裏同樣就好了。
clear() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
複製代碼
還有一個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
類中的第一個索引不必定是0
,因此從lowestCount
的位置開始迭代。
一個隊列就這樣大功告成啦!
Queue
類和Stack
類很是像,主要的區別就在於dequeue
方法和peek
方法,這是因爲兩個數據結構的原則不同所致使。
class Queue {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
enqueue(ele) {
this.items[this.count] = ele;
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.count - this.lowestCount === 0;
}
size() {
return this.count - this.lowestCount;
}
clear() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
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;
}
}
const queue = new Queue();
console.log(queue.isEmpty()); // true
queue.enqueue("前端工程師");
queue.enqueue("後端工程師");
queue.enqueue("算法工程師");
console.log(queue.toString());
// 前端工程師, 後端工程師, 算法工程師
console.log(queue.size()); // 3
queue.dequeue();
console.log(queue.toString());
// 後端工程師, 算法工程師
複製代碼
雙端隊列是一種同時能夠從頭部和尾部添加或刪除的特殊隊列。它是普通隊列和棧的結合版。
舉個例子,例如:你在食堂排隊打飯,你剛打完飯,發現阿姨給的飯有點少。你就回到隊伍的頭部叫阿姨給多點飯。另外,若是你排在隊伍的尾部。看到排在前面還有不少人,你就能夠直接離開隊伍。
在計算機中,雙端隊列常見的應用是存儲一系列的撤銷操做。每當在軟件中進行一個操做時,該操做會被存在雙端隊列裏。當點擊撤銷時,該操做會從雙端隊列末尾彈出。當操做的次數超出了給定的次數後,最早進行的操做會從雙端隊列的頭部移除。
和以前同樣,先聲明一個Deque
類。
class Deque {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
}
複製代碼
能夠看到Deque
類的部分代碼和普通隊列的代碼同樣。還有isEmpty
、size
、clear
和toString
方法都是同樣的。
雙端隊列能夠在兩端添加和移除元素,下面列出這幾種方法。
addFront
:從雙端隊列的頭部添加元素。addBack
:從雙端隊列的尾部添加元素(和隊列的enqueue
方法同樣)。removeFront
:從雙端隊列的頭部移除元素(和隊列的dequeue
方法同樣)。removeBack
:從雙端隊列的尾部移除元素(和棧的peek
方法同樣)。peekFront
:獲取雙端隊列頭部的第一個元素(和隊列的peek
方法同樣)。peekBack
:獲取雙端隊列尾部的第一個元素(和棧的peek
方法同樣)。addFront(ele) {
if (this.isEmpty()) {
this.addBack(ele);
} else if (this.lowestCount > 0) {
this.lowestCount--;
this.items[this.lowestCount] = ele;
} else {
for (let i = this.count; i > 0; i--) {
this.items[i] = this.items[i - 1];
}
this.count--;
this.lowestCount = 0;
this.items[0] = ele;
}
}
複製代碼
要將一個元素添加到雙端隊列的頭部,有三種狀況。
lowestCount
屬性的值大於等於1
時,就把lowestCount
的值減1
並將新元素的值放到該鍵的位置上。lowestCount
的值爲0
時,咱們能夠設置一個負值的鍵,就拿數組來講。要在第一個位置添加一個元素,就要把全部的元素都日後挪一位來空出第一個位置。就從最後一位開始迭代,並把元素賦上索引值減1
的位置的值(也就是前一個元素)。在全部元素都完成了移動以後,第一位的索引值將是0
,再把添加的元素覆蓋掉它就能夠了。const deque = new Deque();
deque.addBack("前端工程師");
deque.addBack("後端工程師");
console.log(deque.toString());
// 前端工程師, 後端工程師
deque.addBack("算法工程師");
console.log(deque.toString());
// 前端工程師, 後端工程師, 算法工程師
console.log(deque.size()); // 3
deque.removeFront(); // 前端工程師跑路了
console.log(deque.toString());
// 後端工程師, 算法工程師
deque.removeBack(); // 算法工程師也跑路了
console.log(deque.toString());
// 後端工程師
deque.addFront("前端工程師"); // 前端工程師又回來了
console.log(deque.toString());
// 前端工程師, 後端工程師
複製代碼
class Deque {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
addFront(ele) {
if (this.isEmpty()) {
this.addBack(ele);
} else if (this.lowestCount > 0) {
this.lowestCount--;
this.items[this.lowestCount] = ele;
} else {
for (let i = this.count; i > 0; i--) {
this.items[i] = this.items[i - 1];
}
this.count--;
this.lowestCount = 0;
this.items[0] = ele;
}
}
addBack(ele) {
this.items[this.count] = ele;
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.count - this.lowestCount === 0;
}
size() {
return this.count - this.lowestCount;
}
clear() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
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;
}
}
複製代碼
循環隊列的一個例子是擊鼓傳花遊戲。在這個遊戲裏,小孩子圍成一個圈,把花盡快地傳遞給旁邊的小孩子。在某個時刻傳花中止了,花在誰手上,誰就被淘汰。重複這個過程,直到只剩下一個孩子。
下面來模擬擊鼓傳花遊戲。
function hotPotato(names, num) {
const queue = new Queue();
const eliminatedList = []; // 淘汰名單
for (let i = 0; i < names.length; i++) {
// 先把名單加入隊列
queue.enqueue(names[i]);
}
while (queue.size() > 1) {
for (let i = 0; i < num; i++) {
// 從隊列頭部移除一項,並把該項添加到隊列尾部
queue.enqueue(queue.dequeue());
}
// for循環一旦中止了,就將隊列最前一項移除並添加到淘汰名單中
eliminatedList.push(queue.dequeue());
}
return {
eliminated: eliminatedList,
winner: queue.dequeue(),
}
}
複製代碼
hotPotato
函數接收兩個參數:names
是一份名單,num
是循環次數。首先把名單裏的名字添加到隊列中,而後用num
迭代隊列。從隊列頭部移除一項並將該項添加到隊列尾部。一旦到達num
的次數(for
循環中止了),將從隊列移除一個元素並添加到淘汰名單裏,直到隊列裏只剩下一我的時,這我的就是獲勝者。
咱們來試驗一下hotPotato
算法。
const names = [
"前端工程師",
"後端工程師",
"算法工程師",
"測試工程師",
"運維工程師",
];
const result = hotPotato(names, 1);
result.eliminated.forEach((item) => {
console.log(`${item}被淘汰了`);
});
// 後端工程師被淘汰了
// 測試工程師被淘汰了
// 前端工程師被淘汰了
// 運維工程師被淘汰了
console.log(`${result.winner}獲勝了!`);
// 算法工程師獲勝了!
複製代碼
下圖展現整個過程。
能夠傳入不一樣的數值,模擬不一樣的場景。
將一個句子正着讀和倒着讀的意思同樣,就能夠稱爲迴文。
檢查一個詞或字符串是否是迴文,最簡單的方式是把字符串反轉過來並檢查它和原字符串是否相同。若是相同,那就是迴文。能夠用棧來實現,可是利用數據結構來解決這個問題最簡單的方法就是雙端隊列。
function palindromeCheck(str) {
// 判斷傳入的字符串是否合法
if (str === undefined || str === null || (str != null && str.length === 0)) {
return false;
}
const deque = new Deque();
// 把字符串轉成小寫並剔除空格
const lowerString = str.toLocaleLowerCase().split(" ").join("");
// 迴文標識
let isEqual = true;
// 存儲雙端隊列頭部字符串
let firstChar = "";
// 存儲雙端隊列尾部字符串
let lastChar = "";
// 將字符串逐個添加到雙端隊列中
for (let i = 0; i < lowerString.length; i++) {
deque.addBack(lowerString.charAt(i));
}
while (deque.size() > 1 && isEqual) {
// 移除雙端隊列頭部的字符串並將返回結果賦值給firstChar變量
firstChar = deque.removeFront();
// 移除雙端隊列尾部的字符串並將返回結果賦值給lastChar變量
lastChar = deque.removeBack();
// 若是雙端隊列兩端移除的元素互不相同,證實不是迴文
if (firstChar !== lastChar) {
isEqual = false;
}
return isEqual;
}
}
console.log(palindromeCheck("stts")); // true
console.log(palindromeCheck("level")); // true
console.log(palindromeCheck("小姐姐姐姐小")); // true
console.log(palindromeCheck("上海自來水來自海上")); // true
console.log(palindromeCheck("知道不不知道")); // false
複製代碼
這篇文章介紹了隊列和雙端隊列所遵循的原則,還有它們的實現方法。還介紹了兩個經典的隊列問題:擊鼓傳花和迴文檢查。 喜歡的掘友能夠點擊關注+點贊哦!後面會持續更新其餘數據結構,也把本身學的知識分享給你們。固然寫做也能夠當成覆盤。