資料結構演算法 二進位制轉十進位制_資料結構 - 棧
技術標籤:資料結構演算法 二進位制轉十進位制
兩種類似陣列的資料結構,在新增和刪除元素時更為可控,他們就是棧和佇列
棧是一種遵從後進先出(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]
}
檢查棧是否為空,實現 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)
下圖描繪了我們對棧的操作,以及棧的當前狀態
呼叫兩次 pop 方法從棧裡移除兩個元素
stack.pop()
stack.pop()
console.log(stack.size()) //2
在兩次呼叫 pop 方法前,我們的棧裡有四個元素。呼叫兩次後,現在棧裡僅剩下 5 和 8 了,下圖描繪了這個執行過程
建立一個基於物件的 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
驗證一個棧是否為空, 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 轉化成二進位制的數字,過程大概是如下這樣。
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,以此類推。