1. 程式人生 > 其它 >JS高階-資料結構的封裝

JS高階-資料結構的封裝

最近在看了《資料結構與演算法JavaScript描述》這本書,對大學裡學的資料結構做了一次複習(其實差不多忘乾淨了,哈哈)。如果能將這些知識撿起來,融入到實際工作當中,估計編碼水平將是一次質的飛躍。帶著這個美好的願望,開始學習吧O(∩_∩)O~~

我們知道在JS中,常常用來組織資料的無非是陣列和物件(這些基礎就不介紹了)。但在資料結構中,還有一些抽象的資料型別:列表、棧、佇列、連結串列、字典、雜湊、集合、二叉樹、圖等,可以用來更好的對實際場景建模。當然這些資料型別,原生JS不支援,那麼就需要通過封裝來模擬,其底層還是陣列和物件(被看穿嘍~),接下來我們挨個來解析吧

一、列表

定義:列表是一組有序的資料

,每個列表中的資料項稱為元素。元素可以是任意資料型別, 也不事先限定元素個數。 

生活中經常使用到列表,通訊錄、購物車、十佳榜單等。當不需要在一個很長的序列中查詢元素或排序可以使用列表。

列表的封裝程式碼:

function List() {//列表的建構函式
    this._dataStore = []; //初始化一個空陣列來儲存列表元素
    this._pos = 0;//當前的位置
}
List.prototype={
    constructor:List,
    clear:function(){//清空列表
        delete this._dataStore;
        this._dataStore = []; this._pos = 0;
    },
    find:function(element){//在列表中查詢某一元素,若有返回位置,否則返回-1
        for (var i = 0; i < this._dataStore.length; ++i) {
            if (this._dataStore[i] === element) {return i;}
        };return -1;
    },
    contains:function(element){//判斷給定值是否在列表中
        for (var i = 0; i < this._dataStore.length; ++i) {
            if (this._dataStore[i] === element) {return true; break;}
        };return false;
    },
    insert:function(element, after){//當前位置插入新元素
        var insert_pos = this.find(after);
        if (insert_pos > -1) {this._dataStore.splice(insert_pos+1, 0, element);return true;};
        return false;
    },
    append:function(element){this._dataStore[this._dataStore.length] = element;},//末尾新增新元素
    remove:function(element){//刪除元素
        var foundAt = this.find(element);
        if (foundAt > -1) {this._dataStore.splice(foundAt,1);return true;};
        return false;
    },
    front:function(){this._pos = 0;},//將當前位置指標設為表首
    end:function(){this._pos = this._dataStore.length-1;},//將當前位置指標設為表尾
    prev:function(){if (this._pos > 0) {--this._pos;}},//當前位置上移指標
    next:function(){if (this._pos < this._dataStore.length-1) {++this._pos;}},//當前位置下移指標
    moveTo:function(_position){this._pos = _position;},//移動當前位置指標到指定位置
    length:function(){return this._dataStore.length;},//獲取列表的中元素的個數
    curr_pos:function(){return this._pos;},//返回當前位置指標
    getElement:function(){return this._dataStore[this._pos];},//返回當前位置的列表項
    toString:function(){return this._dataStore;}//返回列表的字串形式
}

列表與陣列比較類似,只是簡單的對陣列做了二次封裝,用案例來展示一下列表的使用場景,進一步加深理解。

案例:影碟租賃自助查詢系統

var moviearr=['肖申克的救贖','教父','教父 2','低俗小說','黃金三鏢客','十二怒漢','辛德勒名單','黑暗騎士','指環王:王者歸來','搏擊俱樂部','星球大戰5:帝國反擊戰','飛越瘋人院','指環王:護戒使者','盜夢空間','好傢伙','星球大戰','七武士','黑客帝國','阿甘正傳','上帝之城']//資料

var movieList = new List();//電影列表
for (var i = 0; i < moviearr.length; ++i) {movieList.append(moviearr[i]);}//將資料新增到‘電影列表’
var customerList = new List();//客戶列表
function Customer(name, movie) {//客戶租賃物件的建構函式
    this.name = name;
    this.movie = movie;
}

