JavaScript數據結構 - 棧

你們好,我是前端圖圖。今天就來聊聊的數據結構,由於最近在學數據結構和算法,我也把寫文章看成一次覆盤。下面廢話很少說,開始吧!前端

棧的數據結構

棧是一種後進先出原則的有序集合(就是後面進去的先出來的意思)。添加或待刪除的元素都保存在棧的同一端,叫做棧頂(棧的末尾),另外一端叫做棧底。在棧裏面,新元素都在棧頂,舊的元素都在棧底。git

在生活中有許多用來描述棧的例子。例如:疊放的書籍、盤子。算法

在計算機中,棧被用在編譯器和內存中保存變量、方法調用等等,也被用在瀏覽器的歷史記錄(瀏覽器的返回按鈕)。數組

基於數組的棧

下面用ES6的類來建立一個棧。瀏覽器

class Stack {
  constructor() {
    this.items = [];
  }
}
複製代碼

對於棧來講,可使用數組,也可使用對象。只要聽從LIFO(後進先出)原則就行。markdown

下面是棧的一些方法。數據結構

  • push(ele):添加一個或多個元素到棧頂。
  • pop():移除棧頂的元素並返回移除的元素。
  • peek():獲取棧頂的元素。
  • isEmpty():校驗是否爲空棧,若是爲空就返回true,不然返回false
  • clear():清空棧。
  • size():返回棧中的元素數量。

向棧添加元素

push方法負責向棧內添加元素,這個方法只添加元素到棧頂(也就是棧的末尾)。數據結構和算法

push(ele) {
  this.items.push(ele);
}
複製代碼

這裏是數組來保存棧的元素,因此直接使用數組的push方法。ui

從棧裏移除元素

pop方法負責移除棧裏的元素。棧聽從LIFO原則,因此移除最後添加的元素。this

pop() {
  return this.items.pop();
}
複製代碼

pushpop這兩個方法操做棧裏的元素,這樣天然聽從LIFO原則了。

查看棧頂元素

若是想知道棧裏最後添加的元素是什麼,用peek方法便可。

peek() {
  return this.items[this.items.length - 1];
}
複製代碼

length - 1便可訪問數組最後的一個元素。

上面的圖中,數組中包含了三個元素1, 2, 3,數組的長度是3。而length - 1(3-1)就是2

檢查棧是否爲空

isEmpty方法校驗棧是否爲空棧,爲空就返回true,不然返回false

isEmpty() {
  return this.items.length === 0;
}
複製代碼

這裏簡單的判斷一下數組的長度是否爲0就能夠了。

獲取棧裏的元素個數

size方法返回數組的長度就能夠獲取元素的個數了。

size() {
  return this.items.length;
}
複製代碼

清空棧

clear方法移除棧中的全部元素,最簡單的方式就是把items初始化成一個空數組。

clear() {
  this.items = [];
}
複製代碼

這樣就完成了棧的方法。

使用 Stack 類

首先初始化一個Stack類,而後查看棧是否爲空。

const stack = new Stack();
console.log(stack.isEmpty()); // true 爲true就表明棧是空的
複製代碼

而後,向棧裏添加元素。

stack.push(1);
stack.push(2);

console.log(stack.peek()); // 2
複製代碼

這裏添加了12,固然你能夠添加任何類型的元素。而後調用了peek方法,輸出的是2,由於它是棧裏最後一個元素。

再往棧裏添加一個元素。

stack.push(10);
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false
複製代碼

咱們往棧裏添加了10。調用size方法,輸出的是3,棧裏有三個元素。調用isEmpty方法,輸出的是false

下面展現了到如今爲止對棧的操做,以及棧的當前狀態。

在調用pop方法以前,棧裏有三個元素,調用兩次後,如今棧只剩下1了。

stack.pop();
stack.pop();
console.log(stack.size()); // 1
複製代碼

總體代碼

class Stack {
  constructor() {
    this.items = [];
  }

  push(ele) {
    this.items.push(ele);
  }
  pop() {
    return this.items.pop();
  }
  peek() {
    return this.items[this.items.length - 1];
  }
  isEmpty() {
    return this.items.length === 0;
  }
  size() {
    return this.items.length;
  }
  clear() {
    this.items = [];
  }
}

const stack = new Stack();
console.log(stack.isEmpty()); // true

stack.push(1); // 向棧裏添加了元素1
stack.push(2); // 向棧裏添加了元素2

