前端學數據結構之棧


前面的話

  學習數據結構和算法十分重要。首要原因是數據結構和算法可以很高效地解決常見問題,這對今后的代碼質量至關重要(也包括性能,要是用了不恰當的數據結構或算法,很可能會產生性能問題)。其次,對於計算機科學,算法是最基礎的概念。數組是計算機科學中最常用的數據結構,我們知道,可以在數組的任意位置上刪除或添加元素。然而,有時候還需要一種在添加或刪除元素時有更多控制的數據結構。有兩種數據結構類似於數組,但在添加和刪除元素時更為可控。它們就是棧和隊列。本文將詳細介紹棧

 

數據結構

  棧是一種遵從后進先出(LIFO)原則的有序集合。新添加的或待刪除的元素都保存在棧的末尾,稱作棧頂,另一端就叫棧底。在棧里,新元素都靠近棧頂,舊元素都接近棧底。

  在現實生活中也能發現很多棧的例子。例如,下圖里的一摞書或者餐廳里堆放的盤子

stack

  棧也被用在編程語言的編譯器和內存中保存變量、方法調用等

 

創建棧

  下面將創建一個類來表示棧,先聲明這個類:

function Stack() {
//各種屬性和方法的聲明
}

  使用一種數據結構來保存棧里的元素。可以選擇數組:

let items = [];

  接下來,為棧聲明一些方法

push(element(s)):添加一個(或幾個)新元素到棧頂
pop():移除棧頂的元素,同時返回被移除的元素
peek():返回棧頂的元素,不對棧做任何修改(這個方法不會移除棧頂的元素,僅僅返回它)
isEmpty():如果棧里沒有任何元素就返回true,否則返回false
clear():移除棧里的所有元素
size():返回棧里的元素個數。這個方法和數組的length屬性很類似

【push】

  push方法負責往棧里添加新元素,有一點很重要:該方法只添加元素到棧頂,也就是棧的末尾

  因為使用了數組來保存棧里的元素,所以可以數組的push方法來實現

this.push = function(element){ 
  items.push(element);
};

【pop】

  接着來實現pop方法。這個方法主要用來移除棧里的元素。棧遵從LIFO原則,因此移出的是最后添加進去的元素。因此,可以用數組的pop方法

this.pop = function(){ 
  return items.pop();
};

  只能用push和pop方法添加和刪除棧中元素,這樣一來,棧自然就遵從了LIFO原則

【peek】

  現在,為類實現一些額外的輔助方法。如果想知道棧里最后添加的元素是什么,可以用peek方法。這個方法將返回棧頂的元素:

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

  因為類內部是用數組保存元素的,所以訪問數組的最后一個元素可以用 length - 1

stack2

  在上圖中,有一個包含三個元素的棧,因此內部數組的長度就是3。數組中最后一項的位置是2,length - 1(3 -1)正好是2

【isEmpty】

  下面要實現的方法是 isEmpty,如果棧為空的話將返回true,否則就返回false:

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

  使用isEmpty方法,能簡單地判斷內部數組的長度是否為0

【size】

  類似於數組的length屬性,也能實現棧的length。對於集合,最好用size代替length。因為棧的內部使用數組保存元素,所以能簡單地返回棧的長度:

this.size = function(){ 
  return items.length;
};

【clear】

  最后來實現clear方法。clear方法用來移除棧里所有的元素,把棧清空。實現這個方法最簡單的方式是:

this.clear = function(){ 
  items = [];
};

  另外也可以多次調用pop方法,把數組中的元素全部移除,這樣也能實現clear方法

  棧已經實現。通過一個例子來應用它,為了檢查棧里的內容,我們來實現一個輔助方法,叫print。它會把棧里的元素都輸出到控制台:

this.print = function(){ 
  console.log(items.toString());
};

  這樣,我們就完整創建了棧!

  棧的完整代碼如下

