hash
- 哈希概念
- 哈希冲突
- 哈希函数
- 哈希冲突的两种解决方法之闭散列
- 哈希冲突的两种解决方法之开散列
- 开散列和闭散列的比较
哈希概念
在c++98中还并没有提出哈希这样的结构,只有以红黑树为底层结构的map,set系列,这样使得查询时的效率 l o g 2 N log_2 N log2N,但是当出现大量的数据时,查询的效率也不理想,因此在c++11,又提出了4个关联式容器,也就是unordered系列,其底层结构为hash。
哈希函数:哈希结构中,使用哈希函数使元素的存储位置与它的关键码之间建立一一映射的关系,这样在查找时就可以通过该函数很快的查找到该元素了。
哈希表:通过哈希函数构造出来的结构称为哈希表。
哈希冲突
如图,对数据集合A为{1, 7, 6, 5, 9}进行哈希函数的映射,哈希函数设计为
hash(key) = key % capacity;capacity为底层空间大小。
那么对于集合A,首先将1,7,6,5,9放入哈希表中后,如果再放一个11,通过hash函数映射后本应该放入1这个位置,但是1这个位置已经有元素了,这时候就是哈希冲突。
也就是说,不同关键字通过相同hash函数计算出相同的哈希地址,这种现象就称为哈希冲突或者哈希碰撞。
哈希函数
引起哈希冲突的原因中有一个是:哈希函数设计的不合理。
对于一个哈希函数,在设计时就要有以下原则:
1.哈希函数的定义域要包含所有的key
2.哈希函数计算出来的地址能够均匀的分布在整个空间中。
我们常见的哈希函数有:
1.直接定址法
2.除留余数法
3.平方取中法
4.折叠法
5.随机数法
6.数学分析法
注意:哈希函数的设计越巧妙,只能使得哈希冲突出现的可能性越低,但是并不能完全避免哈希冲突
哈希冲突的两种解决方法之闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
这里有两种方式:
1.线性探测: 也就是从冲突发生的位置开始,依次向后探测,直到找到下一个空位置为止。 那么对于线性探测来找空位置的方法来说,如果需要删除某个位置的元素,就不能直接删除了,因为要删除的位置可能并不是直接插入的,而是依次向后找了好多位置才找到的,贸然删除某个位置的元素,可能会导致其他的元素查找受到影响
对于线性探测来说,优点是实现简单,但是缺点是发生哈希冲突后,可能会造成数据的“堆积”,从而导致搜索效率下降。
2.二次探测: 二次探测就是为了解决数据堆积的问题,它的做法不是简单的向后依次找空位置,而是 H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m 其中:i =1,2,3…,
H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
哈希冲突的两种解决方法之开散列
对于闭散列的方式解决哈希冲突的方式来说,其空间利用率太低,这也是哈希的缺陷。
而开散列的做法是:
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。我们也称之为哈希桶。
从上图可以看到,开散列中每个桶中放的都是发生哈希冲突的元素。
那么如果元素继续增加,哈希桶又该如何扩容呢?
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?
开散列最佳情况是每个桶中只挂一个元素,那么设计为当元素个数等于桶的个数时,给哈希桶进行扩容。
开散列和闭散列的比较
开散列应用链地址法来处理哈希冲突,看似要增加链接指针,增加了存储开销,但是实际上闭散列由于要保持大量的空闲空间,以确保搜索效率(线性探测要求装载因子<= 0.5, 二次探测要求装载因子 <= 0.7)都会浪费大量的空间,因此综合来看,开散列比闭散列要更加节省空间