用js來實現那些數據結構04(棧01-棧的實現)


  其實說到底,在js中棧更像是一種變種的數組,只是沒有數組那么多的方法,也沒有數組那么靈活。但是棧和隊列這兩種數據結構比數組更加的高效和可控。而在js中要想模擬棧,依據的主要形式也是數組。

  從這篇文章開始,可能會接觸到一些原型,原型鏈,類,構造函數等相關的js概念,但是這里並不會過多的介紹這些概念,必要的時候會進行一些簡要的說明,推薦大家去看看湯姆大叔的深入理解Javascript系列,王福朋大神的深入理解Javascript原型和閉包系列。都是極為不錯的深度好文,推薦大家可以深入學習。

  要想實現一個數據結構,首先你要明白它的基本原理,那么棧是什么?又是如何工作的呢?

  棧(stack)是一種遵循后進先出(Last In First Out)原則的有序集合。新添加的元素和待刪除的元素都保存在棧的同一端,稱為棧頂,另一端就叫做棧底。在棧里,新元素都接近棧頂,舊元素都靠近棧底。其實可以把棧簡單理解成往一個木桶里堆疊的放入物品,最后放進去的在桶的頂端,也是可以最先拿出來的,而最先放進去的卻在桶的底部,只有把所有上面的物品拿出來之后才可以拿走底部的物品。

  對於數組來說,可以添加元素,刪除元素,獲取數組的長度以及返回對應下標得到值,那么在開始構造一個棧之前,我們需要了解一下棧都有哪些基本操作。

  1、壓棧,也稱之為入棧,也就是把元素加入棧中。就像是數組中的push一樣。

  2、出棧,移除棧頂的元素。就像是數組中的pop一樣。

  3、獲取棧頂的元素,不對棧做任何其他操作。就像是在數組中通過下標獲取對應的值一樣。

  4、判斷棧是否為空。就像是判斷數組的長度是否為0一樣。

  5、清空棧,也就是移除棧里的所有元素。就像是把數組的長度設置為0一樣。

  6、獲取棧里的元素個數。就像是數組的length屬性一樣。

  那么,我相信我大家已經對棧有了一個基本的了解,那么我們接下來就看看如何通過構造函數來實現一個自己的js棧。

function Stack () {
    var items = [];

    //首先,我們來實現一個入棧的方法,這個方法負責往棧里加入元素,要注意的是,該方法只能添加元素到棧頂,也就是棧的尾部。
    this.push = function (ele) {
        items.push(ele)
    }

}

var stack = new Stack();

  我們聲明一個構造函數,並且在構造函數中生命一個私有變量items,作為我們Stack類儲存棧元素的基本支持。然后,加入一個push方法,通過this來使其指向調用該方法的實例。下面我們還會通過這樣的方式依次添加其他的方法。

function Stack () {
    var items = [];

    //首先,我們來實現一個入棧的方法,這個方法負責往棧里加入元素,要注意的是,該方法只能添加元素到棧頂,也就是棧的尾部。
    this.push = function (ele) {
        items.push(ele);
    }

    //然后我們再添加一個出棧的方法,同樣的,我們只能移除棧頂的元素。
    this.pop = function (ele) {
        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())
    }
}

  這樣我們就通過構造函數完整的創建了一個棧。我們可以通過new命令實例化一個Stack對象來測試一下我們的棧好不好用。