function Stack() {

    let items = [];

    this.push = function(element){
        items.push(element);
    };

    this.pop = function(){
        return items.pop();
    };

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

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

    this.size = function(){
        return items.length;
    };

    this.clear = function(){
        items = [];
    };

    this.print = function(){
        console.log(items.toString());
    };

    this.toString = function(){
        return items.toString();
    };
}

 

使用stack類

  下面來學習如何使用Stack類。 首先,需要初始化Stack類。然后,驗證一下棧是否為空(輸出是true,因為還沒有往棧里添加元素)

var stack = new Stack(); 
console.log(stack.isEmpty()); //輸出為true

  接下來,往棧里添加一些元素(可以添加任意類型的元素)

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

  如果調用peek方法,將會輸出8,因為它是往棧里添加的最后一個元素:

console.log(stack.peek());//輸出8

  再添加一個元素:

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

  我們往棧里添加了11。如果調用size方法,輸出為3,因為棧里有三個元素(5、8和11)。 如果調用isEmpty方法,會看到輸出了false(因為棧里有三個元素,不是空棧)。最后, 我們再添加一個元素:

stack.push(15);

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

stack3

  然后,調用兩次pop方法從棧里移除2個元素:

stack.pop();
stack.pop(); 
console.log(stack.size()); //輸出2 
stack.print(); //輸出[5, 8]

  在兩次調用pop方法前,我們的棧里有四個元素。調用兩次后,現在棧里僅剩下5和8了。下圖描繪這個過程的執行:

stack4

 

ES6

  下面來花點時間分析一下代碼,看看是否能用ES6的新功能來改進

  我們創建了一個可以當作類來使用的Stack函數。JS函數都有構造函數,可以用來模擬類的行為。我們聲明了一個私有的items變量,它只能被Stack函數/類訪問。然而,這個方法為每個類的實例都創建一個items變量的副本。因此,如果要創建多個Stack實例,它就不太適合了

  下面用ES6新語法來聲明Stack類

class Stack {

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

