Hash
一个对象或者内容,输入到一个算法执行计算得出一个唯一的值。
- 这个过程我们称为映射;也叫hash
- 这个算法我们称为hash算法或者hash函数;该算法是单向不可逆的,即无法通过hash值反向推导出原对象。
- 计算出来的唯一值我们称为hash值。
- 计算出来的hash值在一个环境下是一个固定长度的值。通常算法是将一个很大的输入映射为一个很小的固定长度的值。否则难以保存。
计算出来的hash值可能会重复,这取决于hash函数/算法的可用性和准确性。理论上,这个值必需是唯一的,但是目前研发出来的hash算法都多多少少会出现重复的情况,只是概率极低。这种重复我们称为“hash碰撞”。
计算出来的值我们可以称之为key,一个key映射到一个地址,该地址保存该对象或者内容。通过该key可以直接访问到对应的对象。
而保存这些对象的结构我们称为hash表,或者散列表。
通常的实现是,使用一个数组来保存这些key,而使用另外一个数组来保存对应的对象。而这些key通过某些方法保存与之对应的数组索引或者地址。
这种结构与下述场景很相似:
现在有很多房屋,每个房屋通过某种方法确定一个唯一的编号与之对应。这些房屋的钥匙存在一个箱子,箱子标有房屋的编号。想进入房屋的方法是:拿到该房屋的编号,然后去对应的箱子去拿到该该房屋的钥匙。
所以Hash表的访问速度非常快,总是O(1)。
应用场景
hash可以使用在如下场景:
- 加密:其单向性决定的;
- 数据验证:其结果唯一性决定的;
- 海量数据:海量数据的关键问题不是存储空间,而是查找时间。hash表支持随机访问。也就是因为这个特性,目前市面上的许多高效的存储引擎都使用hash表来保存数据。传统的RMDB是无法做到的。
Hash碰撞
hash碰撞也叫hash冲突。这是hash表最关键的,也是设计hash算法最难的。
解决hash碰撞的方法有:
- 开放寻址:
- 链表法:
Hash表和二叉树
哈希表有以下几个突出的缺点:
-
当更多的数插入时,哈希表冲突的可能性就更大。对于冲突,哈希表通常有两种解决方案:第一种是线性探索,相当于在冲突的槽后建立一个单链表,这种情况下,插入和查找以及删除操作消耗的时间会达到O(n),且该哈希表需要更多的空间进行储存。第二种方法是开放寻址,他不需要更多的空间,但是在最坏的情况下(例如所有输入数据都被map到了一个index上)的时间复杂度也会达到O(n)。
-
所以,在决定建立哈希表之前,最好可以估计输入的数据的size。否则,resize哈希表的过程将会是一个非常消耗时间的过程。例如,如果现在你的哈希表的长度是100,但是现在有第101个数要插入。这时,不仅哈希表的长度可能要扩展到150,且扩展之后所有的数都需要重新rehash。
-
哈希表中的元素是没有被排序的。然而,有些情况下,我们希望储存的数据是有序的。
二叉树:
-
二叉树不会有冲突(collision),这意味着我们能够保证二叉树的插入和查找操作一直都是O(log(n))的时间复杂度。
-
二叉树的空间占用跟输入的输入数据一致。所以我们不需要为二叉树预先分配固定的空间。所以,你也不需要预先知道输入数据的size。
-
所有的元素在树中是排序好的。