學習JavaScript數據結構與算法 — 散列表

定義

散列表是字典(鍵、值對)的一種實現方式。每次在字典中獲取一個值,都須要重複遍歷字典,若是用散列表,字典中的每一個key都對應一個肯定的位置,從而再也不須要遍歷。
以電子郵件地址簿爲例,每一個名字(key)對應一個郵件地址,用散列函數計算每一個key在散列表中的位置(這裏使用key的全部字符的ASCII碼值相加),如圖:javascript

clipboard.png

方法

  • put(key,value):向散列表增長一個新的項(也能更新散列表)。html

  • remove(key):根據鍵值從散列表中移除值。java

  • get(key):返回根據鍵值檢索到的特定的值。segmentfault

實現

function HashTable() {
    // 私有變量table,做爲散列表的載體
    var table = [];
    // 散列函數,計算key對應的hash值
    var loseloseHashCode = function (key) {
        var hash = 0;
        for (var i = 0; i < key.length; i++) {
            hash += key.charCodeAt(i); // 全部字符的ASCII碼值相加
        }
        // 爲了將hash值變爲更小的值,除以一個數並取餘數
        // 這裏除以素數37是爲了下降計算出重複hash的機率(後續會處理hash重複的問題)
        return hash % 37;
    };
    // put方法,向散列表增長一個新的項(也能更新散列表)
    this.put = function(key, value) {
        var position = loseloseHashCode(key); // 計算key的hash值做爲當前數據在散列表中的位置
        table[position] = value; // 將當前數據插入散列表
    };
    // get方法,返回根據鍵值檢索到的特定的值
    this.get = function (key) {
        return table[loseloseHashCode(key)]; //根據key計算出的hash取對應位置中的值
    };
    // remove方法,根據鍵值從散列表中移除值
    this.remove = function(key) {
        table[loseloseHashCode(key)] = undefined;
    };
}

到這裏,一個基本的的散列表已經實現了,但沒有考慮散列函數計算出重複hash值的問題,這會致使後添加的數據覆蓋先添加的數據,好比:app

var table = new HashTable();
// Jamie和Sue的hash值都爲5,所以Sue的數據會覆蓋Jamie的數據
table.put('Jamie', 'Jamie@qq.com');
table.put('Sue', 'Sue@gmail.com');

處理上述衝突的方式主要有:分離連接、線性探查,雙散列法,這裏使用前兩種。函數

分離連接

分離連接法在散列表的每個位置建立一個鏈表並將元素存儲在裏面。它的缺點是在HashTable實例以外還須要額外的存儲空間。如圖,散列表的每個位置都是一個鏈表,鏈表裏能夠存儲多個數據。性能

clipboard.png

下面,重寫put、get、remove方法,實現散列表的分離連接(其中鏈表類的實現參照鏈表)。this

// 首先要添加一個新的輔助類來實例化添加到鏈表的元素
    var ValuePair = function(key, value){
        this.key = key;
        this.value = value;
    };
    // 改寫put方法
    this.put = function(key, value){
        var position = loseloseHashCode(key);
        if (table[position] == undefined) {
            // 在當前位置示例化一個鏈表
            table[position] = new LinkedList();
        }
        // 在鏈表中添加元素
        table[position].append(new ValuePair(key, value));
    };
    // 改寫get方法
    this.get = function(key) {
        var position = loseloseHashCode(key);
        if (table[position] !== undefined){
            // 獲取鏈表的第一個元素
            var current = table[position].getHead();
            // 遍歷鏈表(這裏不能遍歷到最後一個元素,後續特殊處理)
            while(current.next){
                // 若是鏈表中存在當前key對應的元素,返回其值
                if (current.element.key === key){
                    return current.element.value;
                }
                // 處理下一個元素
                current = current.next;
            }
            // 處理鏈表只有一個元素的狀況或處理鏈表的最後一元素
            if (current.element.key === key){
                return current.element.value;
            }
        }
        // 不存在值,返回undefined
        return undefined;
    };
    // 改寫remove方法
    this.remove = function (key) {
        var position = loseloseHashCode(key);
        if (table[position] !== undefined) {
            // 獲取當前位置鏈表的第一個元素
            var current = table[position].getHead();
            // 遍歷鏈表(這裏不能遍歷到最後一個元素,後續特殊處理)
            while (current.next) {
                if (current.element.key === key) {
                    // 遍歷到對應元素,從鏈表中刪除
                    table[position].remove(current.element);
                    if (table[position].isEmpty()) {
                        // 若是鏈表已經空了,將散列表的當前位置置爲undefined
                        table[position] = undefined;
                    }
                    // 返回true表示刪除成功
                    return true;
                }
                // 處理下一個元素
                current = current.next;
            }
            // 處理鏈表只有一個元素的狀況或處理鏈表的最後一元素
            if (current.element.key === key) {
                table[position].remove(current.element);
                if (table[position].isEmpty()) {
                    table[position] = undefined;
                }
                return true;
            }
        }
        // 要刪除的元素不存在,返回false
        return false;
    };

線性探查

線性探查法在向散列表中插入元素時,若是插入位置position已經被佔據,就嘗試插入position+1的位置,以此類推,直到找到空的位置。下面用線性探查的方式重寫put、get、remove方法spa

// 重寫put方法
    this.put = function(key, value){
        var position = loseloseHashCode(key);
        // 依次查找,若是當前位置不爲空,position + 1,直到找到爲空的位置爲止
        while (table[position] != undefined){
            position++;
        }
        table[position] = new ValuePair(key, value);
    };
    // 重寫get方法
    this.get = function(key) {
        var position = loseloseHashCode(key);
        var len = table.length;
        // 只要當前位置小於散列表長度就要查找
        if (position < len){
            // 因爲查找的值多是以 position + 1 的形式類推,找到空位後插入的
            // 所以須要從當前位置(position)開始查找,直到找到key相同的位置,或者找完整個散列表
            while (position < len && (table[position] === undefined || table[position].key !== key)){
                position++;
            }
            // 若是最終position >= len,說明沒找到
            if (position >= len) {
                return undefined
            } else {
                // 不然說明找到了,返回對應值
                return table[position].value;
            }
        }
        // 若是當前位置爲空,說明添加時沒有累加position,直接返回undefined
        return undefined;
    };
    // 改寫remove方法
    this.remove = function(key) {
        var position = loseloseHashCode(key);
        var len = table.length;
        if (position < len){
            // 從當前位置(position)開始查找,直到找到key相同的位置,或者找完整個散列表
            while (position < len && (table[position] === undefined || table[position].key !== key)){
                position++;
            }
            // 若是最終position < len,說明找到了,將對應位置數據刪除
            if (position < len) {
                table[position] = undefined;
            }
        }
    };

更好的散列函數

上述散列函數表現並很差,它極易計算出相同的hash值,從而致使衝突。一個表現良好的散列函數應該有較好的插入和查找性能且有較低的衝突可能性。下面的散列函數,被證實是比較合適的。code

var djb2HashCode = function (key) {
    var hash = 5381; // 一個較大的素數基準值
    for (var i = 0; i < key.length; i++) {
        hash = hash * 33 + key.charCodeAt(i); // 基準值乘以33再加ASCII碼值
    }
    return hash % 1013; //除以1013取餘
};
相關文章
相關標籤/搜索