    push(element){
        this.items.push(element);
    }
    //其他方法
}

  我們只是用ES6的簡化語法把Stack函數轉換成Stack類。這種方法不能像其他語言(Java、C++、C#)一樣直接在類里面聲明變量,只能在類的構造函數constructor里聲明,在類的其他函數里用this.items就可以引用這個變量

  盡管代碼看起來更簡潔、更漂亮,變量items卻是公共的。ES6的類是基於原型的,雖然基於原型的類比基於函數的類更節省內存,也更適合創建多個實例,卻不能聲明私有屬性(變量)或方法。而且,在這種情況下,我們希望Stack類的用戶只能訪問暴露給類的方法。否則,就有可能從棧的中間移除元素(因為我們用數組來存儲其值),這不是我們希望看到的

  ES6語法有沒有其他方法來創建私有屬性呢?

【Symbol】

  ES6新增了一種叫作Symbol的基本類型,它是不可變的,可以用作對象的屬性。看看怎么用它來在Stack類中聲明items屬性

let _items = Symbol(); //{1}
class Stack {
 constructor () {
   this[_items] = []; //{2}
 }
 //Stack方法
}

  在上面的代碼中,我們聲明了Symbol類型的變量_items(行{1}),在類的constructor函數中初始化它的值(行{2})。要訪問_items,只需把所有的this.items都換成this[_items]

  這種方法創建了一個假的私有屬性,因為ES6新增的Object.getOwnPropertySymbols方法能夠取到類里面聲明的所有Symbols屬性。下面是一個破壞Stack類的例子:

let stack = new Stack();
stack.push(5);
stack.push(8);
let objectSymbols = Object.getOwnPropertySymbols(stack);
console.log(objectSymbols.length); // 1
console.log(objectSymbols); // [Symbol()]
console.log(objectSymbols[0]); // Symbol()
stack[objectSymbols[0]].push(1);
stack.print(); //輸出 5, 8, 1

  從以上代碼可以看到,訪問stack[objectSymbols[0]]是可以得到_items的。並且,_items屬性是一個數組,可以進行任意的數組操作,比如從中間刪除或添加元素。我們操作的是棧,不應該出現這種行為

【WeakMap】

  有一種數據類型可以確保屬性是私有的,這就是WeakMap。WeakMap可以存儲鍵值對,其中鍵是對象,值可以是任意數據類型。

  如果用WeakMap來存儲items變量,Stack類就是這樣的:

const items = new WeakMap(); //{1}
class Stack {
 constructor () {
   items.set(this, []); //{2}
 }
 push(element) {
  let s = items.get(this); //{3}
  s.push(element);
 }
 pop() {
  let s = items.get(this);
  let r = s.pop();
  return r;
 }
 //其他方法
}

  行{1},聲明一個WeakMap類型的變量items。行{2},在constructor中,以this(Stack類自己的引用)為鍵,把代表棧的數組存入items。行{3},從WeakMap中取出值,即以this為鍵(行{2}設置的)從items中取值

  現在知道,items在Stack類里是真正的私有屬性了,但還有一件事要做。items現在仍然是在Stack類以外聲明的,因此誰都可以改動它。要用一個閉包(外層函數)把Stack類包起來,這樣就只能在這個函數里訪問WeakMap:

let Stack = (function () {
 const items = new WeakMap();
 class Stack {
  constructor () {
    items.set(this, []);
  }
  //其他方法
 }
  return Stack; //{5}
})();

  當Stack函數里的構造函數被調用時,會返回Stack類的一個實例(行{5})

  現在,Stack類有一個名為items的私有屬性。雖然它很丑陋,但畢竟實現了私有屬性。然而,用這種方法的話,擴展類無法繼承私有屬性。魚與熊掌不可兼得

  棧的完整代碼如下

let Stack3 = (function () {

    const items = new WeakMap();

    class Stack3 {

        constructor () {
            items.set(this, []);
        }

        push(element){
            let s = items.get(this);
            s.push(element);
        }

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

        peek(){
            let s = items.get(this);
            return s[s.length-1];
        }

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

        size(){
            let s = items.get(this);
            return s.length;
        }

        clear(){
            items.set(this, []);
        }

        print(){
            console.log(this.toString());
        }

        toString(){
            return items.get(this).toString();
        }
    }

    return Stack3;
})();

  把上面的代碼跟最初實現的Stack類做個比較,我們會發現有一些相似之處:

function Stack() {
 let items = [];
 //其他方法
}

  事實上,盡管ES6引入了類的語法,仍然不能像在其他編程語言中一樣聲明私有屬性或方法。有很多種方法都可以達到相同的效果,但無論是語法還是性能,這些方法都有各自的優點和缺點

  哪種方法更好?這取決於在實際項目中如何使用算法,要處理的數據量,要創建的實例個數,以及其他約束條件

 

應用

  棧的實際應用非常廣泛。在回溯問題中,它可以存儲訪問過的任務或路徑、撤銷的操作。Java和C#用棧來存儲變量和方法調用,特別是處理遞歸算法時,有可能拋出一個棧溢出異常

  下面將學習使用棧的三個最著名的算法示例。首先是十進制轉二進制問題,以及任意進制轉換的算法;然后是平衡圓括號問題;最后,學習如何用棧解決漢諾塔問題

【十進制轉二進制】

  現實生活中,我們主要使用十進制。但在計算科學中,二進制非常重要,因為計算機里的所有內容都是用二進制數字表示的(0和1)。沒有十進制和二進制相互轉化的能力,與計算機交流就很困難

  要把十進制轉化成二進制,我們可以將該十進制數字和2整除(二進制是滿二進一),直到結果是0為止。舉個例子,把十進制的數字10轉化成二進制的數字,過程大概是這樣

stack5

  下面是對應的算法描述:

function divideBy2(decNumber){
 var remStack = new Stack(),
 rem,
 binaryString = '';
 while (decNumber > 0){ //{1}
  rem = Math.floor(decNumber % 2); //{2}
  remStack.push(rem); //{3}
  decNumber = Math.floor(decNumber / 2); //{4}
 }
 while (!remStack.isEmpty()){ //{5}
  binaryString += remStack.pop().toString();
 }
 return binaryString;
} 

  在這段代碼里,當結果滿足和2做整除的條件時(行{1}),我們會獲得當前結果和2的余數,放到棧里(行{2}、{3})。然后讓結果和2做整除(行{4})。另外請注意:JavaScript有數字類型,但是它不會區分究竟是整數還是浮點數。因此,要使用Math.floor函數讓除法的操作僅返回整數部分。最后,用pop方法把棧中的元素都移除,把出棧的元素變成連接成字符串(行{5})。

  用剛才寫的算法做一些測試,使用以下代碼把結果輸出到控制台里:

console.log(divideBy2(233)); //輸出11101001 
console.log(divideBy2(10)); //輸出1010 
console.log(divideBy2(1000)); //輸出1111101000

【進制轉換算法】

  我們很容易修改之前的算法,使之能把十進制轉換成任何進制。除了讓十進制數字和2整除 轉成二進制數,還可以傳入其他任意進制的基數為參數,就像下面算法這樣:

function baseConverter(decNumber, base){
 var remStack = new Stack(),
     rem,
     baseString = '',
     digits = '0123456789ABCDEF'; //{6}
 while (decNumber > 0){
  rem = Math.floor(decNumber % base);
  remStack.push(rem);
  decNumber = Math.floor(decNumber / base);
 }
 while (!remStack.isEmpty()){
  baseString += digits[remStack.pop()]; //{7}
 }
 return baseString;
} 

  我們只需要改變一個地方。在將十進制轉成二進制時,余數是0或1;在將十進制轉成八進制時,余數是0到7之間的數;但是將十進制轉成16進制時,余數是0到9之間的數字加上A、B、C、D、E和F(對應10、11、12、13、14和15)。因此,我們需要對棧中的數字做個轉化才可以(行{6}和行{7})

  可以使用之前的算法,輸出結果如下:

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

【平衡圓括號】

function parenthesesChecker(symbols){

    let stack = new Stack(),
        balanced = true,
        index = 0,
        symbol, top,
        opens = "([{",
        closers = ")]}";

    while (index < symbols.length && balanced){
        symbol = symbols.charAt(index);
        if (opens.indexOf(symbol) >= 0){
            stack.push(symbol);
            console.log(`open symbol - stacking ${symbol}`);
        } else {
            console.log(`close symbol ${symbol}`);
            if (stack.isEmpty()){
                balanced = false;
                console.log('Stack is empty, no more symbols to pop and compare');
            } else {
                top = stack.pop();
                //if (!matches(top, symbol)){
                if (!(opens.indexOf(top) === closers.indexOf(symbol))) {
                    balanced = false;
                    console.log(`poping symbol ${top} - is not a match compared to ${symbol}`);
                } else {
                    console.log(`poping symbol ${top} - is is a match compared to ${symbol}`);
                }
            }
        }
        index++;
    }
    if (balanced && stack.isEmpty()){
        return true;
    }
    return false;
}

console.log(parenthesesChecker('{([])}')); //true
console.log(parenthesesChecker('{{([][])}()}')); //true
console.log(parenthesesChecker('[{()]')); //false

【漢諾塔】

function towerOfHanoi(n, from, to, helper){

    if (n > 0){
        towerOfHanoi(n-1, from, helper, to);
        to.push(from.pop());
        console.log('-----');
        console.log('Source: ' + from.toString());
        console.log('Dest: ' + to.toString());
        console.log('Helper: ' + helper.toString());
        towerOfHanoi(n-1, helper, to, from);
    }
}

var source = new Stack();
source.push(3);
source.push(2);
source.push(1);

var dest = new Stack();
var helper = new Stack();

towerOfHanoi(source.size(), source, dest, helper);

source.print();
helper.print();
dest.print();

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM