简介
由于顺序表的物理地址是连续的,因此查询对应索引的数据时,效率非常高,即:利用偏移量一次访问到实际数据
散列表(哈希表)原理:创建顺序表保存数据,通过一系列散列函数(又称哈希算法),实现关键字转化为顺序表索引
注意
散列函数无论怎样设计,总会有相同的元素映射到一个位置,因此会产生冲突,需要冲突解决方案
顺序表其大小一般比实际数据要大,实际数据占用区间默认为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;
}
链地址法
与开放地址法不同,链地址法只会在顺序表中哈希值对应索引处存放放数据,一旦哈希值重复,则该位置会转化为一个纵向的链表数组,纵向扩充重复哈希值的数据
即:链地址法哈希值对应索引处,只会存在索引对应哈希值的数据,其内容存放在纵向链表中
如下图所示表示链地址法,此方法较为常用