JavaScript數據結構-隊列

你們好,我是前端圖圖,已經有段時間沒有寫文章了😅。回家過年以後就沒有什麼心思了,只想多陪陪家人。致使假期回來才慢慢找回感受😅。好啦!下面廢話很少說,就來聊聊數據結構隊列前端

隊列和雙端隊列

隊列和棧類似,可是使用和棧不一樣的原則。雙端隊列是隊列和棧的原則混合在一塊兒的數據結構。算法

隊列

隊列是遵循先進先出(FIFO,也就是先進來的先出去的意思)原則的一組有序的項。隊列是從尾部添加新元素,並從頭部移除元素,最新添加元素必須排在隊列的末尾。後端

在生活中有不少例子,好比超市的收銀臺,你們都會排隊,而排在第一位的人先接收服務。數組

在計算機中,一個常見的例子是打印文件,好比說要打印五份文件。在點擊打印的時候,每一個文件都會被髮送到打印隊列。第一個發送到打印隊列的文檔會先被打印,以此類推,知道打印完全部文件。markdown

建立隊列

下面就來建立一個表示隊列的類。前端工程師

class Queue {
  constructor() {
    this.count = 0; // 隊列元素總數
    this.lowestCount = 0; // 跟蹤隊列第一個元素的值
    this.items = {};
  }
}
複製代碼

首先用一個存儲隊列的數據結構,能夠是數組,也能夠是對象。items就是用來存儲元素的。看起來是否是和棧很是類似?只是添加和刪除的原則不同而已。數據結構

count屬性是用來控制隊列大小的。而lowestCount屬性是用來在刪除隊列前面的元素時,追蹤第一個元素。運維

下面是要聲明一些隊列的方法。函數

  • enqueue:向隊列的尾部添加一個元素。
  • dequeue:移除隊列的第一個元素而且返回該元素。
  • peek:返回隊列中最早添加的元素,也是最早被移除的元素。
  • isEmpty:校驗該隊列是否爲空隊列。
  • size:返回隊列中的元素個數,和數組的length相似。

enqueue方法

首先實現的是enqueue方法,該方法用於向隊列尾部添加元素。你們要記住!新添加的元素只能在隊列的末尾添加,這個方法和棧的push方法同樣。測試

enqueue(ele) {
  this.items[ele] = ele;
  this.count++;
}
複製代碼

dequeue方法

接下來就是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方法

peek方法用於查看隊列頭部的元素。把lowestCount做爲鍵名來獲取元素值。

peek() {
  if (this.isEmpty()) {
    return undefined;
  }

  return this.items[this.lowestCount];
}
複製代碼

isEmpty方法

isEmpty方法和棧的isEmpty方法同樣,只不過這裏用的是countlowestCount之間的差值計算而已。

isEmpty() {
  return this.count - this.lowestCount === 0;
}
複製代碼

size方法

size方法也是用countlowestCount之間的差值計算。而後返回計算後的差值便可。

size() {
  return this.count - this.lowestCount;
}
複製代碼

clear方法

清空隊列中的全部元素,直接把隊列裏面的全部屬性值都重置爲構造函數裏同樣就好了。

clear() {
  this.count = 0;
  this.lowestCount = 0;
  this.items = {};
}
複製代碼

toString方法

還有一個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類

和以前同樣,先聲明一個Deque類。

class Deque {
  constructor() {
    this.count = 0;
    this.lowestCount = 0;
    this.items = {};
  }
}
複製代碼

能夠看到Deque類的部分代碼和普通隊列的代碼同樣。還有isEmptysizecleartoString方法都是同樣的。

雙端隊列能夠在兩端添加和移除元素,下面列出這幾種方法。

  • addFront:從雙端隊列的頭部添加元素。
  • addBack:從雙端隊列的尾部添加元素(和隊列的enqueue方法同樣)。
  • removeFront:從雙端隊列的頭部移除元素(和隊列的dequeue方法同樣)。
  • removeBack:從雙端隊列的尾部移除元素(和棧的peek方法同樣)。
  • peekFront:獲取雙端隊列頭部的第一個元素(和隊列的peek方法同樣)。
  • peekBack:獲取雙端隊列尾部的第一個元素(和棧的peek方法同樣)。

addFront方法

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;
  }
}
複製代碼

要將一個元素添加到雙端隊列的頭部,有三種狀況。

  1. 當雙端隊列爲空時,就把元素從尾部添加到雙端隊列中,這樣就添加到雙端隊列的頭部了。
  2. 一個元素已經從雙端隊列的頭部移除,也就是說lowestCount屬性的值大於等於1時,就把lowestCount的值減1並將新元素的值放到該鍵的位置上。
  3. lowestCount的值爲0時,咱們能夠設置一個負值的鍵,就拿數組來講。要在第一個位置添加一個元素,就要把全部的元素都日後挪一位來空出第一個位置。就從最後一位開始迭代,並把元素賦上索引值減1的位置的值(也就是前一個元素)。在全部元素都完成了移動以後,第一位的索引值將是0,再把添加的元素覆蓋掉它就能夠了。

測試Deque類

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());
// 前端工程師, 後端工程師
複製代碼

Deque類總體代碼

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
複製代碼

總結

這篇文章介紹了隊列和雙端隊列所遵循的原則,還有它們的實現方法。還介紹了兩個經典的隊列問題:擊鼓傳花和迴文檢查。 喜歡的掘友能夠點擊關注+點贊哦!後面會持續更新其餘數據結構,也把本身學的知識分享給你們。固然寫做也能夠當成覆盤。

相關文章
相關標籤/搜索