JavaScript數據結構——棧的實現與應用

  在計算機編程中,棧是一種很常見的數據結構,它聽從後進先出(LIFO——Last In First Out)原則,新添加或待刪除的元素保存在棧的同一端,稱做棧頂,另外一端稱做棧底。在棧中,新元素老是靠近棧頂,而舊元素老是接近棧底。git

  讓咱們來看看在JavaScript中如何實現棧這種數據結構。算法

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()); }; }

  咱們用最簡單的方式定義了一個Stack類。在JavaScript中,咱們用function來表示一個類。而後咱們在這個類中定義了一些方法,用來模擬棧的操做,以及一些輔助方法。代碼很簡單,看起來一目瞭然,接下來咱們嘗試寫一些測試用例來看看這個類的一些用法。編程

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

stack.push(5);
stack.push(8);
console.log(stack.peek()); // 8

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

stack.push(15);
stack.pop();
stack.pop();
console.log(stack.size()); // 2
stack.print(); // 5,8

stack.clear();
stack.print(); // 

  返回結果也和預期的同樣!咱們成功地用JavaScript模擬了棧的實現。可是這裏有個小問題,因爲咱們用JavaScript的function來模擬類的行爲,而且在其中聲明瞭一個私有變量items,所以這個類的每一個實例都會建立一個items變量的副本,若是有多個Stack類的實例的話,這顯然不是最佳方案。咱們嘗試用ES6(ECMAScript 6)的語法重寫Stack類。小程序

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

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

    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 = [];
    }

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

  沒有太大的改變,咱們只是用ES6的簡化語法將上面的Stack函數轉換成了Stack類。類的成員變量只能放到constructor構造函數中來聲明。雖然代碼看起來更像類了,可是成員變量items仍然是公有的,咱們不但願在類的外部訪問items變量而對其中的元素進行操做,由於這樣會破壞棧這種數據結構的基本特性。咱們能夠借用ES6的Symbol來限定變量的做用域。數據結構

let _items = Symbol();

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

    push(element) {
        this[_items].push(element);
    }

    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] = [];
    }

    print() {
        console.log(this[_items].toString());
    }
}

  這樣,咱們就不能再經過Stack類的實例來訪問其內部成員變量_items了。可是仍然能夠有變通的方法來訪問_items:閉包

let stack = new Stack();
let objectSymbols = Object.getOwenPropertySymbols(stack);

  經過Object.getOwenPropertySymbols()方法,咱們能夠獲取到類的實例中的全部Symbols屬性,而後就能夠對其進行操做了,如此說來,這個方法仍然不能完美實現咱們想要的效果。咱們可使用ES6的WeakMap類來確保Stack類的屬性是私有的:ide

const items = new WeakMap();

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

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

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

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

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

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

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

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

  如今,items在Stack類裏是真正的私有屬性了,可是,它是在Stack類的外部聲明的,這就意味着誰均可以對它進行操做,雖然咱們能夠將Stack類和items變量的聲明放到閉包中,可是這樣卻又失去了類自己的一些特性(如擴展類沒法繼承私有屬性)。因此,儘管咱們能夠用ES6的新語法來簡化一個類的實現,可是畢竟不能像其它強類型語言同樣聲明類的私有屬性和方法。有許多方法均可以達到相同的效果,但不管是語法仍是性能,都會有各自的優缺點。函數

let Stack = (function () {
    const items = new WeakMap();
    class Stack {
        constructor () {
            items.set(this, []);
        }

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

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

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

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

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

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

        print() {
            console.log(items.get(this).toString());
        }
    }
    return Stack;
})();

 

  下面咱們來看看棧在實際編程中的應用。性能

進制轉換算法

  將十進制數字10轉換成二進制數字,過程大體以下:測試

  10 / 2 = 5,餘數爲0

  5 / 2 = 2,餘數爲1

  2 / 2 = 1,餘數爲0

  1 / 2 = 0, 餘數爲1

  咱們將上述每一步的餘數顛倒順序排列起來,就獲得轉換以後的結果:1010。

  按照這個邏輯,咱們實現下面的算法:

function divideBy2(decNumber) {
   let remStack = new Stack();
   let rem, binaryString = '';

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

   while(!remStack.isEmpty()) {
       binaryString += remStack.pop().toString();
   }

   return binaryString;
}

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

  Stack類能夠自行引用本文前面定義的任意一個版本。咱們將這個函數再進一步抽象一下,使之能夠實現任意進制之間的轉換。

function baseConverter(decNumber, base) {
    let remStack = new Stack();
    let rem, baseString = '';
    let digits = '0123456789ABCDEF';

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

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

    return baseString;
}

console.log(baseConverter(233, 2)); // 11101001
console.log(baseConverter(10, 2)); // 1010
console.log(baseConverter(1000, 2)); // 1111101000

console.log(baseConverter(233, 8)); // 351
console.log(baseConverter(10, 8)); // 12
console.log(baseConverter(1000, 8)); // 1750

console.log(baseConverter(233, 16)); // E9
console.log(baseConverter(10, 16)); // A
console.log(baseConverter(1000, 16)); // 3E8

  咱們定義了一個變量digits,用來存儲各進制轉換時每一步的餘數所表明的符號。如:二進制轉換時餘數爲0,對應的符號爲digits[0],即0;八進制轉換時餘數爲7,對應的符號爲digits[7],即7;十六進制轉換時餘數爲11,對應的符號爲digits[11],即B。

漢諾塔

  有關漢諾塔的傳說和由來,讀者能夠自行百度。這裏有兩個和漢諾塔類似的小故事,能夠跟你們分享一下。

  1. 有一個古老的傳說,印度的舍罕王(Shirham)打算重賞國際象棋的發明人和進貢者,宰相西薩·班·達依爾(Sissa Ben Dahir)。這位聰明的大臣的胃口看來並不大,他跪在國王面前說:「陛下,請您在這張棋盤的第一個小格內,賞給我一粒小麥;在第二個小格內給兩粒,第三格內給四粒,照這樣下去,每一小格內都比前一小格加一倍。陛下啊,把這樣擺滿棋盤上全部64格的麥粒,都賞給您的僕人吧!」。「愛卿。你所求的並很少啊。」國王說道,內心爲本身對這樣一件奇妙的發明所許下的慷慨賞諾不致破費太多而暗喜。「你固然會如願以償的。」說着,他使人把一袋麥子拿到寶座前。計數麥粒的工做開始了。第一格內放一粒,第二格內放兩粒,第三格內放四粒,......還沒到第二十格,袋子已經空了。一袋又一袋的麥子被扛到國王面前來。可是,麥粒數一格接以各地增加得那樣迅速,很快就能夠看出,即使拿來全印度的糧食,國王也兌現不了他對西薩·班·達依爾許下的諾言了,由於這須要有18 446 744 073 709 551 615顆麥粒呀!

  這個故事實際上是一個數學級數問題,這位聰明的宰相所要求的麥粒數能夠寫成數學式子:1 + 2 + 22 + 23 + 24 + ...... 262 + 263 

  推算出來就是:

  

  其計算結果就是18 446 744 073 709 551 615,這是一個至關大的數!若是按照這位宰相的要求,須要全世界在2000年內所生產的所有小麥才能知足。

  2. 另一個故事也是出自印度。在世界中心貝拿勒斯的聖廟裏,安放着一個黃銅板,板上插着三根寶石針。每根針高約1腕尺,像韭菜葉那樣粗細。梵天在創造世界的時候,在其中的一根針上從下到上放下了由大到小的64片金片。這就是所謂的梵塔。不論白天黑夜,都有一個值班的僧侶按照梵天不渝的法則,把這些金片在三根針上移來移去:一次只能移一片,而且要求無論在哪一根針上,小片永遠在大片的上面。當全部64片都從梵天創造世界時所放的那根針上移到另一根針上時,世界就將在一聲霹靂中消滅,梵塔、廟宇和衆生都將玉石俱焚。這其實就是咱們要說的漢諾塔問題,和第一個故事同樣,要把這座梵塔所有64片金片都移到另外一根針上,所須要的時間按照數學級數公式計算出來:1 + 2 + 22 + 23 + 24 + ...... 262 + 263 = 264 - 1 = 18 446 744 073 709 551 615

  一年有31 558 000秒,假如僧侶們每一秒鐘移動一次,日夜不停,節假日照常幹,也須要將近5800億年才能完成!

  好了,如今讓咱們來試着實現漢諾塔的算法。

  爲了說明漢諾塔中每個小塊的移動過程,咱們先考慮簡單一點的狀況。假設漢諾塔只有三層,借用百度百科的圖,移動過程以下:

  一共須要七步。咱們用代碼描述以下:

function hanoi(plates, source, helper, dest, moves = []) {
    if (plates <= 0) {
        return moves;
    }
    if (plates === 1) {
        moves.push([source, dest]);
    } else {
        hanoi(plates - 1, source, dest, helper, moves);
        moves.push([source, dest]);
        hanoi(plates - 1, helper, source, dest, moves);
    }
    return moves;
}

  下面是執行結果:

console.log(hanoi(3, 'source', 'helper', 'dest'));
[
  [ 'source', 'dest' ],
  [ 'source', 'helper' ],
  [ 'dest', 'helper' ],
  [ 'source', 'dest' ],
  [ 'helper', 'source' ],
  [ 'helper', 'dest' ],
  [ 'source', 'dest' ]
]

  能夠試着將3改爲大一點的數,例如14,你將會獲得以下圖同樣的結果:

  若是咱們將數改爲64呢?就像上面第二個故事裏所描述的同樣。恐怕要令你失望了!這時候你會發現你的程序沒法正確返回結果,甚至會因爲超出遞歸調用的嵌套次數而報錯。這是因爲移動64層的漢諾塔所須要的步驟是一個很大的數字,咱們在前面的故事中已經描述過了。若是真要實現這個過程,這個小程序恐怕很難作到了。

  搞清楚了漢諾塔的移動過程,咱們能夠將上面的代碼進行擴充,把咱們在前面定義的棧的數據結構應用進來,完整的代碼以下:

function towerOfHanoi(plates, source, helper, dest, sourceName, helperName, destName, moves = []) {
    if (plates <= 0) {
        return moves;
    }
    if (plates === 1) {
        dest.push(source.pop());
        const move = {};
        move[sourceName] = source.toString();
        move[helperName] = helper.toString();
        move[destName] = dest.toString();
        moves.push(move);
    } else {
        towerOfHanoi(plates - 1, source, dest, helper, sourceName, destName, helperName, moves);
        dest.push(source.pop());
        const move = {};
        move[sourceName] = source.toString();
        move[helperName] = helper.toString();
        move[destName] = dest.toString();
        moves.push(move);
        towerOfHanoi(plates - 1, helper, source, dest, helperName, sourceName, destName, moves);
    }
    return moves;
}

function hanoiStack(plates) {
    const source = new Stack();
    const dest = new Stack();
    const helper = new Stack();

    for (let i = plates; i > 0; i--) {
        source.push(i);
    }

    return towerOfHanoi(plates, source, helper, dest, 'source', 'helper', 'dest');
}

  咱們定義了三個棧,用來表示漢諾塔中的三個針塔,而後按照函數hanoi()中相同的邏輯來移動這三個棧中的元素。當plates的數量爲3時,執行結果以下:

[
  {
    source: '[object Object]',
    helper: '[object Object]',
    dest: '[object Object]'
  },
  {
    source: '[object Object]',
    dest: '[object Object]',
    helper: '[object Object]'
  },
  {
    dest: '[object Object]',
    source: '[object Object]',
    helper: '[object Object]'
  },
  {
    source: '[object Object]',
    helper: '[object Object]',
    dest: '[object Object]'
  },
  {
    helper: '[object Object]',
    dest: '[object Object]',
    source: '[object Object]'
  },
  {
    helper: '[object Object]',
    source: '[object Object]',
    dest: '[object Object]'
  },
  {
    source: '[object Object]',
    helper: '[object Object]',
    dest: '[object Object]'
  }
]

   棧的應用在實際編程中很是廣泛,下一章咱們來看看另外一種數據結構:隊列。

相關文章
相關標籤/搜索