数据结构与算法系列二十一(散列函数)

208 阅读3分钟

上一篇我们一起分享了散列表的基础知识。你还记得什么是散列表吗?我们来回顾一下:散列表是数组的一种扩展,利用了数组支持按照下标随机访问的特性

我们还举了一个例子:日常开发中,经常使用的HashMap,它的底层结构就是用了散列表。从现在开始,你应该知道在你的项目中,如何用好HashMap了吧。

另外,上一篇我们留下了两个思考:

1.如何设计散列表的散列函数?

2.如何解决散列表中的散列冲突?

#考考你:
1.你知道如何设计散列表的散列函数吗?
2.你知道如何解决散列冲突吗?

案例

散列函数

散列函数直观理解

我们说散列表是数组的一种扩展,这里的意思是说散列表需要经过某种转换,把要存储的数据与数组的下标建立起联系,这样才能利用数组按照下标随机访问的特性,时间复杂度O(1)。如下图所示:

image.png

上面这个图你应该还记得。

#散列函数直观理解:
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哈希算法,也是存在散列冲突的。
    • 散列冲突背后隐藏的一个逻辑是:我们不能把散列函数设计得太复杂,太复杂的散列函数,需要耗费更多的计算资源,性能必定下降。这不符合使用散列表的初衷(高性能)
    • 既然存在散列冲突,是不是就不用散列表了呢?答案是:否。关于散列冲突,有相应的解决方法。我们留在下一篇中分享