var stack = new Stack();
console.log(stack.isEmpty());//true
stack.push(1);
stack.print();
stack.push(3);
stack.print();
console.log(stack.isEmpty());//false
console.log(stack.size());//2
stack.push(10);
stack.print();
stack.pop();
stack.print();
stack.clear();
console.log(stack.isEmpty());//true

  我們發現我們的Stack類執行的還算不錯。那么還有沒有其他的方式可以實現Stack類呢?在ES6之前我可能會遺憾懵懂的對你Say No。但是現在我們可以一起來看看ES6帶我們的一些新鮮玩意。

   在開始改造我們的Stack類之前,需要先說一下ES6的幾個概念。Class語法Symbol基本類型WeakMap。簡單解釋一下,以對后面的改造不會一臉懵逼,而大家想要更深入的了解ES6新增的各種語法,可以去自行查閱。

  Class語法簡單來說就是一個語法糖,它的功能ES5也是完全可以實現的,只是這樣看寫起來更加清晰可讀,更像是面向對象的語法。

  Symbol是ES6新增的一個基本類型,前面幾篇文章說過,ES5只有6中數據類型,但是在ES6中又新增一種數據類型Symbol,它表示獨一無二的值。

  WeakMap,簡單來說就是用於生成鍵值對的集合,就像是對象({})一樣,WeakMap的一個重要用處就是部署私有屬性。

  當然,上面的簡單介紹可不僅僅是這樣的,真正的內容要比這些多得多。

  那么在大家知道了它們的一些基本意義。咱們開始改造一下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 = [];
  }

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

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

}

  這是用class來實現的Stack類,其實我們可以看一下,除了使用了constructor構造方法以外,其實並沒有什么本質上的區別。

  那么我們還可以使用Symbol數據類型來實現,簡單改造一下:

const _items = Symbol('stackItems');

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

  toString() {
    return this[_items].toString();
  }
}

  使用Symbol也沒有大的變化,只是聲明了一個獨一無二的_items來代替構造方法中的數組。

  但是這樣的實現方式有一個弊端,那就是ES6新增的Object.getOwnPropertySymbols方法可以讀取到類里面聲明的所有Symbols屬性。

const stack = new Stack();
const objectSymbols = Object.getOwnPropertySymbols(stack);
stack.push(1);
stack.push(3);
console.log(objectSymbols.length); // 1
console.log(objectSymbols); // [Symbol()]
console.log(objectSymbols[0]); // Symbol()
stack[objectSymbols[0]].push(1);
stack.print(); // 1, 3, 1

  不知道大家注意沒有,我們定義的Symbol是在構造函數之外的,因此誰都可以改動它。所以這樣的方式還不是很完善的。那么我們還可以使用ES6的WeakMap,然后用閉包實現私有屬性。

//通過閉包把聲明的變量變成私有屬性
let Stack = (function () {
//聲明棧的基本依賴
const _items = new WeakMap();
//聲明計數器
const _count = new WeakMap();

class Stack {
  constructor() {
//初始化stack和計數器的值,這里的set是WeakMap的自身方法,通過set和get來設置值和取值,這里用this作為設置值的鍵名,那this又指向啥呢?自行console!
    _count.set(this, 0);
    _items.set(this, {});
  }

  push(element) {
//在入棧之前先獲取長度和棧本身
    const items = _items.get(this);
    const count = _count.get(this);
//這里要注意_count可是從0開始的噢
    items[count] = element;
    _count.set(this, count + 1);
  }

  pop() {
//如果為空,那么則無法出棧
    if (this.isEmpty()) {
      return undefined;
    }
//獲取items和count,使長度減少1
    const items = _items.get(this);
    let count = _count.get(this);
    count--;
//重新為_count賦值
    _count.set(this, count);
//刪除出棧的元素,並返回該元素
    const result = items[count];
    delete items[count];
    return result;
  }

  peek() {
    if (this.isEmpty()) {
      return undefined;
    }
    const items = _items.get(this);
    const count = _count.get(this);
//返回棧頂元素
    return items[count - 1];
  }

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

  size() {
    return _count.get(this);
  }

  clear() {
    /* while (!this.isEmpty()) {
        this.pop();
      } */
    _count.set(this, 0);
    _items.set(this, {});
  }

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

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

return Stack;
})()

const stack = new Stack();
stack.push(1);
stack.push(3);
stack.print(); // 1, 3, 1

  這是最終比較完善的版本了。那么不知道大家注沒注意到一個小細節,前面我們只是聲明一個變量,先不管他是不是私有的,就是數組,整個Stack構造函數都是基於items數組來進行各種方法的。

但是這里通過WeakMap作為基本,我們卻多用了一個_count,前面說了_count是計數器,那么為啥要用計數器?因為WeakMap是鍵值對的“對象類型”,本身是沒有像數組這樣的長度之說的,所以需要一個計數器來代替數組的下標,以實現基於Stack的各種方法。

   到這里基本上就完成了我們的棧,下一篇文章會看看如何用我們寫好的棧去做一些有趣事情。

 

  最后,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!

  


免責聲明!

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



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