哈希表(HashTable,也叫散列表),是根据键名(Key)直接访问对应内存存储位置的数据结构。
其实现原理是通过哈希函数(也叫散列函数)将元素的键名映射为数组下标(转化后的值叫做哈希值或散列值),然后在对应下标位置存储记录值。当我们按照键名查询元素时,可以使用同样的哈希函数,将键名转化为数组下标,从对应的数组下标位置读取数据:
显然,哈希表使用了数组支持按照下标随机访问数据的特性,所以哈希表其实就是数组的一种扩展,由数组演化而来。可以说,没有数组,就没有哈希表。我们知道,数组访问元素的时间复杂度是 O(1),所以哈希表也是一样(不考虑哈希函数的复杂度的话),因此非常高效。
此外,我们也可以看到,哈希技术既是一种存储方法,也是一种查找方法。不过,与之前介绍的查找算法不同的是哈希表的不同记录之间不存在逻辑关系,因此最适合求解的问题是查找与给定值相等的记录,而不适合做范围查询。
哈希表中有两个关键的概念,一个是哈希函数(或者叫散列函数),一个是哈希冲突(或者叫散列冲突)。下面,我们来重点介绍这两个概念。
哈希函数用于将键名经过处理后转化为对应的哈希值。具有以下特性:
所谓哈希冲突,简单来说,指的是 key1 != key2 的情况下,通过哈希函数处理,hash(key1) == hash(key2),这个时候,我们就说发生了哈希冲突。
设计再好的哈希函数也无法避免哈希冲突,根本原因是哈希值是非负整数,总量是有限的,但是现实世界中要处理的键名是无限的,将无限的数据映射到有限的集合,肯定避免不了冲突。
事实上,如果不考虑哈希冲突,哈希表的查找效率是非常高的,时间复杂度是 O(1),比二分查找效率还要高,但是因为无法避免哈希冲突,所以哈希表查找的时间复杂度取决于哈希冲突,最坏的情况可能是 O(n),退化为顺序查找。这种情况在哈希函数设计不合理的情况下更糟。
要减少哈希冲突,提高哈希表操作效率,设计一个优秀的哈希函数至关重要,我们平时经常使用的 MD5 加密就是一个哈希函数,但是其实还有其他很多自定义的设计实现,要根据不同场景,设计不同的哈希函数来减少哈希冲突,而且哈希函数也要足够简单,否则执行哈希函数本身会成为哈希表的性能瓶颈。
我们日常很少会自己去设计哈希函数,但是做一些简单的了解还是有必要的。通常有以下几种哈希函数构造方法:
以上只是一些比较常见的哈希函数设计思路,还有很多其他的设计方法,这里就不一一列举了。
我们前面说过,设计再好的哈希函数也不能完全避免哈希冲突,我们只能优化自己的实现让哈希冲突尽可能少出现罢了,如果出现了哈希冲突,该如何处理呢?下面给出一些思路:
介绍完以上内容之后,想必你对如何打造工业级哈希表已经心中有数。主要考虑因素包含以下几个方面:
补充一张链地址法处理哈希冲突的图示:
我们前面分享了哈希表、哈希函数和哈希冲突,哈希算法简单理解就是实现前面提到的哈希函数的算法,用于将任意长度的二进制值串映射为固定长度的二进制值串,映射之后得到的二进制值就是哈希值。
我们日常开发中最常见的哈希算法应用就是通过 MD5 对数据进行加密了:
package main
import (
"crypto/md5"
"fmt"
)
func main() {
data := []byte("Hello, World!")
hash := md5.Sum(data)
fmt.Printf("原始值: %s\n", data)
fmt.Printf("MD5值: %x\n", hash)
}
这里的 md5.Sum 就是一个哈希函数。执行上述代码,打印结果如下:
哈希算法的一般特性如下:
1、场景一:安全加密
我们日常用户密码加密通常使用的都是 md5、sha 等哈希函数,因为不可逆,而且微小的区别加密之后的结果差距很大,所以安全性更好。
2、场景二:唯一标识
比如我们的 URL 字段或者图片字段要求不能重复,这个时候就可以通过对相应字段值做 md5 处理,将数据统一为 32 位长度从数据库索引构建和查询角度效果更好,此外,还可以对文件之类的二进制数据做 md5 处理,作为唯一标识,这样判定重复文件的时候更快捷。
3、场景三:数据校验
比如我们从网上下载的很多文件(尤其是 P2P 站点资源),都会包含一个 MD5 值,用于校验下载数据的完整性,避免数据在中途被劫持篡改。
4、场景五:哈希函数
前面我们已经提到,PHP 中的 md5、sha1、hash 等函数都是基于哈希算法计算哈希值。
5、场景五:负载均衡
对于同一个客户端上的请求,尤其是已登录用户的请求,我们需要将其会话请求都路由到同一台机器,以保证数据的一致性,这可以借助哈希算法来实现,通过用户 ID 尾号对总机器数取模(取多少位可以根据机器数定),将结果值作为机器编号。
6、场景六:分布式缓存
分布式缓存和其他机器或数据库的分布式不一样,因为每台机器存放的缓存数据不一致,每当缓存机器扩容时,需要对缓存存放机器进行重新索引(或者部分重新索引),这里应用到的也是哈希算法的思想。后面我们介绍 Redis 系列的时候会系统阐述这一块。