1. 问题
问题同《简单散列函数算法》
设有10个非负整数,用不多于20个的储存单元来存放,如何存放这10个数,使得搜索其中的某一个数时,在储存单元中查找的次数最少?
问题类似于,有10个带号码的球,放到编号为{0, 1, 2, …, 19}共20个盒子中,每个盒子最多放一个,问如何放,使能够用最少的次数打开盒子,知道任一个球所在的盒子编号?
2. 分析
《简单散列函数算法》中,已经分析得出,只要能解决冲突问题,就能将查找时间降为常量范围内。
思路:当一个数发生冲突时,再找一个没有被占用的空盒子来放这个球
哈哈,思路相当简单,好像也很有道理的样子,关键问题是:如何知道这个没占用的空盒子和球号的对应关系?
这里使用《初等数论及其应用》中第5章所介绍的方法,该书中对方法的描述有的地方进行了简略,也没讲如何查找,我这里进行补齐,并写了份python的代码,便于理解和应用
3. 单散列函数解决冲突问题
3.1 方法的思路:
设盒子数量是m, 球的总数是n
当一个数发生冲突时,则看这个冲突的盒子(k)的下一个盒子(k+1)是否是空的,如果是,则放入,如果不是,则继续看下下个(k+2),一直加到m,大于m还没找到,则到{0, 1, …, k}即k前面的盒子中去找空盒子:
如假设有球{0, 1, 30},仍设m = 10,当0和1分别放入对应的0, 1号盒子,当要放入30时,f(30) = 30 % 10 = 0,0号盒子被占用,冲突,再看下一个盒子1号,发现1号也被占用,再看下一个2号盒子,发现是空的,放入即可
当又有一个球为40时呢,类似,会发现0, 1, 2号盒子都被占用,这时需要放入3号盒子
那么这时又来一个真正的3号球呢,我们会发现3号盒子已经被占用,所以只能放到4号盒子中去了
最后{0, 1, 30, 40, 3}放入的情况如下:
盒子1编号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
球号 | 0 | 1 | 30 | 40 | 3 |
注意:这里由于球号的排列顺序不同,放的位置并不一致,如球号如此排列{0, 1, 3, 30, 40},则3号球就会在3号位置了
∵ 而m >= n
∴ 对于任一个x,总可以找到一个空的盒子给其放球
3.2 数学表达式
设 h0(k) ≡ k (mod m), k = 球号, m = 盒子数量,这里”≡ ”表示同余,不是相等,h(k)即为除m的余数
hj(k) ≡ h0(k) + j,0<= j < m, hj(k) 表示发生 j 次冲突后,球所放入的盒子编号
∴ hj+1(k) ≡ h0(k) + (j + 1) ≡ hj(k) + 1
即表示,当在hj(k)的位置发生冲突后,即再查看其下一个盒子是否为空
∵当k = m - 1时, k ≡ 0 (mod m),根据模的算法
∴其下一个位置 k + 1 = 0,即表示回到0号盒子开始查找空盒子
3.3 如何查找球k所在的盒子
方法和放球时一样的,先查找h0(k) ≡ k (mod m), 如果相等,则ok
如果不相等,则说明可能在下一个盒子中,按3.2的公式依次递归,最终会找到对应的盒子
3.4 最坏复杂度
假设有9个球已经占据了{0, 1, 2, …, 8}前8个盒子,最后一个球k9 ≡ k0 (mod m),则需要从第0个位置依次 +1 加到9位置,才能找到不冲突的盒子,也就是说最坏要打开10个盒子才能找到,最坏复杂度 = n,n为球的数量,
哈哈,看起来费了半天劲还不如《简单散列函数算法》中的方法2.2。
但好处也很明显,《简单散列函数算法》中的方法2.2需要先对在第2组盒子中的球号进行排序,如果新增加一个球,就要再排一次
3.5 Python code及测试结果
#mod = m, h(k) = n % m, hj(k) = (h(k) + j) % m def SingleHash(numList): if len(numList) > m: print "num list len is :", len(numList), "is big than mod :", m; return None; hashList = [None] * m; for k in numList: for j in range(m): hj_k = (k + j) % m; if None == hashList[hj_k]: hashList[hj_k] = k; break; else: print "num list is too big, hash list is overflow"; return None; return hashList; def CheckSingleHash(numList, hashList): if None == hashList: return; for k in numList: for j in range(m): hj_k = (k + j) % m; #check the key is equal with the one in hash list, if not check the next one if hashList[hj_k] == k: if j > 0: print k, "find", j+1, "times"; break; else: print k, "conflict with", hashList[hj_k]; else: print k, "is not find..."; return False; return True;
测试时,设置了m = 19
测试数列为: numList = [0, 1, 2, 7, 9, 15, 19, 20, 77, 38],为了测试冲突,故意设置了一些冲突数,为了减少无用的输出,对于没有冲突的就不打出了,测试结果如下:
可以看出,38由于多次冲突,需要查找7次才能找到
4. 散列算法冲突问题
不论是《简单散列函数算法》中的散列算法还是单散列算法,如果没有冲突的情况下,都只要一次就能找到球所在的盒子,所以如果算法冲突的概率低,那么平均的时间复杂度也是很是越来越接近常量的。
4.1 简单散列算法冲突的概率
《简单散列函数算法》中的散列算法只要一个球k模m(k%m)已经在盒子中了,就一定会产生冲突,设k已在第一组盒子中,则对所有的f(x) = k + im, (i ∈ {0, 1, 2, ...}),都会产生冲突,冲突的概率是很高的。
设最大的球号为s, 则共有(s/m)个满足f(x) = k + im的球号(当s很大时,可以忽略除不尽的部分)
则简单散列函数算法中第一个为满足f(x) = k + im的球的概率 :
设共有n个球,恰有2个球满足f(x)时,第2个球的概率:,当s很大时,1可以忽略,则约为 (n-1)/m
故刚好取2个球满足f(x)时的概率 = (n2-1)/m2
类似的,恰有3个球满足f(x)时的概率 = n(n-1)(n-2) / m3
可以看出,当n和m接近时,冲突的概率越来越接近100%,所以一定要使m > n,且大得越多越好
回到我们问题中的设定,n = 10, m = 20, 则刚好2个球满足时,冲突的概率 = 25%
刚好有3个球满足时,冲突的概率 = 9%
总的冲突概率 > 34%,冲突概率是非常高的
4.2 单散列算法冲突概率
单散列算法中,由于第一次产生冲突时,设为hj(k),第2次产生冲突时则必须有一个hj(k) + 1 的球已经在盒子中,所以发生2次以上冲突的概率会有所降低,当然,这种情况下是需要查看2个盒子的。
由4.1知,同时取出2个都满足f(x)时的概率 = (n2-1)/m2,则第3个球必须是f(x)或f(x)+1才会有冲突
第3个球是f(x)的概率 = (n-2)/m
第3个球是f(x) + 1的概率 = (n-2)/m,
故发生冲突的概率 = 2n(n-1)(n-2) / m3
其他大于3个球冲突的概率就不予计算比较了,单用此和简单散列算法恰有2个球的冲突比较:
当n = 10, m = 20, 单散列算法冲突的概率 = 18%
可以看出,单散列算法在此情况下冲突概率上是优于简单散列算法的。