进修JavaScript数据结构与算法 — 散列表

定义

散列表是字典(键、值对)的一种完成体式格局。每次在字典中猎取一个值,都须要反复遍历字典,假如用散列表,字典中的每一个key都对应一个肯定的位置,从而不再须要遍历。
以电子邮件地址簿为例,每一个名字(key)对应一个邮件地址,用散列函数盘算每一个key在散列表中的位置(这里运用key的一切字符的ASCII码值相加),如图:

《进修JavaScript数据结构与算法 — 散列表》

要领

  • put(key,value):向散列表增添一个新的项(也能更新散列表)。

  • remove(key):依据键值从散列表中移除值。

  • get(key):返回依据键值检索到的特定的值。

完成

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值的题目,这会致使后增加的数据掩盖先增加的数据,比方:

var table = new HashTable();
// Jamie和Sue的hash值都为5,因而Sue的数据会掩盖Jamie的数据
table.put('Jamie', 'Jamie@qq.com');
table.put('Sue', 'Sue@gmail.com');

处置惩罚上述争执的体式格局主要有:星散链接、线性探查,双散列法,这里运用前两种。

星散链接

星散链接法在散列表的每一个位置建立一个链表并将元素存储在里面。它的瑕玷是在HashTable实例以外还须要分外的存储空间。如图,散列表的每一个位置都是一个链表,链内外能够存储多个数据。

《进修JavaScript数据结构与算法 — 散列表》

下面,重写put、get、remove要领,完成散列表的星散链接(个中链表类的完成参照链表)。

    // 首先要增加一个新的辅佐类来实例化增加到链表的元素
    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要领

    // 重写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值,从而致使争执。一个表现优越的散列函数应该有较好的插进去和查找机能且有较低的争执能够性。下面的散列函数,被证实是比较适宜的。

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取余
};
    原文作者:whale
    原文地址: https://segmentfault.com/a/1190000008556414
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