数据结构与算法之美 | 学习笔记14 —— 散列表基础

一、散列表(Hash Table)

散列表是数组的一种扩展,用的是数组支持按照下标随机访问的特性。

1. 散列思想

假设在运动会中,为了通过编号快速找到对应运动员的信息,当编号不再是1-89这样简单的排列时,就需要一种方法,使编号跟数组下标的元素一一对应。例如,编号前两位表示年级,中间两位表示班级,最后两位表示编号,那么一一对应的关系就是将最后两位作为数组下标。
运用这种散列思想,参赛选手的编号叫做(Key),把参赛编号转化为数组下标的影射方法叫做散列函数(也叫哈希函数),散列函数计算得到的值叫散列值(哈希值)。
因此,散列表用的就是这样的思想,通过散列函数把元素的键值影射为下标,将数据存储在对应下标的位置。当查询元素时,用同样的散列函数,将键值转化为数组下标,从对应数组下标的位置取数据。
数据结构与算法之美 | 学习笔记14 —— 散列表基础_第1张图片

2.散列函数

散列函数中**hash(key)**中,key表示元素的键值,hash(key)表示经过散列计算后得到的散列值。例如前面运动员的例子中,散列函数:

int hash(String key) {
  // 获取后两位字符
  string lastTwoChars = key.substr(length-2, length);
  // 将后两位字符转换为整数
  int hashValue = convert lastTwoChas to int-type;
  return hashValue;
}

针对散列函数的设计基本要求:

  1. 计算出的散列值是非负整数;
  2. 如果key1=key2, 那么hash(key1)==hash(key2);
  3. 如果key1≠key2, 那么hash(key1)≠hash(key2);

对于第三条,也就是,两个不同的key不应生成相同的散列值。如果不满足这个条件,可能会造成散列冲突。例如著名的哈希算法MD5, SHA, CRC等,也无法完全避免。

二、散列冲突

1. 开放寻址法(open addressing)

如果出现散列冲突,就重新探测一个空闲位置,将其插入。其中具体有两种方法:

线性探测(Linear Probing)
当向散列表中插入数据时,如果对应位置已经被占用,那么就从当前位置开始依次往后遍历查找,直到找到空闲位置为止。
数据结构与算法之美 | 学习笔记14 —— 散列表基础_第2张图片
当查找散列表中元素时,先通过散列函数求出对应散列值,再根据散列值查找对应下标元素,如果对应的数据和要查找的元素不相等,就依次往后查找,如果直到遍历到空闲位置仍未找到,说明要查找的元素不在散列表中。
数据结构与算法之美 | 学习笔记14 —— 散列表基础_第3张图片
当删除散列表中元素时,要将元素标记为deleted,不然在查找时遇到这个空闲位置会判定为散列表中不存在这个数据。
数据结构与算法之美 | 学习笔记14 —— 散列表基础_第4张图片
但当散列表中插入的数据越来越多时,会造成越来越多的散列冲突,最坏情况下要遍历数组中所有数据,最坏情况时间复杂度为O(n)。

二次探测(Quadratic probing)
线性探测每次探测的步长是1,对于二次探测的步长探测的下标序列是 h a s h ( k e y ) + 0 , h a s h ( k e y ) + 1 2 , h a s h ( k e y ) + 2 2 , . . . hash(key)+0, hash(key)+1^2, hash(key)+2^2, ... hash(key)+0,hash(key)+12,hash(key)+22,...

双重散列(Double hashing)
使用一组散列函数,当一个散列函数遇到对应位置被占用时,就用下一个散列函数,依次类推。
一般情况下,对于各种探测方法,要保证散列表中有一定比例的空闲槽位。用装载因子(load factor)表示空位的多少。

散列表的装载因子=填入表中的元素个数/散列表的长度
2. 链表法(Chaining)

相比之下,链表法是更加常用的方法。在散列表中数组的每个元素被看作“桶”或“槽”,每个槽对应一个链表,所有散列值相同的元素放在对应链表中。
数据结构与算法之美 | 学习笔记14 —— 散列表基础_第5张图片
插入和删除时,通过散列函数计算出对应的槽,然后遍历对应链表。操作的时间复杂度跟链表的长度k成正比,对散列比较均匀的散列函数来说,k=n/m,表示散列中数据的个数,m表示散列表中槽的个数。

三、应用

  1. Word文档中的单次拼写检查功能就是用散列表实现的。常用的20个英文单词大约占用2MB,对于计算机完全可以将其放在内存中,进行快速查找判断。
  2. :假设我们有 10 万条 URL 访问日志,如何按照访问次数给 URL 排序?
    解答:遍历 10 万条数据,以 URL 为 key,访问次数为 value,存入散列表,同时记录下访问次数的最大值 K,时间复杂度 O(n)。
    如果 K 不是很大,可以使用桶排序,时间复杂度 O(n)。如果 K 非常大(比如大于 10 万),就使用快速排序,复杂度 O(nlogn)。(来自smallfly)
  3. :有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?
    解答:以第一个字符串数组构建散列表,key 为字符串,value 为出现次数。再遍历第二个字符串数组,以字符串为 key 在散列表中查找,如果 value 大于零,说明存在相同字符串。时间复杂度 O(n)。(来自smallfly)

你可能感兴趣的:(数据结构,算法,数据结构)