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

1,617 阅读5分钟

简介

由于顺序表的物理地址是连续的,因此查询对应索引的数据时,效率非常高,即:利用偏移量一次访问到实际数据

散列表(哈希表)原理:创建顺序表保存数据,通过一系列散列函数(又称哈希算法),实现关键字转化为顺序表索引

注意

散列函数无论怎样设计,总会有相同的元素映射到一个位置,因此会产生冲突,需要冲突解决方案

顺序表其大小一般比实际数据要大,实际数据占用区间默认为0~1之间(一般占用0.6~0.9较佳,太少了空间浪费,多了容易冲突降低查询速度)

因此,可以看出: 散列函数由顺序表、散列函数、冲突解决方案组成

散列函数

散列储存实现了关键字到地址的转化,其构成的散列的线性表成为散列表或者哈希表

散列函数的常用设计方法:数字分析法、除留余数法(取余法)、平方取中法、折叠法

也可以自行设计出更加优秀的散列函数

数字分析法

根据关键字特征,来设计散列函数,灵活性非常强,比较常见

案例1:

给定关键字:12121201、12121203、12121243、12121244、12121246、12121263、12121290

可以看出关键字前面6位都是一样的,而后两位才是决定其不同的关键,则可以取后两位作为索引

由于数组基本集合在0~100之间,顺序表数组长度设置为100

散列函数取后两位即可

long hashNum(long num) {
    return num % 1000000;
}

映射索引: 01、03、43、44、46、63、90

取值过程:

假如取值12121243,使用散列函数hashNum生成索引43,并对比索引43的值是不是当前值,如果是,则查找成功

案例2:

给定关键字:1212120112、1212123232、1212124345、1212124497、1212124635、1212126379、1212129045

可以看出关键字前面6位都是一样的,而后4位是不同之处,可是数字又比较少,创建10000个空间着实浪费,因此可以通过散列函数将其映射为2位长度

由于索引两位长度,数组基本集合在0~100之间,顺序表数组长度仍设置为100

而散列函数、设计则可以通过后面数字两两相加,生成新的数,取最后两位

long hashNum(long num) {
     num %= 1000000;
     return (num / 100 + num % 100) % 100;
}

映射索引: 13、64、88、41、81、22、35

因此数字分析法的精髓:根据数据的关键字位置、长度进行散列函数的设计和优化

取余法

通过取余数来使其分布均匀,选择合适的除数p更为重要,一般取质数(n < p < m,其中n为要储存元素个数,m为散列表长度), 比较常见

案例:

给定关键字:13838、4847474、1343535、233555、233425、663443、6654545、74465745

用肉眼分辨不出其数字特征,相对较乱,使用取余法来处理

设定顺序表长度为20(m),总共有8(n)个数, 除数p(20),取余作为索引即可

散列函数直选定除数,直接取余即可

int hashNum4(int num) {
    return num % 20; //除数p介于n和m之间,一般为顺序表长度m
}

平方取中法

对数值平方,取中间几位的方法,适合单个位数取值不均匀的情况(数字重复率高,例如:11010101),比较常见

案例:

给定关键字:444, 323, 333, 111, 666, 555, 533, 222

设定顺序长度为10(m),总共有8(n)个数,则平方结果取中间一位作为索引

散列函数掐头去尾,直接取中间数字即可

int hashNum5(int num) {
    return num * num % 100 / 100; //当前为三位数一下相乘,则范围为 0 ~ 1000000
}

折叠法

对关键字从左往右按一定长度进行拆分成几组,然后相加取等长位数的关键字,适用于储存数字较长的情况。

例如:1234343535 哈希表长度1000 则分为 123 434 353 5 然后相加取三位数即可:即储存的关键字为915

散列函数如下所示:

int hashNum6(long num) {
    int res = 0;
    while (num > 0) {
        res += num % 1000;
        num /= 1000;
    }
    return res % 1000;
}

冲突解决方案

从上面生成哈希表的过程可以看出,或者猜到,无论是否存在重复数字,在某种情况下一定会出现关键字映射到同一个索引的情况,这种情况称为冲突,因此引出冲突解决方案

解决方案分为两种:开放地址法、链地址法

开放地址法

开放地址法针对的是只有一个顺序表数组存放内容的情况,所有的元素(包括冲突元素)都会存放到该顺序表中,元素冲突,则平行后移,找到空位处存放

即:开发地址法为了解决冲突,可能会出现哈希值对应的索引位置,存放的不是对应的哈希值元素

开放地址法解决冲突方案又分为两种:线性探测法、双散列函数探测法

下面通过线性探测法、链地址法介绍开发地址法

1.线性探测法

当哈希值对应地址有值后,则冲突,每次向后移动1位,直到找到空位,将顺序表看成逻辑上的环形,如果找一圈回到当前节点,则存满,结束

如下图演示了线性探测法的冲突处理步骤

//线性探测法
int crashLineHandle(int *hashList, int index, int length, int data) {
    int lastIndex = index;
    do {
        index++;
        index = index % length;
        int tem = hashList[index];
        if (tem) continue;
        else return index;
    } while (lastIndex == index);
    return -1;
}

2.双散列函数探测法

与线性探测法类似,当哈希值对应地址有值后,则冲突,每次向后移动n位,直到找到空位,将顺序表看成逻辑上的环形,如果找一圈回到当前节点,则存满,结束(其中n为素数较佳,且表长不能为n的倍数)

如果图所示,演示双散列函数探测法的处理步骤,这里探测间隔为3

//双散列函数探测法
int crashDoubleLineHandle(int *hashList, int index, int length, int data) {
    int lastIndex = index;
    do {
        index += 11; //与现行探测法不同的是,设置一定间隔,和总长度length互为素数,能减少探测间隔,例如11
        index = index % length;
        int tem = hashList[index];
        if (tem) continue;
        else return index;
    } while (lastIndex == index);
    return -1;
}

链地址法

与开放地址法不同,链地址法只会在顺序表中哈希值对应索引处存放放数据,一旦哈希值重复,则该位置会转化为一个纵向的链表数组,纵向扩充重复哈希值的数据

即:链地址法哈希值对应索引处,只会存在索引对应哈希值的数据,其内容存放在纵向链表中

如下图所示表示链地址法,此方法较为常用