《學習javascript數據結構與算法》 集合,字典和散列表

集合

集合是由一組無序且唯一(即不能重複)的項組成的。這個數據結構使用了與有限集合相同 的數學概念,但應用在計算機科學的數據結構中。在數學中,集合也有並集、交集、差集等基本操作.

我們要實現的類就是以ECMAScript 6中Set類的實現爲基礎的

有一個非常重要的細節,我們使用對象而不是數組來表示集合(items)。但也可以用數組 實現。在這裏我們用對象來實現,稍微有點兒不一樣,也學習一下實現相似數據結構的新方法。 同時,JavaScript的對象不允許一個鍵指向兩個不同的屬性,也保證了集合裏的元素都是唯一的

接下來,需要聲明一些集合可用的方法(我們會嘗試模擬與ECMAScript 6實現相同的Set類

方法 詳情
add(value) 向集合添加一個新的項。
remove(value) 從集合移除一個值。
has(value) 如果值在集合中,返回true,否則返回false。
clear() 移除集合中的所有項。
size() 返回集合所包含元素的數量。與數組的length屬性類似
values() 返回一個包含集合中所有值的數組。

具體代碼

function Set(){
    let items = {};
    this.has = function(value){
        return items.hasOwnProperty(value);
    }
    this.remove = function(value){
        if(this.has(value)){
            delete items[value];
            return true;
        }
        return false;
    }
    this.clear = function(){
        items = {};
    }
    this.add = function(value){
        if(!this.has(value)){
            items[value] = value;
            return true
        }
        return false;
    }
    this.size = function(){
        return Object.keys(items).length;
    }
    this.values = function(){
        let keys = [];
        for(let key in items){
            keys.push(key); 
        }
        return keys;
    }
    //並集 
    this.union = function(otherSet){
        let unionSet = new Set();

        let currentValues = this.values();
        for(let i in currentValues){
            unionSet.add(currentValues[i])
        }

        let otherValues =  otherSet.values();
        for(let j in otherValues){
            unionSet.add(otherValues[j])
        }

        return unionSet;
    }
    // 交集
    this.intersection = function(others){
        let values = this.values();
        let intersection = new Set();
        for(let key in values){
            if(others.has(key)){
                intersection.add(key)
            }
        }
        return intersection;
    }
    //差集
    this.difference = function(otherSet){
        let difference = this.values();
        let differenceSet = new Set();
        for(let key in values){
            if(!others.has(key)){
                differenceSet.add(key)
            }
        }
        return differenceSet;
    }
    this.subset = function(otherSet){
        if (this.size() > otherSet.size()){ 
            return false;
        } else {
            var values = this.values();
            for (var i=0; i<values.length; i++){ 
                if (!otherSet.has(values[i])){ 
                    return false; 
                } 
            }
            return true; 
        }
    }
}

字典和散列表

字典

集合、字典和散列表可以存儲不重複的值。在集合中,我們感興趣的是每個值本身,並把它 當作主要元素。在字典中,我們用[鍵,值]的形式來存儲數據。在散列表中也是一樣(也是以[鍵, 值]對的形式來存儲數據)。但是兩種數據結構的實現方式略有不同,本章中將會介紹。

我們將要實現的類就是以ECMAScript 6中Map類的實現爲基礎的。你會發現它和 類很相似(但不同於存儲[值,值]對的形式,我們將要存儲的是[鍵,值]對)。

創建一個字典 他有如下方法

方法 詳情
set(key,value) 向字典中添加新元素。
remove(key) 通過使用鍵值來從字典中移除鍵值對應的數據值。
has(key) 如果某個鍵值存在於這個字典中,則返回true,反之則返回false。
clear() 將這個字典中的所有元素全部刪除。
size() 返回字典所包含元素的數量。與數組的length屬性類似。
keys() 將字典所包含的所有鍵名以數組形式返回。
values() 將字典所包含的所有數值以數組形式返回。

代碼

function Dictionary(){

    let items = {};

    this.has = function(key){
        return key in items;
    };

    this.get = function(key){
        return this.has(key)?items[key]:undefined;
    };

    this.set = function(key,value){
        items[key] = value;
    }

    this.remove = function(key){
        if(this.has(key)){
            delete items[key];
            return true;
        }
        return false;
    }

    this.values = function(){
        let result = [];
        for(let key in items){
            if (this.has(key)) {
                result.push(items[key])
            }
        }
        return result;
    }

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

var dictionary = new Dictionary();
dictionary.set('Gandalf', '[email protected]');
dictionary.set('John', '[email protected]');
dictionary.set('Tyrion', '[email protected]');

console.log(dictionary.has("Gandalf"));
console.log(dictionary.get("Gandalf"));

散列表

散列算法的作用是儘可能快地在數據結構中找到一個值。在之前的章節中,你已經知道如果 要在數據結構中獲得一個值(使用get方法),需要遍歷整個數據結構來找到它。如果使用散列 函數,就知道值的具體位置,因此能夠快速檢索到該值。散列函數的作用是給定一個鍵值,然後 返回值在表中的地址。

舉個例子,我們繼續使用在前一節中使用的電子郵件地址簿。我們將要使用最常見的散列函 數——“lose lose”散列函數,方法是簡單地將每個鍵值中的每個字母的ASCII值相加

創建一個散列表

function HashTable() {
    var table = [];
    var loseloseHashCode = function (key) {
        let hash = "";
        for(let i=0; i<key.length;i++){
            hash += key.charCodeAt(i);
        }
        return hash%37;
    }
    this.put = function(key,value){
        let position = loseloseHashCode(key);
        table[position] = value;
    }
    this.get = function (key) {
        return table[loseloseHashCode(key)];
    };
    this.remove = function(key){
        table[loseloseHashCode(key)] = undefined;
    }
}

處理散列表中的衝突

有時候,一些鍵會有相同的散列值。不同的值在散列表中對應相同位置的時候,我們稱其爲衝突
處理衝突有幾種方法:分離鏈接、線性探查和雙散列法。我們會介紹前兩種方法。

分離鏈接( 散列表+鏈表 )
分離鏈接法包括爲散列表的每一個位置創建一個鏈表並將元素存儲在裏面。它是解決衝突的最簡單的方法,但是它在HashTable實例之外還需要額外的存儲空間

我們在之前的測試代碼中使用分離鏈接的話,輸出結果將會是這樣

對於分離鏈接和線性探查來說,只需要重寫三個方法:put、get和remove。這三個方法在 每種技術實現中都是不同的。

爲了實現一個使用了分離鏈接的HashTable實例,我們需要一個新的輔助類來表示將要加入 LinkedList實例的元素。我們管它叫ValuePair類(在HashTable類內部定義):

具體實現

function HashMap(){
    let table = [];

    var loseloseHashCode = function (key) {
        let hash = "";
        for(let i=0; i<key.length;i++){
            hash += key.charCodeAt(i);
        }
        return hash%37;
    }

    var ValuePair = function(key, value){
        this.key = key;
        this.value = value;
        this.toString = function(){
            return "["+this.key+"-"+this.value+"]"
        }
    }

    this.put = function(key,value){
        let position = loseloseHashCode(key);
        if(table[position]===undefined){
            table[position] = new LinkedList();
        }
        table[position].append(new ValuePair(key,value))
    }

    this.get = function(key){
        let position = loseloseHashCode(key);
        if(table[position]){
            let current = table[position].getHead();
            while(current.next){
                if(current.element.key==key){
                    return current.element.value
                }
                current = current.next;
            }
            //檢查元素在鏈表第一個或最後一個節點的情況
            if (current.element.key === key){ //{9}
                return current.element.value;
            }
        }else{
            return undefined
        }
    }

    this.remove = function(key){
        let position = loseloseHashCode(key);
        if(table[position]!==undefined){
            let current = table[position].getHead();
            while(current.next){
                if ( current.element.key === key ){
                    table[position].remove(current.element);
                    if (table[position].isEmpty()){ 
                        table[position] = undefined; 
                    }
                    return true; 
                }
                current = current.next;
            }
            if (current.element.key === key){ //{16}
                table[position].remove(current.element);
                if (table[position].isEmpty()){
                    table[position] = undefined;
                }
                return true;
            }
        }
        return false;
    }
}

function LinkedList(){
    let Node = function(element){
        this.element = element;
        this.next = null;
    }
    let length = 0;
    let head = null;  // 保存第一個元素的引用
    this.append = function(element){
        let node = new Node(element);
        let current;
        if(head==null){
            head = node;
        }else{
            current = head;
            // 以head 爲起點查找最後一個next屬性不爲空的節點
            while(current.next){
                current = current.next;
            }
            // 把當前創建的節點添加到尾部
            current.next = node;
        }
        length++;
    };
    this.insert = function(position,element){
        if(position>-1&&position<length){
            let node = new Node(element);
            let current = head;
            let index=0;
            let previous;
            if(position===0){
                node.next = current;
                head = node;
            }else{
                // 查找到 position 對應節點上一個和下一個值
                while (index++ < position){ 
                    previous = current;
                    current = current.next;
                }
                previous.next = node;
                node.next = current;
            }
            length++;
            return true;
        }else{
            return false;
        }
    };
    this.removeAt = function(position){
        //檢查越界值
        if (position > -1 && position < length){
            var current = head,
                previous, 
                index = 0; 
            
            if (position === 0){ 
                //如果要刪除的是第一個元素就讓 head 指向current的next 
                head = current.next; 
            } else {
                while (index ++ < position){ 
                    previous = current;    
                    current = current.next; 
                }
                // 連接指定位置的上一個元素和下一個元素
                previous.next = current.next; // {9}
            }
            length--; // {10}
            return current.element;
        } else {
            return null; // {11}
        } 
    }
    this.remove = function(element){
        let index = this.indexOf(element);
        return this.removeAt(index);
    };
    this.indexOf = function(element){
        let current = head;
        let index = -1;
        while(current){
            if(current.element = element){
                return index; 
            }
            index++;
            current = current.next;
        }
        return index;
    };
    this.isEmpty = function(){
        return length == 0;
    };
    this.size = function() {
        return length;
    };
    this.toString = function(){
        let current = head;
        let string = '';
        while(current){
            string += current.element;
            current = current.next;
        }
        return string;
    };
    this.print = function(){};
    this.getHead= function(){
        return head;
    }
}

線性探查

另一種解決衝突的方法是線性探查。當想向表中某個位置加入一個新元素的時候,如果索引 爲index的位置已經被佔據了,就嘗試index+1的位置。如果index+1的位置也被佔據了,就嘗試 index+2的位置,以此類推。

代碼

function HashMap(){
    let table = [];

    var loseloseHashCode = function (key) {
        let hash = "";
        for(let i=0; i<key.length;i++){
            hash += key.charCodeAt(i);
        }
        return hash%37;
    }

    var ValuePair = function(key, value){
        this.key = key;
        this.value = value;
        this.toString = function(){
            return "["+this.key+"-"+this.value+"]"
        }
    }

    this.put = function(key,value){
        let position = loseloseHashCode(key);
        if(table[position]===undefined){
            table[position] = new ValuePair(key,value);
        }else{
            let index = ++ position;
            while(table[index]!==undefined){
                index ++;
            }
            table[index] = new ValuePair(key,value);
        }
    }

    this.get = function(key){
        let position = loseloseHashCode(key);
        if(table[position]){
            if(table[position].key==key){
                return table[position].value;
            }else{
                let index = ++position;

                while(table[index]==undefined||table[index]['key']!==key){
                    index++
                }

                if(table[index]['key']==key){
                    return table[index].value;
                }

            }
        }
        return undefined
    }

    this.remove = function(key){
        let position = loseloseHashCode(key);
        if(table[position]){
            if(table[position].key==key){
                return table[position]=undefined;
            }else{
                let index = ++position;
                while(table[index]==undefined||table[index]['key']!==key){
                    index++;
                }
                if(table[index]['key']==key){
                    return table[index] = undefined;
                }

            }
        }
        return false
    }
}

創建更好的散列函數

我們實現的“lose lose”散列函數並不是一個表現良好的散列函數,因爲它會產生太多的衝 突。如果我們使用這個函數的話,會產生各種各樣的衝突。一個表現良好的散列函數是由幾個方 面構成的:插入和檢索元素的時間(即性能),當然也包括較低的衝突可能性。我們可以在網上 找到一些不同的實現方法,或者也可以實現自己的散列函數。

 var djb2HashCode = function (key) {
        var hash = 5381; //{1}
        for (var i = 0; i < key.length; i++) { //{2}
            hash = hash * 33 + key.charCodeAt(i); //{3}
        }
        return hash % 1013; //{4}
    };

它包括初始化一個hash變量並賦值爲一個質數(行{1}——大多數實現都使用5381),然後 迭代參數key(行{2}),將hash與33相乘(用來當作一個魔力數),並和當前迭代到的字符的ASCII 碼值相加(行{3})。

最後,我們將使用相加的和與另一個隨機質數(比我們認爲的散列表的大小要大——在本例 中,我們認爲散列表的大小爲1000)相除的餘數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章