function checkOut(name, movie, movieList, customerList) {//某客戶需要租賃某電影,同時維護兩個列表
        if(movieList.contains(movie)){//若檢索電影在列表中,新建客戶物件新增到客戶列表,同時在電影列表中刪除該電影
            var c = new Customer(name, movie);
            customerList.append(c);
            movieList.remove(movie);
        }else{console.log(movie + " is not available.");}//若不在電影列表中,則提示不可租賃
        //列印維護後的兩個列表
        console.log('movieList:'+movieList.toString()+'n customerList:'+JSON.stringify(customerList.toString()))
    }

checkOut('gavin','黑客帝國',movieList,customerList)
checkOut('gavin','星球大戰',movieList,customerList)
checkOut('zoe','辛德勒名單',movieList,customerList)
checkOut('gulei','黑客帝國',movieList,customerList)

二、棧

定義:棧是一種特殊的列表, 棧內的元素只能通過列表的一端訪問, 這一端稱為棧頂。 

棧是一種後入先出( LIFO, last-in-first-out) 的資料結構,任何不在棧頂的元素都無法訪問。 為了得到棧底的元素, 必須先拿掉上面的元素。生活中常見的例子如:餐廳的一摞盤子,只能從上面逐個取,洗淨的盤子也只能摞在最上面。

棧的封裝程式碼:

function Stack() {//棧的建構函式
    this._dataStore = [];//初始化一個空陣列來儲存列表元素
    this._top = 0;//記錄棧頂的位置
}
Stack.prototype={
    constructor:Stack,
    clear:function(){//清空棧
        delete this._dataStore;
        this._dataStore = []; this._top = 0;
    },
    push:function(element){this._dataStore[this._top++] = element;},//向棧內新增元素
    pop:function(){return this._dataStore[--this._top];},//從棧內取出元素
    peek:function(){return this._dataStore[this._top-1]},//檢視棧頂元素
    length:function(){return this._top;}//獲取列表的中元素的個數
}

相對列表來說,棧的方法不多顯得很簡潔,同樣來幾個案例,幫助理解棧的使用場景

案例一:迴文

function isPalindrome(word){
    var s = new Stack();
    for (var i = 0; i < word.length; ++i) {s.push(word[i]);}
    var rword = "";
    while (s.length() > 0) {rword += s.pop();}
    if (word == rword) {return true;}else {return false;}
}
console.log(isPalindrome("hello"));//false
console.log(isPalindrome("racecar"));//true

案例二:遞迴演示

function factorial(n) {
    if (n === 0) {return 1;}else {return n * factorial(n-1);}
}
function fact(n) {
    var s = new Stack();
    while (n > 1) {s.push(n--);}
    var product = 1;
    while (s.length() > 0) {product *= s.pop();}
    return product;
}
console.log(factorial(5))//120
console.log(fact(5))//120

三、佇列

定義:列隊也是一種特殊的列表, 不同的是佇列只能在隊尾插入元素, 在隊首刪除元素。 

列隊是一種先進先出( First-In-First-Out, FIFO)的資料結構。排在前面的優先處理,後面的依次排隊,直到輪到它。生活中常見的例子如:列印任務池,模擬櫃檯排隊的顧客等。

佇列的封裝程式碼:

function Queue() {//佇列的建構函式
    this._dataStore = [];//初始化一個空陣列來儲存元素
}
Queue.prototype={
    constructor:Queue,
    clear:function(){//清空佇列
        delete this._dataStore;
        this._dataStore = []; this._top = 0;
    },
    enqueue:function(element){this._dataStore.push(element)},//向隊尾新增一個元素
    dequeue:function(){return this._dataStore.shift();},//刪除隊首元素
    front:function(){return this._dataStore[0];},//讀取隊首元素
    back:function(){return this._dataStore[this._dataStore.length-1];},//讀取隊尾元素
    empty:function(){if(this._dataStore.length === 0){return true;}else{return false;}},//判斷佇列是否為空
    toString:function(){//將佇列元素拼接字串
        var retStr = "";
        for (var i = 0; i < this._dataStore.length; ++i) {retStr += this._dataStore[i] + ",";}
        return retStr;
    }
}

 列隊比棧稍微複雜一點,總體來說也是比較容易理解的。

