散列相关

来源:互联网 发布:linux添加路由 编辑:程序博客网 时间:2024/05/16 06:54

1. 基本概念

散列表依靠散列函数,将每个key映射到0~TableSize-1范围的某个数,将对应的value放入到该数索引的位置上。没有冲突的理想散列表是不存在的,因此设计散列表时面临的关键问题是如何设计散列函数、如何解决冲突。

2. 散列函数

为保证散列表具有较好的性质,通常要求表的大小是素数(参考函数nextPrime()),这样散列函数才有机会将key均匀分散。

对于整数来说:一般合理的方法是直接返回“Key mod TableSize”。

对于字符串来说:有以下的分析。

两个naive的散列函数:

1)字符ASC码相加,并直接对TableSize取余。如果表很大,则不能很好的分配。例如键值为8位的string,则key值最大只能为127*8=1016;

2)只考虑前三个字符,key[0]+27*key[1]+27*27key[2]。理论上有17576中组合,但实际上字典中只有2851种组合;

一种较好的散列函数是:

3)。在编程实现时可以借助Horner法则展开,此散列函数允许溢出(从而引入负数),但必须在尾部加以测试,如下:

/* * 定义string类型hash函数的5步操作(通用) */int hash(const string &key, int tableSize){//step1:初始化哈希返回值int hashValue = 0;//step2:计算哈希函数,得到key的哈希值(horner法则)for(int i = 0; i < key.length(); i++)hashValue = hashValue * 37 + key[i];//step3:对tableSize取余(也是当key为整数时采用的方法)hashValue %= tableSize;//step4:测试正负性,防止step2的hashValue溢出导致负数if(hashValue < 0)hashValue += tableSize;//step5:返回最终的哈希值return hashValue;}
当key过长时,为了减少计算复杂度,可以采取一些截断措施,比如只计算前几位,或者计算基数位置上的字符等等。

3. 冲突检测和消除

决定散列表性能的并不是散列表大,而是装填因子(元素个数与散列表大小的比值)。

3.1 分离链接法(separate chaining)

(在opnet实现DTN bundle保管时用过这种方法)通常要求装填因子接近于1

思想:将散列到同一个值的元素保留到一个链表中。

实现:通常可采用vector<list<HashedObj> >结构实现,不过由于标准库list用的是双链表循环结构,如果空间占用是制约因素的话,可以自己实现;如果要支持重复元素的插入,则需要在该元素上定义一个额外的数据成员表征数量。

缺点:给新链表单元分配地址空间需要花费时间,会降低算法的速度。

分析:一次不成功的查找需要访问的节点数平均为;成功的查找需要遍历大约1+/2个链。

3.2 开放定址法

通常要求装填因子 < 0.5(为了1.保证线性时间界;2.保证能成功插入),否则需要rehash()

思想:解决冲突的另一个办法是当冲突发生时选择另一个单元进行判断,直到出现空单元。即执行,直到找到空单元,其中,f()是解决冲突的函数。这样的表也叫探测散列表(Probing hash tables)。

特点: 此种方法使用的tableSize比分离链接法要大(因为将冲突的元素分配到其他cell中);通常来说,此类方法的装填因子要低于0.5。

实现:

1)线性探测:采取的方式,容易产生一次聚集(primary clustering),产生一些滚雪球的堆块,导致散列到区块中的任何键值都需要多次试选单元才能解决冲突。一种解决方法是采用随机冲突解决方法(怎么查找?),即使得每次探测都与前次探测无关。可以看到,如果装填因子大于0.5时,线性探测不是一个好办法。

2)平方探测:采用的方式,问题在于,一旦装填因子大于0.5,就不能保证总会找到空的单元(如果表大小不是素数,那小于0.5时也可能找不到空单元)。平方探测会引起二次聚集(secondary clustering)效应,但其影响远小于一次聚集,可使用双散列的方法消除二次聚集,但同时也带来了额外的计算开销。

3)双散列(double hashing):一种流行的方式是,第二个散列函数的选择至关重要,一种选择是,R是小于TableSize的素数。实验证明,双散列预期探测次数几乎和随机冲突探测的相同,具有较好的表现。但由于需要两个散列函数,计算很耗时(尤其在string类型的key下)。所以实际中通常采用更加简便快捷的二次探测。

分析:在探测散列表中不能执行标准的删除操作(否则依赖当前cell的后续冲突元素将无法删除),需要采用懒惰删除,即在元素上附加数据成员表征其状态(ACTIVE/EMPTY/DELETED)。

4. 再散列(reHash)

使用平方探测的开放定址法时,如果表中元素太多,那么操作的时间会增加,而且插入操作有可能失败,此时需要对哈希表的大小进行扩张(两倍),使用一个新散列函数扫描原来的散列表,计算每个元素的新散列值。

再散列通常有多种实现方式:1)当表满到一半时;2)遇到插入失败时;3)当表到达一个装填因子时(途中策略)。通常选取一个好的截止点来采用第三种方案。

5. 可扩散列

用于内存不可一次载入的大数据量操作。类比B-树的实现:根D存在主存中,对磁盘内容进行索引。关键问题在于如何降低分支系数和如何进行树结构的分裂和扩展。具体分析不再详述。

需要注意一点是,如果M(每片树叶最多能存储的元素数目)过小的话,可能会导致目录过大,此时需要做一个二级索引(变相增加了M的大小),但存在潜在的无法避免的二次磁盘访问(如果主存不足以装下二级索引)。

6. 对比与应用

散列的最坏情形一般来自于实现错误,对于二叉树查找树来说,有序的输入会使其运行的很差,平衡查找树实现的代价又相当高,因此如果不需要排序(返回最大最小值)或者不确定输入是否已经排序时可以选择散列这种数据结构。

1)编译器使用散列跟踪源代码中声明的变量,即符号表(symbol table);

2)游戏编程中,通过计算基于位置的散列函数跟踪和存储一些已知的运动路径,当同样的位置再次出现时,可以避免昂贵的重复计算,即置换表(transposition table);

3)在线拼写检查程序。


《数据结构与算法分析 C++描述》原书第5章整理

原创粉丝点击