散列函数与字典
散列又称为哈希(hash)它本身就是一个整数值,在不同的数制系统下有不同的表示,例如在16进制下的表示。但散列的整数值具有特殊的意义,它对应于一段字符串。一段字符串在给定的散列函数下生成确定的散列值,我们可以用以下的方式表示:
1 | address = Hash(string) |
我们认为address和string有对应关系,根据address我们可以快速确认string的值。
根据上述原理,可是实现符号表数据结构,这种实现不同于树类的实现。它可以在O(1)的时间复杂度下找到给定key下的value。而是类实现的字典,由于有一个查找过程,需要O(lgN)的时间复杂度。当然,这两种实现的性能比较是相对的。使用散列实现的符号表的性能和散列函数的质量关系很大,这个质量包括计算复杂度、随机程度。
接下来先讲散列函数的实现然后是符号表实现及其遇到的种种问题,然后对比散列实现的符号表和树类结构实现的符号表的差异。
散列函数
散列函数有很好的通用性,根据不同的输入都能输出散列后的值,例如输入字符串、数值、或者字符串和数值的混合。通常来说,高效的散列满足一下三个性质:
- 一致性 相同的键必然会产生相等的散列值
- 高效性 根据输入,快速计算出散列值
- 均匀性 计算结果应该在一定范围内均匀分布
一致性并不能保证,不同的键会有不同的散列结果。不同的键生产相同的结果我们称为冲突。在有限的空间内均匀容纳无限组合可能的key必然或产生冲突,这时我们需要解决冲突的方式,后面会详细。
整数值散列函数
- 直接定址
1 | Hash(key) = a*key + b |
该方法计算简单、不会产生冲突,适用于key很小的情况,如果key很大,那么hash空间就非常大了,这并不实际。
- 除留余数
在大小为m的Hash空间中
1 | Hash(key) = key%k k<= m |
k最后取接近m的质数,使用质数是确保key不能被整除,从而产生随机的余数。
结合方法1,我们可以设计如下的散列函数
1 | Hash(key) = (a*key + b) % k k<=m |
- 折叠法
对于这个整数,我们可以把它均匀分为两部分或多部分,然后对它们求和。例如:
1 | key = 123456 |
为了避免过大的散列空间,我们可以把产生进位的最高位除掉。
- 乘余取整数法
浮点数散列函数
对于浮点数,我们可以对其乘与一个大数M取下限,然后转化为整数值散列函数。
例如
1 | Hash(key) = (M*key + b) % k k<=m |