案例:舞伴分配

function Dancer(name, sex) {
    this.name = name;
    this.sex = sex;
}

function getDancers(males, females) {
    var names = ['F Allison McMillan','M Frank Opitz','M Mason McMillan','M Clayton Ruff','F Cheryl Ferenback','M Raymond Williams','F Jennifer Ingram','M Bryan Frazer','M David Durr','M Danny Martin','F Aurora Adney'];
    for(var i = 0; i < names.length; ++i) {
        var dancer = names[i].split(" ");
        var sex = dancer[0];
        var name = dancer[1];
        if (sex == "F") {females.enqueue(new Dancer(name, sex));
        } else {males.enqueue(new Dancer(name, sex));}
    }
}
function dance(males, females) {
    console.log("The dance partners are: n");
    while (!females.empty() && !males.empty()) {
        var person1 = females.dequeue();
        var person2 = males.dequeue();
        console.log("Female dancer is: " + person1.name+" and the male dancer is: " + person2.name);
    }
}
var maleDancers = new Queue();
var femaleDancers = new Queue();
getDancers(maleDancers, femaleDancers);
dance(maleDancers, femaleDancers);
if (!femaleDancers.empty()) {console.log(femaleDancers.front().name + " is waiting to dance.");}
if (!maleDancers.empty()) {console.log(maleDancers.front().name + " is waiting to dance.");}

在一般情況下,從列表中刪除元素是優先刪除先入隊的元素,但有時候也可能需要使用一種優先佇列的資料來模擬,比如醫院的急診,主要通過給佇列中每個元素新增一個優先級別,並改寫dequeue方法實現。

dequeue:function() {
    var priority = this._dataStore[0].code;//code表示優先級別,數值越小優先順序越高
    for (var i = 1; i < this._dataStore.length; ++i) {priority =Math.min(priority,i);} 
    return this.dataStore.splice(priority,1);
}

四、連結串列

定義:連結串列是由一組節點組成的集合,每個節點都使用一個物件的引用指向下一個節點,這個引用叫做

除了對資料的隨機訪問,連結串列幾乎可以代替一維陣列。它與陣列的主要區別是:陣列的元素靠位置進行引用,連結串列靠相互指向進行引用。

連結串列的封裝程式碼:

function Node(element) {//連結串列中節點的建構函式
    this.element = element;
    this.next = null;
}
function LList() {//連結串列的建構函式
    this.head = new Node("head");
}
LList.prototype={
    constructor:LList,
    find:function(item){//查詢連結串列,如果找到則返回該節點,否者返回頭節點
        var currNode = this.head;
        while (currNode.element != item) {currNode = currNode.next;} 
        return currNode;
    },
    insert:function(newElement, item){//在找到的節點後,新增一個節點
        var newNode = new Node(newElement);//新增節點
        var current = this.find(item);//查詢節點
        newNode.next = current.next;//先將當前節點的next賦值給新節點的next
        current.next = newNode;//再將當前節點的next設定為新節點
    },
    display:function(){
        var currNode = this.head;
        while (currNode.next!==null){console.log(currNode.next.element);currNode = currNode.next; }
    },
    findPrev:function(item){//查詢連結串列,返回當前節點的上一個節點
        var currNode = this.head;
        while (currNode.next!==null && currNode.next.element!==item){ currNode = currNode.next; }
        return currNode;
    },
    remove:function(item){//在連結串列中刪除給定的節點
        var prevNode = this.findPrev(item);
        if (prevNode.next !== null) { prevNode.next = prevNode.next.next;}
    }
}

