数据结构与算法-Day17-哈希(散列)表

1,120 阅读6分钟

前面说过的静态查找表以及动态查找表中的一些查找方法,在查找的过程中都不可避免的会遇到同查找表中的数据进行比较,查找算法的效率取决于同表中数据的查找次数。

哈希表可以通过关键字Key以及一个函数函数f(key)来获取对应数据的存储位置,大大降低了比较的次数,是一种相对来说比较高效的查找算法。

哈希

哈希是将记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个对应关系找到给定值key的映射f(key)。若查找表中存在这个记录,那么必定在f(key)的位置上。

哈希函数的构建

常见的构造哈希函数的方法有以下几种:

折叠法

将关键字从左到右分割成位数相等的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和,并按散列表的表长,取最后几位作为散列地址。

上图中记录9876543210的散列地址为962

除留余数法

若已知整个哈希表的最大长度m,可以取一个不大于m的数p,然后对关键字key做取余运算。

f(key) = key % p (p<=m) 以下图为例:

散列表长度为7,令p=7,对各个原始数据进行取余操作,最终得到新的存储方式。

在此方法中,由经验得中p可以为不大于m的质数或者不包含小于20的质因子的合数。

直接定址法

直接定址法的哈希函数为一次函数,如下所示:

f(key) = a * key + b (a,b为常数)

数字分析法

如果关键字由多位字符或者数字组成,就可以考虑抽取其中的 2 位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。

上图中灰色区域的字符是一样的,而后三位数据有较大的差异性,可以选择以后三位数字作为哈希地址。

平方取中法

对关键字做平方操作,取中间几位作为哈希地址

421² = 177241
423² = 178929
436² = 190096

对于关键字序列{421,423,436},对各个关键字进行平方操作,得到{177241,178929,190096},取中间的两位数{72,89,00},作为哈希地址。

随机数法

是取关键字的一个随机函数值作为它的哈希地址,即:H(key)=random(key),此方法适用于关键字长度不等的情况。

注意:这里的随机函数其实是伪随机函数,随机函数是即使每次给定的 key 相同,但是 H(key)都是不同;而伪随机函数正好相反,每个 key 都对应的是固定的 H(key)。

构造哈希函数需要考虑的因素:

  • 关键字的长度。如果长度不等,就选用随机数法。如果关键字位数较多,就选用折叠法或者数字分析法;反之如果位数较短,可以考虑平方取中法;
  • 哈希表的大小。如果大小已知,可以选用除留余数法;
  • 关键字的分布情况;
  • 查找表的查找频率;
  • 计算哈希函数所需的时间

处理哈希冲突

哈希冲突指的是不同的关键字key的哈希值f(key)一样的情况。哈希冲突是无法避免的,需要采取适当的措施去处理。 例如长度为11的哈希表中已经填好17、60、29这三个数据。对应的哈希函数为f(key)=key Mod 11,现在第4个数据为38,通过哈希函数求得的哈希地址为5,与60冲突,我们可以通过以下几种方法来分析对应的解决方案。

开放定址法

f(key) = (f(key)+d) Mod m; m为哈希表的表长,di为一个增量

通常来说有三种方法可以来获取d值,分别是

  • 线性探测法
d = 1,2,3...,m-1

根据公式
f(38) = (f(38)+1)%11 = (5+1)%11 = 6;
发现6的位置已经有了17,冲突,继续
f(38) = (f(38)+2)%11 = (5+2)%11 = 7;
发现7的位置已经有了29,冲突,继续
f(38) = (f(38)+3)%11 = (5+3)%11 = 8;
找到合适的位置,放入38

  • 二次探测法
d = 1²,-1²,2²,-2²...q²,-q²;

根据公式
f(38) = (f(38)+1)%11 = (5+1)%11 = 6;
发现6的位置已经有了17,冲突,继续
f(38) = (f(38)-1)%11 = (5-1)%11 = 4;
找到合适的位置,放入38

  • 伪随机数法

伪随机探测,每次加上一个随机数,直到探测到空闲位置结束。

链地址法

将所有产生冲突的关键字所对应的数据全部存储在同一个线性链表中。
例如有一组关键字为{19,14,23,01,68,20,84,27,55,11,10,79},其哈希函数为f(key)=key%13,使用链地址法构建的哈希表如下图所示:

公共溢出法

建立两张表,一张为基本表,另一张为溢出表。基本表存储没有发生冲突的数据,当关键字由哈希函数生成的哈希地址产生冲突时,就将数据填入溢出表。

typedef struct
{
    //数据元素存储基址,动态分配数组
    int *elem;
    //当前数据元素个数
    int count;
}HashTable;

//1.初始化散列表
Status InitHashTable(HashTable *H)
{
    int i;
    //① 设置H.count初始值; 并且开辟m个空间
    int m=HASHSIZE;
    H->count=m;
    H->elem=(int *)malloc(m*sizeof(int));
    
    //② 为H.elem[i] 动态数组中的数据置空(-32768)
    for(i=0;i<m;i++)
        H->elem[i]=NULLKEY;
    
    return OK;
}

//2. 散列函数
int Hash(int key)
{
    //除留余数法
    return key % HASHSIZE;
}

//3. 插入关键字进散列表
void InsertHash(HashTable *H,int key)
{
    //① 求散列地址
    int addr = Hash(key);
    //② 如果不为空,则冲突
    while (H->elem[addr] != NULLKEY)
    {
        //开放定址法的线性探测
        addr = (addr+1) % HASHSIZE;
        //如果addr又变成了第一次的hash值,说明表已经满了
        if(addr == Hash(key)) {
            return;
        }
    }
    //③ 直到有空位后插入关键字
    H->elem[addr] = key;
}

//4. 散列表查找关键字
Status SearchHash(HashTable H,int key,int *addr)
{
    //① 求散列地址
    *addr = Hash(key);
    
    //② 如果不为空,则冲突
    while(H.elem[*addr] != key)
    {
        //③ 开放定址法的线性探测
        *addr = (*addr+1) % HASHSIZE;
        
        //④H.elem[*addr] 等于初始值或者循环又回到了原点.则表示关键字不存在;
        if (H.elem[*addr] == NULLKEY || *addr == Hash(key))
            //则说明关键字不存在
            return UNSUCCESS;
    }
    return SUCCESS;
}