点击蓝色 “
五分钟学算法
” 关注我哦!
加个 “
星标
” ,算法动画喂饱你!
今天小鹿就早早起床开始正准备更新今日的文章,我熟练的敲打着键盘,突然出现了下面的情况:
咦?这编辑器查错功能竟然比我手速还快,这我就不服气了,我就开始疯狂地搜着这个编辑器快速查错功能是如何实现的
?
后来在网上一搜,都说用哈希表实现的,我思考着,用哈希表怎么实现的,我对这次“案件”越来越感兴趣,然后我继续深入探索哈希表“案情”背后的秘密。
功夫不负有心人,我终于在维基百科找到了想要的答案:
伴随着此次“案件”的存在疑点重重,我开始深深的陷入对散列表的思考...
维基百科给我们散列表的定义对于新人来说确实有点难理解,如下:
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。 —— 维基百科
那小鹿再来给解释一波吧。何为散列表,散列表就像是我们超市的存储私人物品的存储柜,我们存储物品对应的柜子都会有对应的条形码,我们可以通过扫描条形码来打开对应的柜子。其实,这就类似于一个散列表。
对于数据结构中的散列表是如何实现的呢?是不是还记得我们的两位老朋友,数组和链表。我们之前再次强调,所有的数据结构基本都是由数组和链表演变而来,散列表也不例外。
我们通过自取柜的例子,可以联想到数组,数组是通过下标来访问元素的,其实散列表就是数组的一种演变,那么散列表是如何实现的呢?
我们将自取柜的二维码称之为“
键
”,用它来作为柜子的唯一标识。然后把二维码转化为特定柜子的映射方法叫做“
散列函数
”(也可以称为哈希函数)。通过映射打开对应的柜子,这个映射的值叫做“
哈希值
”
同样,数组的下标对应的就是“键”,下标所映射到的元素就是“散列值”,这就是一个散列表。
上文中,我们提到将“键”映射为“哈希值”的函数,叫做哈希函数。那么这个函数是如何实现的呢?
对于数组演变的散列表,我们可以知道哈希函数有这么几个特点:
有的小伙伴可能会问,同一个哈希值一定是同一个“键”吗?这个问题问的好,你还真别说,还真有不是一个的可能,因为存在哈希冲突。
哈希冲突是避免不了的,就算我们项目中用到的 MD5 加密也无法避免这种情况,但能做的把这种情况概率降到最低。在我们降低概率的时候同时增加了其他的开支。有种像时间换空间,空间换时间思想的意思。
什么是哈希冲突?举个例子,比如我们往 5 个桶里放 6 个小球,每个桶中规定只能放一个,那剩下的一个不得不放入其中一个桶中,这就是所谓的哈希冲突。
难道没有更好的方法解决哈希冲突吗?有的,但是并不能完全解决,而是通过其他的开销来降低冲突的概率。
我们共有两种解决办法,
开放寻址法
和
拉链法
(又叫链表法)。
开发寻址的法的原理就是如果我们发生了哈希冲突,也就是说通过散列函数得出的散列值相同,我们就重新探测一个位置,将数据存储。那如何进行探测呢?
线性探测
所谓的线性探测,就是一个一个的进行探测如下图动画,在散列表中插入一个元素:
查找元素也是同样的道理,如果在散列表中查找的元素和我们要查找的元素相同,则直接取出,否则通过线性探测,一个一个去查找,直到没有查找到位置。
对于删除元素呢?这就比较麻烦一点,因为我们删除元素之后,再进行插入元素或者查找元素就出现位置空缺了,无法完成正常的操作了,所以我们删除元素规定不能将元素进行真正的删除,而是做一个标记,如果查找元素,遇到该标记则继续查找。
二次探测
上边我们是进行线性查找,二次探测就是每次探测都是原来的平方探测。
这两种方式只是方式上的不同,如果散列表的空间不足时,产生的哈希冲突还是很大概率的。我们通常用一个阀值来表示散列表中剩余空间的大小,我们称这个阀值为装载因子。(
装载因子 = 元素个数 / 散列表的大小
)。