console.log(stack.peek()); // 此時棧裏最後一個元素爲2

stack.push(10); // 又往棧裏添加一個元素10
console.log(stack.size()); // 這時棧的長度就變成了3
console.log(stack.isEmpty()); // false

stack.pop(); // 從棧頂中移除了一項
stack.pop(); // 從棧頂中又移除了一項
console.log(stack.size()); // 從棧中移除了兩個元素,最後獲取棧的長度就是1
複製代碼

基於對象的棧

同樣的,使用類來建立基於對象的棧。

class Stack{
  constructor {
    this.count = 0;
    this.items = {};
  }
}
複製代碼

對象版本的Stack類中,用count屬性來記錄棧的大小(也能夠從棧中添加和刪除元素)。

往棧中插入元素

在數組的版本中,能夠向Stack類中添加多個元素,而在對象這個版本的push方法只容許一次插入一個元素。

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

在JavaScript中,對象是一系列鍵值對的集合。要向棧中添加元素,使用count變量做爲items對象的鍵名,插入的元素就是它的值。向棧插入元素以後,就遞增count變量。

const stack = new Stack();
stack.push(5);
stack.push(10);
console.log(stack);
// {count: 2, items: {0: 5, 1: 10}}
複製代碼

能夠看到Stack類內部items裏的值和count屬性的值在最後的log中輸出。

驗證棧是否爲空和它的大小

count屬性也表示棧的大小。這樣就能夠用count屬性的值來實現size方法。

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

判斷棧是否爲空的話,驗證count的值是否爲0就能夠了。

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

從棧中移除元素

因爲沒有使用數組來存儲元素,就要手動實現移除元素的邏輯。pop方法同樣返回從棧移除的元素。

pop() {
  // 首先判斷棧是否爲空,若是爲空,就返回undefined
  if (this.isEmpty()) {
    return undefined;
  }
  // 若是棧不爲空的話,就將`count`屬性減1
  this.count--;
  // result保存了棧頂的元素
  const result = this.items[this.count];
  // 刪除棧頂的元素
  delete this.items[this.count];
  // 以後返回剛纔保存的棧頂元素
  return result;
}
複製代碼

獲取棧頂的元素

訪問棧頂的元素,只須要把count屬性減1便可。

peek() {
  if (this.isEmpty()) {
  	return undefined;
  }
  this.items[this.count - 1];
}
複製代碼

清空棧

清空棧只須要把它的值設置成初始化時的值就好了。

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

固然還能夠用下面這種方法移除棧裏的全部元素。

anotherClear() {
  while(!this.isEmpty()) {
  	this.pop();
  }
}
複製代碼

建立toString方法

在數組的版本中,並不須要建立toString方法,由於可使用數組的toString方法。但對象的版本,就要建立一個toString方法來像數組那樣輸出棧的內容。

toString() {
  // 棧爲空,將返回一個空字符串。
  if (this.isEmpty()) {
    return "";
  }

  // 棧不爲空,就須要用它底部的第一個元素做爲字符串的初始值
  let objString = `${this.items[0]}`;
  // 棧只包含一個元素,就不會執行`for`循環。
  for (let i = 1; i < this.count; i++) {
    // 迭代整個棧的鍵,一直到棧頂,添加一個逗號(,)以及下一個元素。
    objString = `${objString},${this.items[i]}`;
  }
  return objString;
}
複製代碼

這樣就完成了兩個不一樣版本的Stack類。這也是一個不一樣方法寫代碼的例子。對於使用Stack類,選擇使用數組仍是對象並不重要,兩種方法都提供同樣的方法,只是內部實現不同而已。

總體代碼

class Stack {
  constructor() {
    this.count = 0;
    this.items = {};
  }

  push(ele) {
    this.items[this.count] = ele;
    this.count++;
  }

  size() {
    return this.count;
  }

  isEmpty() {
    return this.count === 0;
  }

  pop() {
    // 首先判斷棧是否爲空,若是爲空,就返回undefined
    if (this.isEmpty()) {
      return undefined;
    }
    // 若是棧不爲空的話,就將`count`屬性減1
    this.count--;
    // result保存了棧頂的元素
    const result = this.items[this.count];
    // 這裏是刪除棧頂的元素,因爲使用的是對象,因此可使用delete運算符從對象中刪除一個特定的值
    delete this.items[this.count];
    // 以後返回棧頂的元素
    return result;
  }

