上一篇我们一起分享了散列表的基础知识。你还记得什么是散列表吗?我们来回顾一下:散列表是数组的一种扩展,利用了数组支持按照下标随机访问的特性。
我们还举了一个例子:日常开发中,经常使用的HashMap,它的底层结构就是用了散列表。从现在开始,你应该知道在你的项目中,如何用好HashMap了吧。
另外,上一篇我们留下了两个思考:
1.如何设计散列表的散列函数?
2.如何解决散列表中的散列冲突?
#考考你:
1.你知道如何设计散列表的散列函数吗?
2.你知道如何解决散列冲突吗?
案例
散列函数
散列函数直观理解
我们说散列表是数组的一种扩展,这里的意思是说散列表需要经过某种转换,把要存储的数据与数组的下标建立起联系,这样才能利用数组按照下标随机访问的特性,时间复杂度O(1)。如下图所示:
上面这个图你应该还记得。
#散列函数直观理解:
1.我们假设学生编号:0-100
2.数组的下标:0-100
3.通过转换函数:hash(key) = key,实现选手的比赛成绩,与数组下标联系
4.最终满足:给定任意学生编号50,通过hash(50)=50,在O(1)时间复杂度内,获取到学生的比赛成绩
5.这里的hash(key),就是我们说的散列函数
散列函数基本设计原则
我们知道了散列函数的作用,接下来看一下如何设计散列函数。它有这么几个基本原则:
#散列函数设计原则:
1.通过散列函数得到的散列值,必定是非负整数
2.如果key相同,那么散列值,必定相同
3.如果key不相同,那么散列值,尽量不同
散列函数设计原则理解:
-
通过散列函数得到的散列值,必定是非负整数
- 散列函数的作用,就是要建立与数组下标的联系,我们知道数组的下标是非负整数。因此这一条应该很好理解
-
如果key相同,那么散列值,必定相同
- 假设key1=key2,那么hash(key1)=hash(key2)。这一条应该也很好理解。如果相同的key,得到不同的散列值,我们就没有办法获取散列表中的值。
- 想象一下:你只能把钱存到银行,却不能从银行把钱取出来,你肯定不愿意,对吧
-
如果key不相同,那么散列值,尽量不同
- 这一条理解起来,要麻烦一些了。我们的期望:假设key1 != key2,那么hash(key1) != hash(key2)
- 我们说理想与现实总是存在的距离的,而且很多时候距离还不产生美!在实际应用中,不同的key,经过散列函数后,散列值可能会相同,我们称为散列冲突。即可能是这样:key1 != key2,但是:hash(key1) = hash(key2) 。
- 那你可能会说,这一条真不能避免吗?很遗憾,真的不能,我们只能说是尽量不同,尽量避免冲突。即便是业界知名的MD5、SHA哈希算法,也是存在散列冲突的。
- 散列冲突背后隐藏的一个逻辑是:我们不能把散列函数设计得太复杂,太复杂的散列函数,需要耗费更多的计算资源,性能必定下降。这不符合使用散列表的初衷(高性能)
- 既然存在散列冲突,是不是就不用散列表了呢?答案是:否。关于散列冲突,有相应的解决方法。我们留在下一篇中分享。