首页 > 代码库 > 散列函数之单散列算法解决冲突问题

散列函数之单散列算法解决冲突问题

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%

可以看出,单散列算法在此情况下冲突概率上是优于简单散列算法的。

散列函数之单散列算法解决冲突问题