1. 程式人生 > 其它 >資料結構演算法 二進位制轉十進位制_資料結構 - 棧

資料結構演算法 二進位制轉十進位制_資料結構 - 棧

技術標籤:資料結構演算法 二進位制轉十進位制

兩種類似陣列的資料結構,在新增和刪除元素時更為可控,他們就是佇列

棧是一種遵從後進先出(LIFO)原則的有序集合。新新增或待刪除的元素都儲存在棧的同一端,稱作棧頂,另一端就叫棧底。在棧裡,新元素都靠近棧頂,舊元素都接近棧底。

被用在程式語言的編譯器和記憶體中儲存變數、方法呼叫等,也被用於瀏覽器歷史記錄(瀏覽器的返回按鈕)。

建立一個基於陣列的棧

建立一個類來表示棧,利用陣列來儲存棧裡的元素

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

陣列允許我們在任何位置新增或刪除元素,由於棧遵循 LIFO 原則,所以需要對元素的新增和刪除做一些限制,接下來為棧宣告一些方法

  • push() : 新增新元素到棧頂
  • pop() : 移除棧頂的 元素,同時返回被移除的元素
  • peek() : 返回棧頂的元素,不對棧做任何修改
  • isEmpty() : 如果棧裡沒有任何元素返回 true, 否則返回 false
  • clear() : 移除棧裡所有的元素
  • size() : 返回棧裡的元素個數

向棧新增元素,首先實現 push() ,向棧裡新增新元素,該方法只新增元素到棧頂,可以這樣寫

push(element) {
  this.items.push(element)
}

從棧移除元素,實現 pop() 方法,移除棧裡的元素,棧遵循 LIFO 原則,移除的是最後新增進去的元素

pop() {
  return this.items.pop()
}

限制為 push 和 pop 方法新增和刪除棧中元素,這樣棧就自然遵循了 LIFO 原則

檢視棧頂元素,想知道棧裡最後新增的元素是什麼,可以用 peek 方法,該方法將返回棧頂的元素

peek() {
  return this.items[this.items.length - 1] 
}

fad5e7d1521ab06374fcd6ba1d66319c.png

檢查棧是否為空,實現 isEmpty,如果棧為空的話將返回 true,否則就返回 false

isEmpty() {
  return this.items.length === 0
}

實現棧的長度

size() {
  return this.items.length
}

清空棧元素,實現 clear 方法,移除棧裡所有的元素

clear() {
  this.items = []
}

以上實現了一個棧

使用 Stack 類

在深入瞭解棧的應用前,先來了解如何使用 Stack 類。首先需要初始化 Stack 類,然後驗證一下棧是否為空(輸出是 true,因為還沒有往棧裡新增元素)

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

接下來,往棧裡新增一些元素

stack.push(5)
stack.push(8)

呼叫 peek 方法(),返回棧頂的元素

console.log(stack.peek()) //8

再新增一個元素

stack.push(11)
console.log(stack.size()) //3
console.log(stack.isEmpty()) //false

繼續新增元素

stack.push(15)

下圖描繪了我們對棧的操作,以及棧的當前狀態

a22438a5c0fc9cd77f26fc42704b0771.png

呼叫兩次 pop 方法從棧裡移除兩個元素

stack.pop()
stack.pop()
console.log(stack.size()) //2

在兩次呼叫 pop 方法前,我們的棧裡有四個元素。呼叫兩次後,現在棧裡僅剩下 5 和 8 了,下圖描繪了這個執行過程

b29b03bb0e5a28ec04b72e31d0129b01.png

建立一個基於物件的 Stack 類

使用陣列來儲存元素,在處理大量資料時,需評估如何操作資料是最高效的,使用陣列時,大部分方法的時間複雜度是 O(n) ,我們需要迭代整個陣列直到找到要找的那個元素,在最壞的情況下需要迭代陣列的所有位置,其中的 n 代表陣列的長度。如果陣列有更多元素的話,所需的時間會更長。另外,陣列是元素的一個有序集合,為了保證元素排列有序,它會佔用更多的記憶體空間。

如果我們能直接獲取元素,佔用較少的記憶體空間,並且仍然保證所有元素按照我們的需要排列,那不是更好嗎?對於使用 JavaScript 語言實現棧資料結構的場景,我們也可以使用一個JavaScript 物件來儲存所有的棧元素,保證它們的順序並且遵循 LIFO 原則。我們來看看如何實現這樣的行為。

首先宣告一個 stack 類

class Stack {
  constructor() {
    this.count = 0 //記錄棧的大小
    this.items = {}
  }
}

向棧中插入元素,因為使用的是物件, 所以 push 方法只允許我們一次插入一個元素

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

物件是鍵值對的集合,所以要向棧中新增元素,可以使用 count 變數作為 items 物件的鍵名,插入的元素則是它的值。在向棧插入元素後,我們遞增 count 變數。

使用 Stack 類,插入元素 5,8

const stack = new Stack()
stack.push(5)
stack.push(8)

檢視 stack

6d3b3a3e2ec25ab212f43f3d9c495f50.png

驗證一個棧是否為空, count 屬性也表示棧的大小,因此,我們可以簡單地返回 count 屬性的值來實現 size 方法

size() {
  return this.count
}

驗證棧是否為空

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

從棧中彈出元素,物件中沒有直接用的 api ,所以手動實現

