Word这种文本编辑器你平时应该常常见吧,那你有没有留意过它的拼写检查功能呢?一旦咱们在Word里输入一个个错误的英文单词,它就会用标红的放式提示“拼写错误”。java
Word的这个单词拼写检查功能,虽然很小但却很是实用。你有没有想过,这个功能是如何实现的呢?
其实啊,一点儿都不难。只要你学完今天的内容,散列表(Hash Table)。你就能像微软Office的工程师同样,轻松实现这个功能。算法
散列表用的是数组支持按照下标随机访问数据的特性,因此三列表其实就是数组的一种扩展,因为数组演化而来,能够说、若是没有数组、就没有散列表数组
按照编号查找选手信息,效率是否是很高 时间复杂度就是 O(1)数据结构
参赛编号(6位数) 年级(前2位)+班级(中间2位)+编号(最后2位)编辑器
截取后两位做为数组下标来存取选手信息 取参赛编号的后两位,做为数组下标,来读取数组中的数据。函数
这就是典型的散列思想:性能
其中、参赛选手的编号咱们叫做键(key)或者关键字,咱们用它来表示一个选手、spa
咱们把参赛编号转化为数组下标的映射方法就叫作散列函数(或Hash函数 哈希函数)、而散列函数计算获得的值就叫做散列值设计
咱们能够总结出这样的规律:散列表用的就是数组支持按照下标随机访问时候、时间复杂度是O(1) 的特性。咱们经过散列函数把元素的键值映射为下标,而后将数据存储在数组中对应小标的位blog
置。当咱们按照键值查询元素时,咱们用一样的散列函数,将键值转化数组下标、从对应的数组下标的位置数据
散列函数、顾明思义、它是一个函数、咱们能够把它定义成hash(key),其中key表示元素的键值、hash(key)的
值表示通过散列函数计算获得的散列值
那第一个例子中、编号就是数组下标、因此hash(key)就等于key、改造后的例子,写成散列函数以下
int hash(String key) { // 获取后两位字符 string lastTwoChars = key.substr(length-2, length); // 将后两位字符转换为整数 int hashValue = convert lastTwoChas to int-type; return hashValue; }
若是参赛选手的编号是随机生成的6位数字、又或者用的是a到z之间的字符串,该如何构造散列函数呢?我总结了三点散列函数设计的基本要求
一、散列函数计算的到的散列值是一个非负整数
二、若是key1 = key2,那 hash(key1) == hash(key2);
三、若是key1 ≠ key2,那 hash(key1) ≠ hash(key2);
第三点理解起来可能会有问题,我着重说一下。这个要求看起来合情合理、可是在真实的状况下、要想找到一个不一样的key对应的散列值都不同的三类函数、几乎是不可能的。即使像业界著名的MD五、SHA、CRC等哈希算法,也没法彻底避免这种散列冲突、并且、由于数组的存储空间有限、也会加大散列冲突的几率
因此咱们几乎没法找到一个完美的无冲突的散列函数、即使能找到、付出的时间成本、计算成本也是很大的、因此针对散列冲突问题、咱们须要经过其余途径来解决
若是出现了散列冲突、咱们就从新探测一个空闲位置、将其插入、那如何从新探测新的位置呢?
当某个数据通过散列函数散列滞后、存储位置已经被占用了、咱们就从当前位置开始、依次日后查找,看是否有空闲位置,直到找到为止
黄色的色块表示空闲位置、橙色的色块表示已经存储了数据
一、从图中能够看出、散列表的大小为十、在元素x插入散列表以前,已经6个元素插入到散列表中
二、x通过hash算法以后,被三列到位置下标为7的位置、可是这个位置已经有数据了、因此就产生了冲突,
三、因而咱们就顺序地日后一个一个找,看有没有空闲的位置、遍历到尾部都没有找到空闲的位置
四、因而咱们再从表头开始找,直到找到空闲位置2,因而将其插入到这个位置
散列函数求出要查找元素的键值对应的散列值
而后比较数组中下标为散列值的元素和要查找的元素若是相等、则说明就是咱们要找的元素
不然就数序日后一次查找、若是遍历数组中的空闲位置,尚未找到,就说明要查找的元素并无在列表中
找到一个空闲位置,咱们就能够认定散列表中不存在这个数据。
可是,若是这个空闲位置是咱们后来删除的,就会致使原来的查找算法失效。原本存在的数据,会被认定为不存在。这个问题如何解决呢?
咱们能够将删除的元素、特殊标记为deleted,当线性探测的时候、遇到标记为deleted的空间、并非停下来、而是继续往下探测
最坏状况下的时间复杂度为 O(n)
一、在删除和查找时,也有可能会线性探测整张散列表,才能找到要查找或者删除的数据
二、对于开放寻址的冲突解决方法、除了线性探测方法以外,还有另外两种比较经典的探测方法二次探测和双重散列
线性探测 步长是 1 hash(key)+0,hash(key)+1,hash(key)+2...
二次探测 步长是 1 hash(key)+0,hash(key)+12,hash(key)+22
双重散列 使用一组散列函数 hash1(key),hash2(key),hash3(key)……若是第一个散列函数计算机获得的存储位置已被占用在用第二个散列函数、依次类推、直到找到空闲的存储位置
无论采用哪一种探测方法,当散列表中的空闲位置很少的时候、散列冲突的几率就会大大提升、为了尽量保证散列表的操做效率,通常状况下,咱们会尽量保证散列表中有必定比例的空闲槽位、
咱们用装载引子(load factor)来表示空位的多少
装载因子的计算机公式是:
散列表的装载因子 = 填入表中的元素个数 / 散列表的长度
装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会降低
链表是一种更加经常使用的散列冲突解决办法、相比开放寻址法,它要简单不少、咱们来看这个图,在散列表中,每一个桶或者槽
会对应一条链表,全部散列值相同的元素咱们都放到相同槽位对应的链表中
当插入的时候,咱们只须要经过散列函数计算出对应的散列槽位,将其插入到对应链表中便可,插入时间复杂度是O(1)
当查找、删除一个元素时、咱们一样经过散列函数计算出对应的槽位、而后遍历链表查找或者删除、那查找或删除操做的时间复杂度是多少呢?
实际上、这两个操做的时间复杂度跟链表的长度K成正比,也就是O(k)对于散列比较均匀的散列函数来讲理论上讲k=n/m,其中 n 表示散列中数据的个数,m 表示散列表中“槽”的个数。
链表是一种更加经常使用的散列冲突解决办法、相比开放寻址法,它要简单不少、咱们来看这个图,在散列表中,每一个桶或者槽
会对应一条链表,全部散列值相同的元素咱们都放到相同槽位对应的链表中
有了前面这些基本知识储备,咱们来看一下开篇的思考题:Word中档中单词拼写检查功能是如何实现的?
经常使用的英语单词有20万个左右,假设单词的平均速度是10个字母,平均一个单词占用10个字节的内存空间,那20万英个单词大约占2MB的存储空间,就算放大10倍也就是20MB。
对于如今的计算机来讲,这个如今彻底能够放在内存里面。因此咱们能够用散列表来存储整个英文单词词典。
当用户输入某个英文单词时,咱们拿用户输入的单词去散列表中查找。若是查到,则说明拼写正确;若是没有查到,则说明拼写可能有误,给予提示。
借助散列表这种数据结构,咱们就能够轻松实现快速判断是否存在拼写错误。