首页 > 代码库 > 算法导论 第11章 散列表
算法导论 第11章 散列表
散列表是主要支持动态集合的插入、搜索和删除等操作,其查找元素的时间在最坏情况下是O(n),但是在是实际情况中,散列表查找的期望是时间是O(1),散列表是普通数组的推广,因为可以通过元素的关键字对数组进行直接定位,所以能够在O(1)时间内访问数组的任意元素。
1、直接寻址表
当关键字的全域较小,即所有可能的关键字的量比较小时,可以建立一个数组,为所有可能的关键字都预留一个空间,这样就可以很快的根据关键字直接找到对应元素,这就是直接寻址表,在直接寻址表的查找、插入和删除操作都是O(1)的时间。、
2、散列表
在关键字的全域很大,可能的关键字很多的时候,要建立一个为所有可能的关键字都预留空间的数组是不现实的,特别是当实际要保存的关键字相比全域来说很少的时候就存在大量的空间浪费。这个时候就适合用散列表,其需要的存储空间要比直接寻址表小很多。虽然空间少了,但是散列表也可以保证在平均情况下元素的查找时间为O(1)。
在直接寻址表中,各个关键字就是数组中槽的序号,直接一一对应;但是在散列表中,最重要的就是散列函数,因为在散列表中,表空间只有实际会保存的元素的数量,这样散列函数根据各个元素的关键字来计算出它应该存放在散列表中的位置,这也就产生了一个问题,两个不同的关键字通过散列函数计算出的值可能相同,一个位置不可能存储两个元素,这时就必须解决这种碰撞问题。
解决碰撞问题有两方面的思路:一个就是设计一个好的散列函数,让尽可能少的关键字产生碰撞。另一个就是在实际的表空间比所有的可能的关键字少,或者甚至比实际要保存的关键字的个数都少,这时候必然出现碰撞,我们就必须设计一个方法处理出现的碰撞:链接法和开放寻址法。
2.1、散列函数
一个好的散列函数必须有很好的随机性,能够将每个关键字都等可能的散列到m(散列表大小)个表槽位的任何一个位置上,并与其他的关键字已被散列到哪一个槽位无关。所以散列函数需要独立于数据中可能存在的任何模式的方式导出散列值。下面列出一些常用的散列方法。
因为散列通常是将关键字放入散列函数中进行计算得到散列值,所以通常要将关键字转换成自然数,如果关键字是字符串,则可以将字符串按照适当的基数记号表示成整数。
除法散列
散列函数:h(k) = k mode m,m取与2的整数幂不太接近的质数较好,不宜取2的幂。
乘法散列
散列函数:h(k) = m*(k*A mod 1),其中0<A<1,m乘以 kA 的小数部分从而确定位置,A取黄金比例是个比较理想的值。
全域散列
散列函数是从一个特定设计的散列函数集合中随机的选择一个散列函数进行散列,这样即使是同一个关键字每一次散列的结果都不一样,这样保证了随机性,是散列表的平均性能比较好。
2.2、链接法
当使用散列表遇到冲突碰撞的时候,可以把散列到同一槽中的所有元素都放到一个链表中,这个槽就存放链表的头指针。使用链接法的散列表,在散列表的槽数与元素数量成正比关系的时候,每个槽中的链表的平均长度为O(1),因此在平均情况下,散列表查找、插入和删除的时间复杂度均为O(1)。
2.3、开放寻址法
不同于链接法,开放寻址法不适用链表,而是将所有的元素都存储于散列表的槽中,这还可以省去保存链表指针的存储空间,将这些空间来保存元素。在开放寻址法中,如果一个元素的关键字散列到的位置已经有元素了,则说明出现了碰撞,这时候散列函数会继续将元素散列到下一个位置,如果继续冲突,则继续探查,一次类推,直到找打空的槽来保存元素,因此对于每一个插入的元素,散列函数都会产生一个探查序列,依次探查各个位置寻找空槽,最多探查m(槽数)次。所以这里的散列函数不同于链接法的散列函数只是产生一个特定的散列值,开放寻址法中的散列函数都是产生一个探查序列。常用的方法有:线性探查、二次探查和双重探查。
线性探查
散列函数:h(k, i) = (h‘ (k) + i) mod m,i = 0, 1, 2, ... , m - 1,其中h‘(k)为辅助散列函数。
在线性探查过程中,每一次探查的偏移量为1,一旦h‘(k)确定则整个探查序列也就确定了,所以一共可以产生m中探查序列。此方法容易产生一次集群,因为探查序列式偏移量为1的连续空间,所以被占用的槽越多,碰撞的几率也会越大,查询时间也会增加。
二次探查
散列函数:h(k, i) = (h‘(k) + c1*i^2 + c2*i) mod m,其中h‘是辅助散列函数,c1和c2为辅助常数。
在二次探查中,探查序列的偏移量不是1,而是不断变化的(是一个确定的值序列),但是同线性探查一样,只要h‘(k)确定了,整个探查序列也就确定了,所以一共只有m种探查序列。
双重探查
散列函数:h(k, i) = (h1(k) + i*h2(k)) mod m,其中h1和h2是辅助散列函数
在双重探查中,探查序列的偏移量是不断变化的,而且每一个k值对应的偏移量变化序列都不一样,队以同一个h1(k)的值,可能对应m种不同的偏移量h2(k),所以共能产生m^2个探查序列。所以它是开放寻址法中最好的方法之一。但是为了能查找整个散列表,值h2(k)必须要与表的大小m互质,否则h2的值和m存在公约数,则探查序列的长度就小于m了。
开放寻址法在平均情况下,每个元素的探查次数都是O(1)。所以整体性能也就好。
2.4、完全散列
散列技术因其出色的期望性能而广泛使用,当关键之的集合是静态的时候,也就是关键字存入表中关键字集合就不在变化了,那么就可以使用完全散列,其最坏情况下搜索的时间复杂度能达到O(1),完全散列其实就是两级散列,第一级仍然是建立一个散列表,将n个关键字散列到m个槽中,第二级散列就是出现碰撞的时候不适用链接法和开放寻址法,而是在对应每个槽又建立一个散列表,将冲突的元素散列到同一个表中,并保证第二级散列不出现碰撞,为了保证这一点,第二级散列表的大小应为散列到该槽的元素个数的平方,看似需求很大空间,实际上总体期望空间仍然是O(n)。
算法导论 第11章 散列表