茶艺师学算法打卡10:散列表
学习笔记
散列表
散列表,又叫哈希表,第一印象就是“那个很好用的,能存键值对的数据结构,比如 go 语言里的 map”。实际上,散列表就是“数组”,只不过是通过某种,它的查找性能来源于数组的“根据下标查找元素的时间复杂度为”的特性。
哈希函数
如何将“键”与“数组的下标”一一对应,这就是哈希函数要做的事。
哈希函数可以有多种方法实现,无论用了哪种方法实现,都应该保证以下要求:
- 算出的哈希值,即“数组下标”不能是负数。总不能下标越界吧?
- 如果 ,那么 ,换个说法,就是前后两次计算同一个“键”,得到的得是同一个结果。
- 如果 ,那么 ,换个说法,就是两次计算不同的“键”,其结果不能是同一个值。不然两个不同的东西就要“存”到一起。
哈希冲突与应对方法
这里有个问题,假如经过哈希函数算出的一个“下标”,但发现这里已经存放着别的“值”,即所谓的哈希冲突,这又该怎么办?
虽然说设计好的哈希函数就是要避免这情况,这么好的哈希函数本身就是不这么好设计,而且过小的哈希表大小也是会引发这情况。
因此人们另外发现开放寻址法与链表法来解决该问题。
开放寻址法,一句话概括就是,“在(哈希)计算后得出的下标后面,寻找空的位置安放元素”。
里面最简单的,就是线性探测,“在下标后面挨个找空位”。用这方法时,需要注意两点:
- 如果要删除了里面的某个元素,不能直接将该下标的元素设为 NULL ,不然会让查找算法失效:这里是原本就是空位置,还是经过删除后的空位置?合适的做法是把该元素标记起来。
- 随着元素的放入,哈希表也是会越放越满,寻找空位置的耗时也会越来越长,到最后可能得把整个表都要找一遍,即耗时从 退化为 (n 为散列表的大小)。
二次探测与线性探测的不同在于寻址的步长,线性探测是 ,而二次探测是 。
双重哈希则准备着多个哈希函数 ,当 冲突了, 就换着算 ,直到算出不会冲突的下标为止。
至于链表法,顾名思义,放元素的位置,换成链表,即哈希到同一个位置的元素都放进同一个链表里。
在链表里放一个新元素很快, 时间。
而在同一个链表里查找一个元素,虽然说是要从前往后遍历,但链表长度不会是数据规模 m,在这个哈希表的链表长度都一样的情况下,查找一条链表的耗时最多就是 ,而这个 ,被成为装载因子,值越大,表明链表越长,哈希表性能越差。
装载因子过大与扩容
链表能容忍大的装载因子,但数组不行。当基于数组的哈希表装载因子变大了(意味着空的位置少了),可以扩充数组的空间(增加更多的空位),这时装载因子就可降下来。
数组的扩容,旧数组的内容需要复制进新数组里面去,而哈希表的扩容,还要重新算哈希函数。
如果对旧哈希表数据复制进新哈希表的耗时忍受不了的话,可以试着这样的策略:当有新数据进来,就直接进新表,同时把一些旧表的数据复制进来,把整个数据搬动全分散在若干次的新数据插入动作里。这样用起来体验会好些。