  peek() {
    if (this.isEmpty()) {
      return undefined;
    }
    this.items[this.count - 1];
  }

  clear() {
    this.items = {};
    this.count = 0;
  }

  anotherClear() {
    while (!this.isEmpty()) {
      this.pop();
    }
  }

  toString() {
    // 棧爲空,將返回一個空字符串。
    if (this.isEmpty()) {
      return "";
    }

    // 棧不爲空,就須要用它底部的第一個元素做爲字符串的初始值
    let objString = `${this.items[0]}`;
    // 棧只包含一個元素,就不會執行`for`循環。
    for (let i = 1; i < this.count; i++) {
      // 迭代整個棧的鍵,一直到棧頂,添加一個逗號(,)以及下一個元素。
      objString = `${objString},${this.items[i]}`;
    }
    return objString;
  }
}
複製代碼

用棧解決問題

棧的實際應用很是普遍。它能夠存儲訪問過的任何或路徑、撤銷的操做。

從十進制到二進制

在生活中,咱們主要使用十進制。但在計算機中,二進制很是重要,由於計算機的全部內容都是用二進制數字表示的(01)。

要把十進制轉成二進制,能夠將該十進制數除以2(二進制是滿二進一)並對商取整,直到結果是0爲止。舉個例子,把十進制的數10轉成二進制的數字,下面是對應的算法。

function decimal(num) {
  const remStack = []; // 存儲二進制的棧
  let number = num; // 須要轉成二進制的數
  let rem = ""; // 餘數
  let binaryString = ""; // 存儲推出棧的元素

  // 當參數不爲0時,進入while語句
  while (number > 0) {
    rem = Math.floor(number % 2);
    remStack.push(rem); // 把餘數添加到remStack數組中
    number = Math.floor(number / 2); // number除以2,獲得下次要取餘數的值,此時的number的值已經不是傳入的參數了。
  }

  while (remStack.length !== 0) {
    // 用pop方法把棧中的元素移除,將移除棧的元素連成字符串
    binaryString += remStack.pop().toString();
  }
  return binaryString;
}

console.log(decimal(10)); // 1010
console.log(decimal(100)); // 1100100
複製代碼

在上面這段代碼裏,當參數不是0時,進入while語句。就獲得一個餘數賦值給rem,並放入棧裏。而後讓number除以2,就獲得了下次進入while語句取餘數的值。要注意的是,此時的number的值已經不是傳入參數的值了。最後,用pop方法把棧中的元素移除,將移除的元素連成字符串。

進制轉換算法

修改以前的算法,能夠將十進制轉成計數爲2~36的任何進制。除了把十進制除以2轉成二進制數外,還能夠傳入其餘任何禁止的基數爲參數。

function baseConverter(num, base) {
  const remStack = [];
  const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  let number = num;
  let rem = "";
  let baseString = "";

  if (!(base >= 2 && base <= 36)) {
    return "";
  }

  while (number > 0) {
    rem = Math.floor(number % base);
    remStack.push(rem);
    number = Math.floor(number / base);
  }

  while (remStack.length !== 0) {
    baseString += digits[remStack.pop()];
  }
  return baseString;
}

console.log(baseConverter(10000, 2)); // 10011100010000
console.log(baseConverter(10000, 8)); // 23420
console.log(baseConverter(10000, 16)); // 64
console.log(baseConverter(10000, 36)); // 7PS
複製代碼

上面的代碼只須要改一個地方,把十進制轉成二進制的時候,餘數是01,再把十進制轉八進制的時候,餘數是0~7;可是把十進制轉十六進制時,餘數是0~9再加上A、B、C、D、E、F(對應 十、十一、十二、1三、1四、15)。因此須要對棧中的數組作一個轉換才行(baseString += digits[remStack.pop()]這段代碼)。從十一進制開始,字母表中的每一個字母都對應一個基數,A就表明基數11B就表明基數12,以此類推。

總結

我的感受操做棧數據結構相對於其餘的數據結構來講,仍是比較簡單的。無論用對象仍是數組均可以實現棧數據結構,只要聽從LIFO(後進先出)原則便可。喜歡的掘友能夠點擊關注+點贊哦!後面會持續更新其餘數據結構,也把本身學的知識分享給你們。固然寫做也能夠當成覆盤。2021年加油!實現本身的目標。

相關文章
相關標籤/搜索