zoukankan      html  css  js  c++  java
  • 散列表与哈希算法学习笔记

    散列表与哈希算法

    一,散列表原理(Hash Table)

    散列表来源于数组具有下标随机访问特性,理解这点非常重要。可以说散列表是由数组进化来的。将输入的键通过哈希函数映射得出的value作为index去table中查询,这便是散列的思想。

    graph LR A[键值key] -->|哈希函数|B(结果value)

    我们了解到为什么散列表的查询复杂度是O(1),因为key->value为计算过程,O(1),数组支持随机访问,查询也为O(1),所以散列表的查询效率为O(1)。

    我们可以很明确的看出,散列函数(即hash function)是至关重要的。散列函数具有的特点有:

    • hash(key)为非负的
    • 当key1 == key2 ,hash(key1) == hash(key2)
    • 当key1 != key2 ,hash(key1) != hash(key2)

    第三点要求很难实现,这是由于散列冲突是几乎不可避免的,我们来聊聊散列冲突。

    1,散列冲突

    像知名的hash算法:MD5,SHA也无法完全避免散列冲突,常见的解决方法为两类:开放寻址和链表法。

    Ⅰ ,开放寻址

    开放寻址的思想是:如果出现了散列冲突,我们就重新探测一个位置,将其插入。如何探测一个新的位置呢?我们有几种方法:

    ①,线性探测

    如果出现冲突,则从当前位置往后挪,直到找到空闲位置。

    优点 缺点
    简单 若元素过多,冲突概率会很大,查询/插入/插入的效率急速降低
    删除元素不能直接删除,要标记一下deleted

    ②,二次探测

    它与第一种方法的区别是,线性探测挪动的步伐为1,而二次探测挪动的步伐为0,1,4,9,。。。。

    ③,双重散列

    我们不只使用一个散列函数,我们使用一组散列函数,hash1,hash2,hash3,....

    以上三种方法,不管用哪种,当装载因子过大时,冲突的概率都会大大提高。

    我们用装载因子表示空位的剩余。计算公式为:
    装载因子 = 填入的元素个数/table的长度

    Ⅱ,链表法

    链表法相对开放寻址法,更加常用,更加简明。在链表法中,每个元素存储的时一条链表,所有散列值相同的元素,我们都放到对应的链表中。

    开放寻址与链表法的优劣

    ①,开放寻址优劣

    优点

    开放寻址的数据都在散列表中,有效利用CPU缓存,加快查询,并且序列化简单

    缺点

    删除数据比较麻烦,可能需要标记deleted,相比链表法,冲突概率高,不能将装载因子设计的太大,所以相比链表法,更耗内存。4

    ②,链表法优劣

    优点

    内存利用率相对高,对大的装载因子容忍,

    缺点

    CPU缓存不友好(由于有链表)。

    实际上我们可以在每个槽里不存指向链表的指针,而是存红黑树,跳表这种高效查询的数据结构,有效避免散列碰撞攻击(即使数据都挤在一个槽,查询效率也为O(logn))。

    二,设计散列表

    工业级的散列表,需要应对各种异常情况,避免散列冲突下性能急剧下降,抵抗散列碰撞攻击。

    PS:散列碰撞攻击指的是恶意的用户构造恶意的数据,使得所有数据经过散列函数之后,都进入了一个bucket(slot,槽),再去查询,这样会大量消耗CPU资源,以此来做Dos攻击。

    1,设计散列函数

    要求:

    1. 不能太复杂,不然消耗CPU计算
    2. 生成的value尽量随机且均匀分布。
    3. 合理利用关键字的特点,散列表的大小

    例如,对于字符串,我们可以用26进制求出值,再模长度

    2,装载因子过大如何处理

    装载因子过大,说明表中元素过多,空闲位置少,散列冲突的概率会很大,插入数据会多次寻址(开放寻址法)或槽中的链表的很长(链表法),导致查询效率很低。

    对于频繁插入的动态散列表,当装载因子超过某个阈值,需要进行扩容,重新申请内存空间,重新计算哈希值,并搬移数据,O(n)的复杂度。

    3,一次性扩容并搬移数据,效率很低怎么办?

    我们可以分批处理,先申请空间,将插入的数据直接插入新的散列表里,然后旧的散列表里的数据分批逐步的同步到新散列表,当查询时可以先查新散列表,再查旧的。。
    解决问题:触发阈值扩容并搬移数据这个过程在数据量很大时效率是极低的,让人崩溃

    三,哈希算法

    将任意长度的二进制位映射到固定长度的二进制位的映射规则,称为哈希算法。
    一个优秀的哈希算法包括以下要求:

    1. value不能反推出key
    2. 输入的数据即使相差一个bit,输出的value也会相差很大
    3. 冲突概率尽可能小。‘
    4. 算法的执行效率要高,不占用过多计算资源。

    我们着重了解哈希算法的应用。

    1,安全加密

    最常用的安全加密算法:MD5,SHA,DES,AES

    为什么可以用哈希算法做安全加密是由于key的向量空间是非常大的,利用穷举的方法找到两个哈希值一样的文本是几乎不可能的。

    2,唯一标识

    拿一张图片,去判断图库中是否有这张图片,如何做呢?
    对这张图片做哈希运算,将得到的值进行去查,大大节省了时间。

    3,数据校验

    比如我们下载一个很大的电影,服务器会将这个大文件分拆成上百个小文件发送,那么如何确保数据没有被篡改或者丢失?

    利用哈希的思路:将下载下来的文件做哈希运算,得到的值和种子文件做对比。

    4,散列函数

    散列表是哈希函数的一种应用,区别是散列函数追求简单,快速,对于加密并不重视。

    四,哈希算法与分布式

    1,负载均衡

    如何实现一个会话粘滞(session sticky)的负载均衡算法?非常简单,**通过哈希算法,对客户端IP地址或会话ID计算哈希值,取模运算映射到相应的服务器。

    2,数据分片

    我们来看两个非常常见的面试题:

    Ⅰ 大数据统计“搜索关键字”出现的次数

    Description:我们有1T的内存,我们想快速统计每个关键字被搜索的次数,怎么做呢?我们有以下难点:

    1. 一台机器的内存,无法容纳
    2. 只用一台机器,处理时间会很长
      解决方法:
      ** 先对数据分片,采用多台机器,提高速度。**
      具体思路:
      我们用n台机器并行处理,我们从搜索记录的日志文件中,依次读取每个搜索关键字,进行哈希运算,跟n取模,得到值就是分配到的机器编号。
      由此一来,哈希值相同的搜索关键字就被分配到了同一台机器。
      最后再将n台机器的结果合并在一起。

    这正是MapReduce的思想。

    Ⅱ 快速判断图片是否在图库中(图库特别大)

    如果我们对图片构建散列表,单台机器内存有限。

    同样,我们可以进行数据分片,采用多机处理。每台机器都有对应的散列表,我们去判断的时候,先哈希运算,取值模n得到机器号,再由相应的机器进行处理。当然,相应的机器可以构建散列表,由于数据分片了,内存是合适的。

    3,分布式存储

    面对海量数据,为了提高读写能力,一般用分布式方式存储数据。

    跟前面的思路类似,数据分片,哈希运算获得机器号,然后去相应的机器做查询。

    问题来了,假如缓存机器不够了,需要做扩容怎么办?麻烦来了,简单的增加机器并不可取。比如本来10台机器,那么15被映射到5号机器,我们增加两台,那么15会被映射到3号机器。也就是说此时缓存失效了(需要搬移数据到正确的机器上),会直接向数据库索要数据,会压垮数据库。
    一致性哈希就是解决这个问题的,可以避免大量的数据搬移。

    关于一致性哈希,有大牛讲的很好,贴出链接:
    http://www.zsythink.net/archives/1182

    每一篇博客,不为别的,证明我的成长。每一次发文,不为别的,证明我严阵以待。蜗牛爬得很慢,却终有一日登上参天大树。因为它热爱。
  • 相关阅读:
    java拦截器与过滤器打印请求url与参数
    mybatis学习笔记(六)使用generator生成mybatis基础配置代码和目录结构
    【IDEA】IDEA创建Maven的Web项目并运行以及打包
    【环境变量】Linux 下三种方式设置环境变量与获取环境变量
    【Git】GitHub的SSH提交配置[
    spring配置redis注解缓存
    【查看linux配置】查看linux系统常用的命令,Linux查看系统配置常用命令
    Redis集群
    linux中wget 、apt-get、yum rpm区别
    spring+redis的集成,redis做缓存
  • 原文地址:https://www.cnblogs.com/agui521/p/12499242.html
Copyright © 2011-2022 走看看