跟之前的三種資料結構不同,連結串列沒有采用陣列作為底層資料儲存。而是採用物件節點作為基礎,同時每個節點中都含有一個next屬性指向另一個物件,與優先佇列的中的優先級別code頗為類似。總體來看連結串列是通過每個節點的next屬性,將雜湊的物件連線到了一起

如上我們只是實現了單向連結串列,從頭遍歷到尾很簡單,想要反過來遍歷就沒那麼容易了。我們可以通過給節點增加一個prev屬性,指向它的前一個節點,也能實現雙向連結串列。當然,雙向連結串列在新增和刪除節點時的操作也要複雜一些,需要同時修改前後節點的next或prev屬性。

另外,我們還可以讓單向連結串列的尾節點指向首節點,這樣就變成了迴圈列表。這樣需要對連結串列的一些方法進行改造,防止遍歷連結串列時出現無限迴圈。

五、字典

定義:字典是一種以鍵值對形式儲存的資料結構。

JS中物件就是以字典的形式設計的,但字典的基礎是陣列,而不是物件。這樣可以進行排序,況且JS中一切皆物件,陣列也不例外。

字典的封裝程式碼:

function Dictionary() {//字典的建構函式
    this._datastore = new Array();
}
Dictionary.prototype={
    constructor:Dictionary,
    add:function(key,value){ this._datastore[key]=value; },//增加一條鍵值對
    find:function(key){ return this._datastore[key] },//查詢指定key,返回對應value的值
    remove:function(key){ delete this._datastore[key] },//刪除指定key的鍵值對
    showAll:function(){ //列印字典的所有鍵值對
        //若需排序可以給Object.keys(this._datastore)陣列追加sort方法
        Object.keys(this._datastore).forEach(function(key){console.log(key+" -> "+this._datastore[key]);}.bind(this)) 
    },
    count:function(){//返回字典所含鍵值對數量
        var n = 0;
        for(var key in this._datastore) {++n;}
        return n;
    },
    clear:function(){ //清空字典
        Object.keys(this._datastore).forEach(function(key){ delete this._datastore[key];}.bind(this))
    }
}

字典依然採用陣列作為底層資料儲存,但是與普通按序號索引的陣列不同,它只能以key進行查詢。

六、雜湊

定義:雜湊是一種常用的資料儲存技術, 雜湊後的資料可以快速地插入或取用。 雜湊使用的資料結構叫做散列表

是通過一個雜湊函式(Hash,雜湊)將鍵對映為一個範圍是 0 到散列表長度的數字。

雜湊的封裝程式碼:

function HashTable() {//雜湊的建構函式
    this._table = new Array(137);//陣列的長度應該為質數,即預算散列表的長度
}
HashTable.prototype={
    constructor:HashTable,
    simpleHash:function(data){//簡單的雜湊函式(返回鍵字串的ASCII累加除陣列長度的餘數)
        var total = 0;
        for (var i = 0; i < data.length; ++i) {total += data.charCodeAt(i);}
        return total % this._table.length;
    },
    betterHash:function(data){//更好的雜湊函式演算法,減少碰撞
        const H = 37;
        var total = 0;
        for (var i = 0; i < data.length; ++i) {total += H * total + data.charCodeAt(i);} 
        total = total % this._table.length;
        if (total < 0) {total += this._table.length-1;}
        return parseInt(total);
    },
    put:function(data){var pos = this.simpleHash(data);this._table[pos] = data;},//使用簡單雜湊函式
    //put:function(key,data){var pos = this.betterHash(key);this._table[pos] = data;},//使用高階雜湊函式
    showDistro:function(){//顯示散列表中的資料
        var n = 0;
        for (var i = 0; i < this._table.length; ++i) {
            if (this._table[i] !== undefined) {console.log(i + ": " + this._table[i]);}
        }
    },
    get:function(key){return this._table[this.betterHash(key)];},
}

雜湊其實是通過一種機制(雜湊函式),將資料儲存到散列表對應的位置上去,當機制跟內容相關時僅出現修改才會改變。(MD5類似雜湊函式的機制)

