散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
简要的概括一下哈希表table hash_function(key){}
散列表
- key 唯一标识,类比SQL表内主键
- hash function是计算出value
- 通过value找到table的对应值
散列函数
散列函数,顾名思义,它是一个函数。
我们可以把它定义成hash(key),其中 key 表示元素的键值,hash(key) 的值表示经过散列函数计算得到的散列值。
三点散列函数设计的基本要求:
- 散列函数计算得到的散列值是一个非负整数;
- 如果 key1 = key2,那 hash(key1) == hash(key2);
- 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
所以完美的散列表其实是一个数组+key
即便像业界著名的MD5、SHA、CRC等哈希算法,也无法完全避免这种散列冲突。
而且,因为数组的存储空间有限,也会加大散列冲突的概率。
散列冲突
常用的散列冲突解决方法有两类
- 开放寻址法(open addressing)
- 链表法(chaining)
开放寻址法
-
线性探测(Linear Probing)
- 在散列表内存入数据时,如果遇到了散列值相同时候,向下遍历搜索空位,然后插入(电梯算法提高效率?)。
- 寻值的时候就是将目标值和计算出来的散列值对应的值相同,否则遍历找到最后跳出。
-
二次探测(Quadratic probing)
所谓二次探测,跟线性探测很像,线性探测的下标序列就是 ,,……而二次探测的下标序列就是 ,,……
- 双重散列(Double hashing)
使用一组散列函数,如果第一个散列函数计算出来没有空位就用第二个散列函数,以此类推......
- 用装载因子(load factor)来表示空位多少
散列表的装载因子 = 填入表中的元素个数 / 散列表的长度
- 所以这可以用来衡量是否需要扩容啊,快要满的时候就可以扩容了。
链表法
链表法是一种更加常用的散列冲突解决办法,相比开放寻址法,它要简单很多。在散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
- 我的理解
- 其实就是扩容散列值对应的散列表的那行的解决办法
- 用链表的方式可以存更多的数据在这行(散列值对应的)
- 插入散列表的时候就要找到散列值对应的槽往链表的最后面插入
- 查找和删除就只要找到散列值对应的槽然后遍历
- 我的联想
- 那么既然可以用链表那也可以用其他链式结构的数据结构
- 那么可以用顺序结构吗?这样还能随机访问
设计散列函数
首先,散列函数的设计不能太复杂。过于复杂的散列函数,势必会消耗很多计算时间,也就间接的影响到散列表的性能。其次,散列函数生成的值要尽可能随机并且均匀分布,这样才能避免或者最小化散列冲突,而且即便出现冲突,散列到每个槽里的数据也会比较平均,不会出现某个槽内数据特别多的情况。
实际工作中,我们还需要综合考虑各种因素。这些因素有关键字的长度、特点、分布、还有散列表的大小等。散列函数各式各样,我举几个常用的、简单的散列函数的设计方法,让你有个直观的感受。
实际上,散列函数的设计方法还有很多,比如直接寻址法、平方取中法、折叠法、随机数法等,这些你只要了解就行了,不需要全都掌握。
(我感觉这些没有应用场景,过于苍白)
to be continued
2019年3月8日 20点28分