数据结构四——散列表(上)

文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。

1散列表的由来

从数组随机访问特性说起。
 数组的随机访问特性是:数组a,a[5]可以直接访问到数组的第6个元素。这就类似于在下标和数组对应的值之间建立了映射关系。
 散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。没有数组就没有散列表。
 应用在实践中。例如有100名学生,每个学生有一个学号,学号是从0到99,数组score存放每个学生的成绩,score[0]表示学号是0的学生的成绩,score[1]表示学号是1的学生的成绩… 学号->数组下标->学生成绩。f1(学号)=数组下标。
 现在更进一步,学号的规则是“年级+班级+数字”,例如“50137”表示5年级1班,37还是和上面的含义一样。那么我们就可以写函数把学号和数组下标映射起来,查找学生成绩。依然是:学号->数组下标->学生成绩。f2(学号)=数组下标。f1与f2不同。
 这里的学号就是key,学生成绩是value,f1、f2是散列函数。散列函数计算得到的值是哈希值。

2散列函数

散列函数一般表示为hash(key)
 散列函数的三点要求。
 1 散列值是非负的。
 2 如果key1=key2,那么hash(key1)=hash(key2)。
 3 如果 k e y 1 ̸ = k e y 2 key1\not= key2 key1̸=key2,那么 h a s h ( k e y 1 ) ̸ = h a s h ( k e y 2 ) hash(key1)\not=hash(key2) hash(key1)̸=hash(key2)

3散列冲突

定义
 在实际中因为数组存储空间的限制,要想做到key值不同的时候哈希值不同,几乎很难满足。这个时候就产生了散列冲突,也要哈希冲突。换句话说就是数组只有10个下标,学生有5个人,但学号是随机的,怎么映射,能够快速访问到学生成绩。

3.1开放寻址法

开放寻址法=可以改变哈希值的解决方法
 开放寻址法的核心思想是:如果发生了散列冲突,就重新找一个空闲位置插入数据。怎么找呢?三种方法:线性探测、平方探测、再哈希。
 在这里插入图片描述

线性探测

如果hash(key)=7,且数组score[7]已经被占用,那就探测8的位置是不是被占用,9的位置是不是被占用,0的位置是不是被占用…一直找到空闲位置。探测顺序是(hash(key)+0)%size,(hash(key)+1)%size,(hash(key)+2)%size,(hash(key)+3)%size…

平方探测

如果hash(key)=7,且数组score[7]已经被占用,那就探测8的位置是不是被占用,1的位置是不是被占用,8的位置是不是被占用…一直找到空闲位置。探测顺序是(hash(key)+0)%size,(hash(key)+1)%size,(hash(key)+4)%size,(hash(key)+9)%size…
 数组大小是有限的,再探测的时候一定要对数组大小size取余。

再哈希

再哈希是指当发生冲突的时候,再找一个散列函数计算,探测空间是不是被占用,如果被占用,继续再找一个散列函数计算。
 线性探测和平方探测其实是再哈希的特殊形式。再哈希的函数f(x)=x,或者 f ( x ) = x 2 f(x)=x^2 f(x)=x2

查找

在这里插入图片描述
 查找过程和插入过程类似。我们通过散列函数求出要查找元素key值的哈希值,然后比较数组中下标为散列值的key和要查找的元素key值。如果相等则查找到,否则继续探测查找,直到数组中出现空闲位置。
 我的思考:散列表中存储的是value值。例如最上面的例子,学号就是数组下标的时候,散列表就是数组,存储的是学生成绩。当学号变成随机的时候,散列表中存储的是学生实体。包含学号和学生成绩。查找的时候 比对的是key值是否相同。学号->哈希值->学生成绩。

删除操作

当需要删除数据的时候,需要注意不能直接将数组的值置为空。因为在查找过程中出现空闲位置就停止不找了。这样查找就不准确了。可以将该位置放置删除标记。

3.2 链表法

在这里插入图片描述

所有哈希值相同的元素放在同一个槽(slot)或者链表内,形成一个链表。
 当插入的时候只需要计算插入元素key值的哈希值,找到对应的slot,添加到链表中即可。时间复杂度O(1)。
 当查找或者删除的时候同样计算哈希值,添加或者删除链表中的元素。时间复杂度与链表的长度k成正比,所以是O(k)。所以更希望哈希值的分布式均匀的。