當雜湊函式對於多個輸入產生同樣的輸出時稱為碰撞。開鏈法(用陣列儲存多個相同輸出)和探測法(線性探測下個位置,直到有空值存入)

案列:資料儲存

var students = ["David", "Jennifer", "Donnie", "Raymond", "Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];
var hTable = new HashTable();
for (var i = 0; i < students.length; ++i) {hTable.put(students[i]);}
hTable.showDistro();//九條資料,被雜湊成八條,產生於了一個碰撞

七、集合

定義:是一種不含不同元素的資料結構,這些元素是無序且不重複的。

集合的封裝程式碼:

function Set() {//集合的建構函式
    this._dataStore = [];
}
Set.prototype={
    constructor:Set,
    add:function(data){//向集合中新增元素
        if (this._dataStore.indexOf(data) < 0) {this._dataStore.push(data);return true;
        } else {return false;}
    },
    remove:function(data){//從集合中移除元素
        var pos = this._dataStore.indexOf(data);
        if (pos > -1) {this._dataStore.splice(pos,1);return true;
        } else {return false;}
    },
    contains:function(){//檢查一個元素是否在集合中
        if (this._dataStore.indexOf(data) > -1) {return true;} else {return false;}
    },
    size:function(){return this._dataStore.length},//返回集合的長度
    union:function(set){//返回與另一個集合的並集
        var tempSet = new Set();
        for (var i = 0; i < this._dataStore.length; ++i) {tempSet.add(this._dataStore[i]);}
        for (var i = 0; i < set.dataStore.length; ++i) {
            if (!tempSet.contains(set.dataStore[i])) {tempSet.dataStore.push(set.dataStore[i]);}
        } 
        return tempSet;
    },
    intersect:function(set){//返回與另一個集合的交集
        var tempSet = new Set();
        for (var i = 0; i < this._dataStore.length; ++i) {
            if (set.contains(this._dataStore[i])) {tempSet.add(this._dataStore[i]);}
        } 
        return tempSet;
    },
    subset:function(set){//判斷集合是否其他集合的子集
        if (this.size() > set.size()) {return false;
        } else {
            this._dataStore.foreach(function(member){if (!set.contains(member)) {return false;}})
        } 
        return true;
    },
    difference:function(set){//返回與另一個集合的補集
        var tempSet = new Set();
        for (var i = 0; i < this._dataStore.length; ++i) {
            if (!set.contains(this._dataStore[i])) {tempSet.add(this._dataStore[i]);}
        } 
        return tempSet;
    },
    show:function(){return this._dataStore;},//顯示集合中的元素
}

集合的資料結構比較簡單,主要實現了新增元素時檢查唯一性,以及交集、並集、補集的方法和子集的檢查。

八、二叉樹和二叉查詢樹

定義:樹由一組以邊連線的節點組成,二叉樹是子節點不超過兩個的特殊樹。

二叉樹的封裝程式碼:

function Node2(data, left, right) {//二叉樹中節點的建構函式
    this.data = data;
    this.left = left;
    this.right = right;
    this.show = function(){return this.data;};
}
function BST(){//二叉查詢樹的建構函式
    this.root = null;
}
BST.prototype={
    constructor:BST,
    insert:function(data){//插入節點
        var n = new Node2(data, null, null);
        if (this.root == null) {
            this.root = n;
        } else {
            var current = this.root;
            var parent;
            while (true) {
                parent = current;
                if (data < current.data) {
                    current = current.left;if (current == null) {parent.left = n;break;}
                } else {
                    current = current.right;if (current == null) {parent.right = n;break;}
                }
            }
        }
    },
    inOrder:function(node){
        if (!(node == null)) {
        this.inOrder(node.left);
        console.log(node.show() + " ");
        this.inOrder(node.right);
        }
    },
    getMin:function(){//獲取最小的數,即最左節點
        var current = this.root;
        while (!(current.left == null)) {current = current.left;}
        return current.data;
    },
    getMax:function(){//獲取最大的數,即最右節點
        var current = this.root;
        while (!(current.right == null)) {current = current.right;}
        return current.data;
    },
    find:function(data){//查詢指定的值
        var current = this.root;
        while (current != null) {
            if (current.data == data) {return current;
            } else if (data < current.data) {current = current.left;
            } else {current = current.right;}
        } 
        return null;
    },
    remove:function(data){ root = this.removeNode(this.root, data);},//呼叫removeNode刪除節點
    removeNode:function(node,data){ //刪除節點
        if (node == null) {return null;}
        if (data == node.data) {
            if (node.left == null && node.right == null) {return null;} // 沒有子節點的節點
            if (node.left == null) {return node.right;} // 沒有左子節點的節點
            if (node.right == null) {return node.left;} // 沒有右子節點的節點
            // 有兩個子節點的節點
            var tempNode = getSmallest(node.right);
            node.data = tempNode.data;
            node.right = removeNode(node.right, tempNode.data);
            return node;
        } else if (data < node.data) {
            node.left = removeNode(node.left, data);
            return node;
        } else {
            node.right = removeNode(node.right, data);
            return node;
        }
    }
}

二叉樹有點類似連結串列的資料結構,採用節點的左右屬性來指向兩個子節點。

九、圖和圖演算法

定義:圖是由邊的集合即頂點的集合組成的。常用於地圖和航班等資訊資料的建模。

圖的封裝程式碼:

function Graph(v) {//圖的建構函式,v表示頂點的數量
    this.vertices = v;
    this.edges = 0;
    this.adj = [];
    for (var i = 0; i < this.vertices; ++i) {
        this.adj[i] = [];
        this.adj[i].push("");
    }
    this.marked = [];//遍歷標誌位
    for (var i = 0; i < this.vertices; ++i) {this.marked[i] = false;}
    this.edgeTo = [];//路徑查詢時,儲存兩個頂點之間的邊
}
Graph.prototype={
    constructor:Graph,
    addEdge:function(v,w){//增加一條從頂點v到頂點w的邊
        this.adj[v].push(w);
        this.adj[w].push(v);
        this.edges++;
    },
    showGraph:function(){var p='';//顯示當前圖的結構
        for (var i = 0; i < this.vertices; ++i) { p+='頂點'+i+' ->';
            for (var j = 0; j < this.vertices; ++j) {
                if (this.adj[i][j] !== undefined){ p+=this.adj[i][j]+' ';}
            };p+='n';
        }console.log(p)
    },
    dfs:function(v){//深度優先搜尋
        this.marked[v] = true;
        if (this.adj[v] !== undefined) {console.log("深度優先: " + v);}
        for(var w in this.adj[v]) {
            if(!this.marked[this.adj[v][w]]){this.dfs(this.adj[v][w]);}
        }
    },
    bfs:function(s){//廣度優先搜尋
        var queue = [];
        this.marked[s] = true;
        queue.push(s); // 新增到隊尾
        while (queue.length > 0) {
            var v = queue.shift(); // 從隊首移除
            if (v!==''&&v !== undefined) {console.log("廣度優先: " + v);} 
            for(var w in this.adj[v]) {
                if (!this.marked[this.adj[v][w]]) {
                    this.marked[this.adj[v][w]] = true;
                    this.edgeTo[this.adj[v][w]] = v;
                    queue.push(this.adj[v][w]);
                }
            }
        }
    },
    pathTo:function(v){//獲取最短路徑,即頂點v到頂點0的邊(必須先廣度搜索生成edgeTo)
        var source = 0;
        if (!this.marked[v]) {return undefined;}
        var path = [];
        for (var i = v; i != source; i = this.edgeTo[i]) {path.push(i);}
        path.push(source);
        return path;
    }
}

在對圖資料模型進行搜尋時,有深度優先和廣度優先兩種。當進行最短路徑查詢時,就是廣度優先搜尋的過程。