pop() {
  if (this.isEmpty()) {
    return undefined
  }
  this.count--
  const result = this.items[this.count]
  delete this.items[this.count]
  return result 
}

首先,我們需要檢驗棧是否為空。如果為空,就返回 undefined。如果棧不為空的話,我們會將 count 屬性減 1,並儲存棧頂的值,以便在刪除它之後將它返回。

stack.pop() //8

模擬 pop 操作, 要訪問到棧頂的元素(即最後新增的元素 8),我們需要訪問鍵值為 1 的位置。因此我們將 count 變數從 2 減為 1。這樣就可以訪問 items[1],刪除它,並將它的值返回了。

檢視棧頂的元素

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

清空該棧,只需要將它的值復原為建構函式中使用的值即可

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

建立 toString 方法

在陣列版本中,我們不需要關心 toString 方法的實現,因為資料結構可以直接使用陣列已經提供的 toString 方法。對於使用物件的版本,我們將建立一個 toString 方法來像陣列一樣打印出棧的內容。

toString() {
  if (this.isEmpty()) {
    return ''
  }
  let objString = `${this.items[0]}`
  for (let i = 1; i < this.count; i++) {
    objString = `${objString}, ${this.items[i]}`
  }
  return objString
}

如果棧是空的,我們只需返回一個空字串即可。如果它不是空的,就需要用它底部的第一個元素作為字串的初始值,然後迭代整個棧的鍵,一直到棧頂,新增一個逗號以及下一個元素如果棧只包含一個元素,for迴圈不會執行

除了 toString 方法,我們建立的其他方法的複雜度均為 O(1),代表我們可以直接找到目標元素並對其進行操作(push、 pop 或 peek)。

保護資料結構內部元素

在建立別的開發者也可以使用的資料結構或物件時,我們希望保護內部的元素,只有我們暴露出的方法才能修改內部結構,對於 Stack 類來說,要確保元素只會被新增到棧頂,而不是棧底或其他任意位置(比如棧的中間)。

使用 WeakMap 實現類

WeakMap 可以儲存鍵值對,其中鍵是物件,值可以是任意資料型別,如果用 WeakMap 來儲存 items 屬性(陣列版本), Stack 類就是這樣的:

const items = new WeakMap() //宣告一個 WeakMap 型別的變數 items

class Stack {
  constructor() {
    items.set(this, []) //以 this(Stack 類自己的引用)為鍵,把代表棧的陣列存入 items。
  }
  
  push(element) {
    //從 WeakMap 中取出值,即以 this 為鍵(行{2}設定的)從 items 中取值。
    const s = items.get(this)
    s.push(element)
  }

  pop() {
    const s = items.get(this)
    const r = s.pop()
    return r
  }
}

items 在 Stack 類裡是真正的私有屬性

ECMAScript 類屬性提案(易讀性更好)

class Stack {
  #count = 0
  #items = 0
  //方法
}

用棧解決問題

如何解決十進位制轉二進位制問題,以及任意進位制轉換的演算法。

從十進位制到二進位制

該十進位制數除以 2(二進位制是滿二進一)並對商取整,直到結果是 0 為止。舉個例子,把十進位制的數 10 轉化成二進位制的數字,過程大概是如下這樣。

dbf5e80ae3efd408b28ccf9a25c700f5.png
function decimalToBinary(decNumber) {
  const remStack = new Stack()
  let number = decNumber
  let rem 
  let binaryString = ''

  while (number > 0) {
    rem = Math.floor(number % 2) //js 不區分整數和浮點數,使用 Math.floor 返回整數部分,得到餘數
    remStack.push(rem) //放入棧裡
    number = Math.floor(number / 2) //繼續除以2,直到結果等於0時,才會停止 
  }

  while (!remStack.isEmpty()) {
    binaryString += remStack.pop().toString() //用 pop 方法把棧中的元素都移除,把出棧的元素連線成字串
  }
  return binaryString
}

測試

console.log(decimalToBinary(233)) //11101001
console.log(decimalToBinary(10)) //1010
console.log(decimalToBinary(1000)) //1111101000

進位制轉換演算法

修改上面的演算法,使之能把十進位制轉換成基數為 2~36 的任意進位制。除了把十進位制數除以 2 轉成二進位制數,還可以傳入其他任意進位制的基數為引數,就像下面的演算法這樣。

function baseConverter(decNumber, base) {
  const remStack = new Stack()
  const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  let number = decNumber
  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.isEmpty()) {
    baseString += digits[remStack.pop()]
  }
  
  return baseString
}

console.log(baseConverter(100345, 2)); // 11000011111111001
console.log(baseConverter(100345, 8)); // 303771
console.log(baseConverter(100345, 16)); // 187F9
console.log(baseConverter(100345, 35)); // 2BW0

我們只需要改變一個地方。在將十進位制轉成二進位制時,餘數是 0 或 1;在將十進位制轉成八進位制時,餘數是 0~7;但是將十進位制轉成十六進位制時,餘數是 0~9 加上 A、 B、 C、 D、 E 和 F(對應 10、 11、 12、 13、 14 和 15)。因此,我們需要對棧中的數字做個轉化才可以(行{6}和行{7})。因此,從十一進位制開始,字母表中的每個字母將表示相應的基數。字母 A 代表基數 11, B 代表基數 12,以此類推。