4散列函数与内存

4.1 散列函数的设计要求

散列函数需要满足:
1 设计简单高效,计算时间短。
2 生成的值要随机且均匀。

数据分析法设计散列函数。例如学号复杂的例子,我们分析学号的特征设计散列函数。
 key为字符串类型的可以使用字符串进位相加的方法,然后再跟散列表大小取余。例如"nice"的哈希值为:

hash("nice")=(("n" - "a") * 26*26*26 + ("i" - "a")*26*26 + ("c" - "a")*26+ ("e"-"a")) / 78978

还有其他设计函数的方法。例如:直接寻址法、平方取中法、随机数法等。

4.2动态装载因子

散列表中元素个数m与散列表长度的比值就是装在因子:

装 载 因 子 = 元 素 个 数 长 度 装载因子=\dfrac{元素个数}{长度} =
 散列表中随着数据的插入和删除状态因子发生变化,成为动态装载因子。

4.3 扩容、缩容

当加载因子不断变大的时候,发生散列冲突的概率就会增加,操作就会变慢。这时候可以像动态数组一样做扩容。
 一般散列表扩容会在在当前长度的基础上再扩一倍。扩容之前装载因子是0.8,扩容之后就是0.4。散列表扩容与数组扩容不同的地方是扩容之后,因为散列表大小发生变化,散列值也可能发生变化。例如原来key=6的元素,哈希值是1,扩容后哈希值是10。
 支持动态扩容散列表的插入操作的平均时间复杂度,按照摊还分析法是O(1)。
 当散列表随着删除操作,装载因子会越来越小。如果对内存不敏感,浪费一些也可以,可以不采取操作。如果要求内存尽可能小,可以对散列表缩容。
 装载因子需要权衡时间和空间。操作时间优先,可以允许浪费一定的空间。

4.4避免低效扩容

低效扩容是因为一次扩容,重新计算哈希值,搬移数据导致的。如果原来的数据有1G大小,这一次搬移操作就很费劲。
 我们可以采集的策略是将原始n个数据的搬移工作分配到n次插入操作中。每次插入只将原来散列表中的一个值搬移到新散列表。这样在任何时候插入操作的时间复杂度都是O(1)。
 在这里插入图片描述

5 如何选择冲突解决方法

开放寻址法
 优点:底层结构是数组,可以充分利用CPU缓存加快查询速度;利于序列化。
 缺点:删除数据需要标记,比较麻烦;所有数据在同一个数组,冲突的代价更大,所以会浪费更多的内存空间。
 当数据量比较小、装载因子比较小的时候,选择开放寻址法。例如Java的ThreadLocalMap。

链表法
 优点:内存利用率比开放寻址法高,因为节点可以在需要的时候才创建而不是提前创建。
 可以容忍装载因子大于1。当装载因子大于1,查找速度与每个槽对应的链表长度有关,但是比全链表查询效果要高。我们可以将链表改为跳表或者红黑树,这样即便出现散列冲突的极端情况,时间复杂度也是O(logn)。
 当数据对象比较大、数据量比较大的时候使用链表法。

6工业级散列表举例分析

6.1 Java的HashMap

1 初始大小16,可以指定。
2 装载因子0.75,当装载因子大于0.75的时候动态扩容。
3 冲突解决方法:链表法。当链表长度超过8,使用红黑树。
4 hash函数:

int hash(Object key) {
    int h = key.hashCode();
    return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}

hashcode返回的是key的hash code。

6.2 工业级散列表应该具有的特征

1 支持快速查询、插入、删除;
2 内存占用合理;
3 性能稳定,在极端情况下也不会出现速度不可接受的情况。

6.3 工业级散列表设计思路

1 散列函数设计合适。
2 装载因子设置合理,不过多浪费空间,动态扩容策略合适。
2 散列冲突解决策略。

7散列表的应用

7.1 word中错误单词提示功能

把英文单词加载到内存中,用散列表存储。当用户输入一个词的时候,在散列表中查找是否存在,
常用因为单词20万,假设单词平均长度10个字母。一个单词占用10个字节的内存,所有单词加载大约是2M内存。放大10倍20M。内存可用。

7.2 假设有10条url访问记录,怎么按照访问次数给url排序

对url取哈希值,在散列表存储每个hash值的访问次数。最后再排序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值