哈希表的产生就是为了查找而诞生的,是查找的时间极大的减少。
对于有海量数据要去搜索应该选用哈希表,虽然搜素树的速度也很快,但是哈希表稳定发挥算法复杂度可以达到o(1).
哈希表的缺点:表越满,性能越差,所以是典型的空间换时间。
一.朴素算法
首先我们先来讲一下朴素查找,比如查找值key为3,我们就要通过遍历整个数组来寻找数组是否有我们的Key,假设一共有N个元素,那么它的时间复杂度为O(n)。
如果遍历到了Key值则返回1,如果没有查找到则返回0.
以上就是朴素算法的查找思路。
二.哈希算法
1.Hash table的定义
哈希表就是根据Key关键值,作为自变量通过H(key)哈希函数对应到数组地址,这个数组就叫做散列表(Hash table)。
2.Hash table的特例
key的关键值的范围为0—99,那我们就可以创建一个table[100]数组,哈希函数H(x) = x 。
a数组为Key数组,n为a数组的个数。
void create_hash(int a[],int n,int table[])
{
int i; //使用i遍历数组a中的全部n个元素,数组a为key数组
for(i = 0;i<n;i++)
{
table[a[i]]++ //用Table的下标记录a[i]出现的次数
}
}
打印哈希表结果
查找哈希表中是否存在Key值
int find_key(int table[],int key)
{
return table[key]!=0;
//如果table[key]不是0返回1,否则返回0;
}
此时查找Key是否存在的时间复杂度为O(1);
看到这里小伙伴们是不是发现了就好似桶排序啊,其实桶排序的思想就是特殊的Hash Table,这种排序的算法复杂度为O(n),优于一般的log(n);
接下来我们思考一下,假如数组a中的数据范围不是0—99而是2的31次方,更或者不是int类型,而是浮点数,字符串,甚至是数组,对象等等更复杂的元素我们该怎么去处理呢?
三.哈希函数
为了解决上面的问题,我们就要引入哈希函数了。我们将带存储的数据转化为哈希表长范围内的整数,然后再使用数组下标进行访问。
1.整数数据,可以直接取余哈希表长,得到对应的哈希值
int int_func(int key)
{
return key%MAX_table_len;
}
2.字符串,可以遍历字符串,把当中的字符他们的ASCII码值加起来,再取余表长得到哈希值
int string_func(const char *key)
{
int sum = 0;
while(*key) //遍历字符串中的字符
{
sum += *key; //将它们的ASCII码相加得到整数
key++;
}
return sum % MAX_table_len; //转化为整数后再取余表长
}
例如:abc的ACSII码相加为94,则取余表长为94,存入table下标为94,则代表了
这时候问题又来了,abc和cba会映射到table[94],3和103都会映射到table[3],等等 这时候就造成了哈希冲突,冲突时就会导致查询出现问题。
四.哈希冲突
首先呢我们在构造哈希函数时应遵循,哈希函数数值必须在散列地址的范围内分布均匀,尽量减少地址冲突。
接下来我们介绍两种处理哈希冲突的方式
1.开放定址法
开放地址法的核心就是方发生冲突时再次进行散列,寻找下一个地址。在寻找下一个地址时我们再次构造一个哈希函数,称为双哈希法。
设置第二个哈希的函数,例如:hash2(key) = R-(key mod R)
R要取比数组尺寸小的质数。
例如 R=7: hash(关键字) = 7-(关键字%7)
也就是说,二次哈希的结果在1-7之间,不会等于0;
如果遇到冲突,新位置 = 原始位置 + i*hash2(关键字)
例如:
现在哈希表的数组大小为13,第一个关键字为15,15 mod 13 == 2 , 所以存储在哈希表数组下标为2的地方,第二个关键字为2, 2 mod 13 == 2 , 所以也应该存储在哈希表数组下标为2的地方,但是此时发生了冲突,所以应该进行二次哈希处理。
设第二次哈希函数的R为7,那么
7 - (2 mod 7)== 5 新位置 = 原始位置(2) + i * hash2( 5 ) = 7; (i=1)
若7此时也有存储则i=2,依次类推。
同理,大家可以自己尝试18和28,对应存储位置为5,9。
2.链地址法
把哈希表的数值全部变为指针数组,每一个指针指向一个单链表,这样一个数字就可以存储多个相同哈希值的Key。
以上呢我们解决了哈希冲突,可是新的问题又来了,在第一种情况下如果哈希表满了该怎么办?
经过重新计算后
以上就是对哈希表概念的讲解,代码实现部分请看Up的哈希表C语言实现大全