美文网首页
数据结构——散列

数据结构——散列

作者: 柏丘君 | 来源:发表于2018-02-21 20:23 被阅读0次

    散列也叫作散列表,是一种非线性的数据结构,可以用来快速的查找和插入数据。在 JavaScript 中,通常使用数组来实现散列。
    散列的核心是一个计算特征值的方法,该方法将原始数据进行转换,返回一个索引,随后将值保存在数组对应的索引中。此外,该方法还可以用来进行高效的查找。
    由于不同的特征值计算出的索引不尽相同,因此散列在保存数据时不是按顺序保存的,其是基于列表这样一个线性结构上的“非线性”结构,因此被称为散列。
    通常,我们会将数组的长度设置为一个质数,因为质数的公约数最少,可以有效的防止碰撞。

    散列的代码实现

    下面是 IHashTable 的接口定义:

    interface IHashTable<T>{
        // 获取索引值
        getIndex(flag:string):number;
        // 两种存放方式:放入值,放入一个值和简称值
        put(flag:string,ele:T):void;
        // 获取值
        get(flag:string):T;
        // 移除元素
        remove(flag:string):T;
        // 获取散列中的元素
        toString():T[];    
    }
    

    上面的 IHashTable 中定义了一个 put 方法,该方法用来向散列中添加元素。其第一个参数是要添加元素的一个别名,第二个参数是元素本身。如:

    hashTable.put("MIKE","mike@mike.com")
    hashTable.put("JACK","jack@jack.com")
    

    下面是 HashTable 的代码实现:

    class HashTable<T> implements IHashTable<T>{
        // 散列表
        private dataStore:T[] = [];
        // 散列表的长度
        private dataStoreLen:number = 0;
        // 盐值
        private hashSalt:number = 71;
        constructor(dataStoreLen:number,hashSalt?:number){
            // 创建散列对象时传入散列表的长度和盐值
            this.dataStoreLen = dataStoreLen;
            if(hashSalt){
                this.hashSalt = hashSalt;
            }
        }
        getIndex(flag:string):number{
            let hash:number = -1,pos:number = -1;
            if(flag.length){
              hash = 0;
              for(const c of flag){
                  hash += c.charCodeAt(0);
              }
              pos = hash % this.dataStoreLen;
            }
            // 存放位置为计算出的 hash 值对数组的长度取余
            return pos;
        }
        put(flag:string,ele:T):void{
            const pos:number = this.getIndex(flag);
            this.dataStore[pos] = ele;
        }
        get(flag:string):T{
            const pos:number = this.getIndex(flag);
            return this.dataStore[pos];
        }
        remove(flag:string):T{
            const pos:number = this.getIndex(flag);
            const res = this.dataStore[pos];        
            this.dataStore[pos] = undefined;
            return res;
        }
        toString():T[]{
            const tmp:T[] = [];
            for(const ele of this.dataStore){
                if(ele !== undefined){
                    tmp.push(ele);
                }
            }
            return tmp;
        }
    }
    

    简单测试一下:

    const h = new HashTable<string>(301);
    h.put("MIKE","mike@mike.com")
    h.put("JACK","jack@jack.com")
    h.put("PENNY","penny@penny.com")
    h.put("JERRY","jerry@jerry.com")
    // 获取元素
    console.log(h.get("PENNY"))
    console.log(h.toString())
    // 移除元素
    h.remove("JERRY")
    console.log(h.toString())
    

    运行结果:

    penny@penny.com
    [ 'penny@penny.com',
      'jerry@jerry.com',
      'jack@jack.com',
      'mike@mike.com' ]
    [ 'penny@penny.com', 'jack@jack.com', 'mike@mike.com' ]
    

    碰撞问题

    散列的碰撞也叫作散列的冲突,是指不同的别名计算出了相同的索引,上面代码中实现的 getIndex() 方法,使用字符串的 charCodeAt() 方法获取所有字符的 ASCII 码,并将这些 ASCII 码值相加,得到一个数值,然后使用这个数值对散列表的长度取余,就得到了元素在散列表中的位置。
    但只进行上述简单的计算,是非常容易产生冲突的,譬如下面的测试代码:

    const h = new HashTable<string>(301);
    h.put("MIKE","mike@mike.com")
    h.put("JACK","jack@jack.com")
    h.put("PENNY","penny@penny.com")
    h.put("JERRY","jerry@jerry.com")
    h.put("ACKJ","ackj@ackj.com")
    console.log(h.toString())
    

    运行结果:

    [ 'penny@penny.com',
      'jerry@jerry.com',
      'ackj@ackj.com',
      'mike@mike.com' ] 
    

    从上面的运行结果中可见,最先插入的 jack@jack.com 被后插入的 ackj@ackj.com 给覆盖了,这是因为在使用 getIndex() 方法对 JACKACKJ 计算索引值时,得到了相同的结果,导致后面插入的元素覆盖了前面的元素。
    要解决这个问题,首先可以优化 getIndex() 方法:

    ...
    getIndex(flag:string):number{
        // 减少散列碰撞,加点盐
        let 
            hash:number = -1,
            pos:number = -1;
    
        if(flag.length){
            hash = 0;
            for(const c of flag){
                hash += (this.hashSalt * hash + c.charCodeAt(0));
            }
            pos = hash % this.dataStoreLen;
        }
        // 存放位置为计算出的 hash 值对数组的长度取余
        return pos;
    }
    ...
    

    再次执行上面的测试代码,运行结果如下:

    [ 'penny@penny.com',
      'mike@mike.com',
      'jerry@jerry.com',
      'jack@jack.com',
      'ackj@ackj.com' ]
    

    通过对 getIndex() 做一些修正,解决了 JACKACKJ 的冲突。
    下面再推荐一个更好的散列函数:djb2 函数。

    ...
    getIndex(flag:string):number{
        let hash:number = 5381;
        for (const c of flag) {
            hash = hash * 33 + c.charCodeAt(0);
        }
        return hash % 1013;
    }
    ...
    

    使用这个函数时,要求散列表的长度大于 1013,因此还需要对类的代码进行一些改进,这里就不再详述了。

    解决碰撞的其他方案

    上面对获取索引值的 getIndex() 方法做了一些修改,以达到解决冲突的目的。除此之外,解决散列冲突还有两个常用的方案:分离链接法(开链法)和线性探测法。将这两个方案和上面 getIndex() 方法进行结合,解决碰撞会更加有效。下面依次介绍这两个方案。

    分离链接法

    分离链接法也叫开链法,是解决散列碰撞的一种常用方法。在前面实现的 HashTable 类中,散列表在每个索引处存放的是一个元素,应用开链法时,散列表在买个索引处存放的不再是一个具体的元素,而是一个列表,碰撞的元素通通存放在这个列表中。以前面 JACKACKJ 的碰撞为例,应用分离链接法时,它们在散列表中的是这样存放的:

    [
      {key:"JACK",value:"jack@jack.com"},
      {key:"ACKJ",value:"ackj@ackj.com"}
    ]
    

    在查找或移除元素时,会遍历这个列表,根据某个 KEY 值,找到相应的元素。
    对于列表,如果想综合应用前面的知识,可以采用 List 或者链表,甚至集合实现都可以。方法有很多,不用只局限于 JavaScript 数组这一种数据结构,可以根据前面介绍的那些数据结构进行灵活组合。我这里就采用 ES6 原生的 Map 集合。同时,为了方便观察效果,我将 getIndex() 修改回原始的定义,在实际应用中,最好将其换成 djb2 函数。下面是经过改进的 IHashTable 接口和 HashTable 类:

    interface IHashTable<T>{
        // 获取索引值
        getIndex(flag:string):number;
        // 两种存放方式:放入值,放入一个值和简称值
        put(flag:string,ele:T):void;
        // 获取值
        get(flag:string):T;
        // 移除元素
        remove(flag:string):T;
        // 获取散列中的元素
        toString():Map<string,T>[];    
    }
    
    class HashTable<T> implements IHashTable<T>{
        // 散列表
        private dataStore:Map<string,T>[] = [];
        // 散列表的长度
        private dataStoreLen:number = 0;
        // 盐值
        private hashSalt:number = 71;
        constructor(dataStoreLen:number,hashSalt?:number){
            // 创建散列对象时传入散列表的长度和盐值
            this.dataStoreLen = dataStoreLen;
            if(hashSalt){
                this.hashSalt = hashSalt;
            }
        }
        getIndex(flag:string):number{
            // let hash:number = 5381;
            // for (const c of flag) {
            //     hash = hash * 33 + c.charCodeAt(0);
            // }
            // return hash % 1013;
            let hash:number = -1,pos:number = -1;
            if(flag.length){
              hash = 0;
              for(const c of flag){
                  hash += c.charCodeAt(0);
              }
              pos = hash % this.dataStoreLen;
            }
            // 存放位置为计算出的 hash 值对数组的长度取余
            return pos;
        }
        put(flag:string,ele:T):void{
            const pos:number = this.getIndex(flag);
            // 初始化操作
            if(this.dataStore[pos] === undefined){
                // 创建一个 Map 对象
                const tmp = new Map<string,T>();
                tmp.set(flag,ele);
                this.dataStore[pos] = tmp;
            }else{
                this.dataStore[pos].set(flag,ele);
            }
        }
        get(flag:string):T{
            const pos:number = this.getIndex(flag);
            return this.dataStore[pos].get(flag);
        }
        remove(flag:string):T{
            const pos:number = this.getIndex(flag);
            const res = this.get(flag);        
            this.dataStore[pos].delete(flag);
            return res;
        }
        toString():Map<string,T>[]{
            const tmp:Map<string,T>[] = [];
            for(const ele of this.dataStore){
                if(ele !== undefined){
                    tmp.push(ele);
                }
            }
            return tmp;
        }
    }
    

    测试一下:

    const h = new HashTable<string>(301);
    h.put("MIKE","mike@mike.com")
    h.put("JACK","jack@jack.com")
    h.put("PENNY","penny@penny.com")
    h.put("JERRY","jerry@jerry.com")
    h.put("ACKJ","ackj@ackj.com")
    console.log(h.toString())
    

    运行结果:

    [ Map { 'PENNY' => 'penny@penny.com' },
      Map { 'JERRY' => 'jerry@jerry.com' },
      Map { 'JACK' => 'jack@jack.com', 'ACKJ' => 'ackj@ackj.com' },
      Map { 'MIKE' => 'mike@mike.com' } ]
    

    线性探测法

    线性探测法是解决散列冲突的另一种常用手段,在使用 put() 方法向散列中插入元素时,如果发现产生了碰撞,就从获取的索引处依次向下查找,直到找到空位,将元素插入。
    下面是使用改进后的 IHashTable 接口和 HashTable 类的实现:

    interface IHashTable<T>{
        // 获取索引值
        getIndex(flag:string):number;
        // 两种存放方式:放入值,放入一个值和简称值
        put(flag:string,ele:T):void;
        // 获取值
        get(flag:string):T;
        // 移除元素
        remove(flag:string):T;
        // 查找元素
        find(flag:string,startPos:number):number;
        // 获取散列中的元素
        toString():{key:string,value:T}[];    
    }
    
    class HashTable<T> implements IHashTable<T>{
        // 散列表
        private dataStore:{key:string,value:T}[] = [];
        // 散列表的长度
        private dataStoreLen:number = 0;
        // 盐值
        private hashSalt:number = 71;
        constructor(dataStoreLen:number,hashSalt?:number){
            // 创建散列对象时传入散列表的长度和盐值
            this.dataStoreLen = dataStoreLen;
            if(hashSalt){
                this.hashSalt = hashSalt;
            }
        }
        getIndex(flag:string):number{
            // let hash:number = 5381;
            // for (const c of flag) {
            //     hash = hash * 33 + c.charCodeAt(0);
            // }
            // return hash % 1013;
            let hash:number = -1,pos:number = -1;
            if(flag.length){
              hash = 0;
              for(const c of flag){
                  hash += c.charCodeAt(0);
              }
              pos = hash % this.dataStoreLen;
            }
            // 存放位置为计算出的 hash 值对数组的长度取余
            return pos;
        }
        find(flag:string,startPos:number):number{
            let len:number = this.dataStoreLen;
            while(len--){
                const tmp = this.dataStore[startPos];
                if(tmp.key === flag){
                    return startPos;
                }
                startPos++;
            }
        }    
        put(flag:string,ele:T):void{
            let pos:number = this.getIndex(flag);
            // 初始化操作
            if(this.dataStore[pos] === undefined){
                const tmp = {key:flag,value:ele};
                this.dataStore[pos] = tmp;
            }else{
                const tmp = {key:flag,value:ele};
                while(this.dataStore[pos] !== undefined){
                    pos++;
                }            
                this.dataStore[pos] = tmp;
            }
        }
        get(flag:string):T{
            const pos:number = this.getIndex(flag);
            const realPos:number = this.find(flag,pos);
            return this.dataStore[realPos].value;
        }
        remove(flag:string):T{
            const pos:number = this.getIndex(flag);
            const realPos:number = this.find(flag,pos);
            const res = this.get(flag);        
            this.dataStore[realPos] = undefined;
            return res;
        }
        toString():{key:string,value:T}[]{
            const tmp:{key:string,value:T}[] = [];
            for(const ele of this.dataStore){
                if(ele !== undefined){
                    tmp.push(ele);
                }
            }
            return tmp;
        }
    }
    

    测试一下:

    const h = new HashTable<string>(301);
    h.put("MIKE","mike@mike.com")
    h.put("JACK","jack@jack.com")
    h.put("ACKJ","ackj@ackj.com")
    h.put("PENNY","penny@penny.com")
    h.put("CKAJ","ckaj@ckaj.com")
    h.put("AKCJ","ackj@ackj.com")
    h.put("JERRY","jerry@jerry.com")
    console.log(h.toString())
    

    运行结果:

    [ { key: 'PENNY', value: 'penny@penny.com' },
      { key: 'JERRY', value: 'jerry@jerry.com' },
      { key: 'JACK', value: 'jack@jack.com' },
      { key: 'ACKJ', value: 'ackj@ackj.com' },
      { key: 'CKAJ', value: 'ckaj@ckaj.com' },
      { key: 'AKCJ', value: 'ackj@ackj.com' },
      { key: 'MIKE', value: 'mike@mike.com' } ]
    

    至此,我们使用线性探测法解决了散列的碰撞问题,使用线性探测法解决碰撞问题时,散列表需要足够的长度。

    总结

    本文介绍了散列这种数据结构,散列是一种非线性的,可以快速查找和插入数据的数据结构,散列的核心是一个计算特征值的方法。在计算特征值时,容易产生冲突,要解决冲突可以从以下三个方面进行:

    • 优化计算特征值的方法,推荐使用 djb2 函数
    • 使用分离链接法,也称作开链法解决冲突
    • 使用线性探测法解决冲突

    将这几个方法进行结合使用,可以达到更好的效果。

    完。

    相关文章

      网友评论

          本文标题:数据结构——散列

          本文链接:https://www.haomeiwen.com/subject/cotmtftx.html