哈希表&位图&哈希算法

820 阅读6分钟

哈希表&位图&哈希算法

学习哈希算法的过程中,深深的感受到了算法之美。那些优秀的思想,散发着神奇的光芒,照亮着计算机发展前进的方向。

应用场景

  • word 软件的单词拼写错误提示功能

  • 语言层面:Java 的 HashMap LinkedHashMap

  • 布隆过滤器:实现高效的网页爬虫中的地址链接去重功能

  • 改进 LRU淘汰算法

  • 哈希算法解决安全加密,唯一标识,数据校验,哈希函数,负载均衡,数据分片,分布式存储

业界著名哈希算法

MD5, SHA, CRC

为啥哈希表这么受欢迎

这家伙查询,删除,插入的时间复杂度是 O(1)

概念特性

  1. 哈希表是数组的一种扩展,底层依赖数组支持按下标快速访问元素的特性

  2. 哈希思想:将编号(key), 通过一个函数(哈希函数hash)转化为数组下标(哈希值), 然后将对应的数据存储在数组中对应下标的位置。即 arr[hash(key)]=valuearr[hash(key)] = value

  3. 哈希函数特性

  • 哈希函数计算得到的哈希值是一个非负整数

  • 如果 key1 = key2 , 那么 hash(key1) = hash(key2)

  • 如果 key1 != key2 , 那么 hash(key1) != hash(key2)

哈希冲突

很难保证上述哈希函数特点的第三点,再好的哈希函数也无法避免哈希冲突。那么有困难我们就解决困难,加油, 奥里给!

常用的解决哈希冲突的方法有:开放寻址法,链表法

开发寻址法

  1. 思想:发现出现了哈希冲突(要插入的地方发现被占领了),就通过重新探测位置的方法来解决冲突。

  2. 线性探测法:

  • 插入:

    发现冲突,往下面继续找,有坑就占。往下找找到头了也没找到,就从头开始再找,直到找到坑。(哈希表如果满了就不会再插入了)

  • 查找

    通过哈希函数计算当前查找数据的哈希值,去哈希表找,发现找的的这哥们儿不对,就沿着插入的顺序找,如果找的过程中遇到了空的位置,说明要找的数据不在此哈希表。为什么这么说呢,想想插入的过程,你就懂了。

  • 删除

    删除有点烦人,需要考虑查找结束的条件,不能因为删除,出现空坑,干扰人查找的判断逻辑。为解决此问题,就需要删除的时候打上标记。

  1. 线性探测法的分析

    对于线性探测法,当哈希表中插入的数据越来越多时,空闲位置会越来越少,哈希冲突发生的概率就越来越大,线性探测的事件就会越来越长,导致最坏的事件复杂度将为 O(n)

  2. 开发寻址法还有其他两种经典的探测方法:二次探测法和双重哈希法

链表法

🔥 更加常用的解决哈希冲突的方法,而且相比开放寻址法更加简单。

  1. 思想:设置两个结构,槽(数组)和链表。槽记录链表的头,链表记录数据。插入的过程先经过槽,槽未被占,则占槽,给槽开辟一个链表,插入数据;槽被占,则直接插入槽对应的链表中

  2. 时间复杂度分析

    对于基于链表法解决冲突的哈希表,查找、删除操作的时间复杂度与链表的长度 k 成正比,也就是 O(k)。对于哈希比较均匀的哈希函数,从理论上将, k=n/mk = n / m,其中 n 表示哈希表中数据的个数,m 表示哈希表中槽的个数。当 k 是一个不大的常量时,我们可以粗略的认为,在哈希表中查找、删除数据的时间复杂度时 O(1)

  3. 装载因子

    我们把上面的 k 称为装载因子(load factor)。装载因子用公式表示出来就是:装载因子=哈希表中的元素个数/哈希表的长度装载因子 = 哈希表中的元素个数 / 哈希表的长度 。装载因子越大,说明链表长度越长,哈希表的性能就会越低。

打造工业级的哈希表

哈希表碰撞攻击

设计哈希函数

  1. 过于复杂的函数会消耗太多计算时间

  2. 哈希函数生成的值要尽可能随机且均匀分布

  3. 一些设计方法

  • 数据分析法:根据数据特点来设计的哈希函数的一种方法

  • 直接寻址法

  • 平均取中法

  • 折叠法

  • 随机数法

解决装载因子过大的问题

当装载因子过大时进行动态扩容,重新申请一个更大的哈希表,再将数据迁移到新表

避免低效扩容

  1. 扩容会消耗太多时间

  2. 为了解决扩容耗时长问题,可以将扩容操作穿插在多次插入操作中,利用分摊思想,分批完成。

选择合适的冲突解决方法

java 中 LinkedHashMap 通过链表法解决冲突,ThreadLocalMap 中通过基于线性探测的开放寻址法解决冲突。

  1. 开放寻址法的优缺
  • 优点

数据存到数组可以有效利用CPU缓存加快查询速度。方便序列化。

  • 缺点

    删除数据复杂;发生冲突概率高;装载因子不能太大,必须小于 1,就需要更大的存储空间。

  1. 链表法
  • 优点

    内存利用率较开放寻址法高;对大装载因子容忍度较高

  • 缺点

    要存指针,消耗额外内存空间;对CPU缓存不友好;

  • 改进

    可以不用链表,用红黑树

工业级哈希表举例

Java HashMap

  1. 初始大小

    默认大小16,可以自己设置

  2. 装载因子

    默认0.75,触发扩容后扩大到原来两倍

  3. 哈希冲突解决办法

    继续链表法,会根据链表长度,在链表和红黑树之间切换

  4. 哈希函数

    暂不讨论

一些利用哈希表解决实际问题的方案

优化 LRU淘汰算法

  1. 缓冲系统要包含的几个操作
  • 添加一个数据

  • 删除一个数据

  • 查找一个数据

  1. 思想:在原来的有序链表之上,再构建一个哈希表作为索引,哈希表中的每个节点额外存储一个指向有序链表节点的指针

Java LinkedHashMap

linked指的是双向有序链表

  1. 哈希表中的数据是经过哈希函数打乱之后无规律存储的,但是LinkedHashMap却实现了可以按照数据的插入顺序来输出

实现爬虫中网址链接去重

位借助了位图,布隆过滤器

防止数据脱库后用户信息泄露

哈希算法的原理:将任意长度的二进制值串映射为固定长度的二进制串。

哈希算法必须满足如下几个要求

  1. 从哈希值不能推导出原始数据(因此哈希算法也叫单向哈希算法)

  2. 对数据变化敏感,哪怕原始数据只改动一个二进制位,对应的哈希值也大小不同

  3. 哈希冲突的概率要很小,不同原始数据对应相同哈希值的概率非常小

  4. 哈希算法的执行效率要高,对较长的文本,也能快速计算出哈希值。