重温算法Day12:散列表

散列表来源于数组,它借助散列函数对数组这种数据结构进行扩展,利用的是数组支持按照下标随机访问元素的特性。
散列表两个核心问题是散列函数设计散列冲突解决。散列冲突有两种常用的解决方法,开放寻址法(线性探测、二次探测、双重散列)和链表法(常用)
散列函数设计的好坏决定了散列冲突的概率,也就决定散列表的性能。

链表法是一种更加常用的散列冲突解决办法
每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中
插入数据:当插入的时候,我们需要通过散列函数计算出对应的散列槽位,将其插入到对应的链表中即可,所以插入的时间复杂度为O(1)
查找或删除数据:当查找、删除一个元素时,通过散列函数计算对应的槽,然后遍历链表查找或删除。对于散列比较均匀的散列函数,链表的节点个数k=n/m,其中n表示散列表中数据的个数,m表示散列表中槽的个数,所以是时间复杂度为O(k)

 

问题:

1. 假设我们有 10 万条 URL 访问日志,如何按照访问次数给 URL 排序?
(1)遍历 10 万条数据,以 URL 为 key,访问次数为 value,存入散列表,同时记录下访问次数的最大值 K,时间复杂度 O(N)。
(2)如果 K 不是很大,可以使用桶排序,时间复杂度 O(N)。如果 K 非常大(比如大于 10 万),就使用快速排序,复杂度 O(NlogN)。

2. 有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?
(1)以第一个字符串数组构建散列表,key 为字符串,value 为出现次数。
(2)遍历第二个字符串数组,以字符串为 key 在散列表中查找,如果 value 大于零,说明存在相同字符串。时间复杂度 O(N)。

3.word文档如何实现拼错查找功能?
(1)常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间,就算放大 10 倍也就是 20MB。
(2)这个大小完全可以放在内存里面。所以我们可以用散列表来存储整个英文单词词典。
(3)当用户输入某个英文单词时,我们拿用户输入的单词去散列表中查找。如果查到,则说明拼写正确;如果没有查到,则说明拼写可能有误,给予提示。

 

散列表碰撞攻击的基本原理
(1)通过精心构造的数据,使得所有的数据经过散列函数之后,都散列到同一个槽里。
(2)如果我们使用的是基于链表的冲突解决方法,那这个时候,散列表就会退化为链表,查询的时间复杂度就从 O(1) 急剧退化为 O(n)。
(3)对于10万的数据,如果之前运行 100 次查询只需要 0.1 秒,那现在就需要 1 万秒。这样就有可能因为查询操作消耗大量 CPU 或者线程资源,导致系统无法响应其他请求,从而达到拒绝服务攻击(DoS)的目的。


如何设计散列函数
(1)不能太复杂(2)尽可能随机均匀分布

例如:word单词检查,将单词中每个字母的ASCll 码值“进位”相加,然后再跟散列表的大小求余、取模,作为散列值
hash("nice")=(("n" - "a") * 26*26*26 + ("i" - "a")*26*26 + ("c" - "a")*26+ ("e"-"a")) / 78978

插入一个数据,最好情况下,不需要扩容,最好时间复杂度是 O(1)。最坏情况下,散列表装载因子过高,启动扩容,我们需要重新申请内存空间,重新计算哈希位置,并且搬移数据,所以时间复杂度是 O(n)。用摊还分析法,均摊情况下,时间复杂度接近最好情况,就是 O(1)。

扩容:
(1)我们可以将扩容操作穿插在插入操作的过程中,分批完成
(2)当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中
(3)当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表
(4)每次插入一个数据到散列表,我们都重复上面的过程。

对于查询操作,为了兼容了新、老散列表中的数据,我们先从新散列表中查找,如果没有找到,再去老的散列表中查找

基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树或跳表代替链表,也能避免碰撞冲突。

package main

import (
	"crypto/md5"
	"crypto/sha1"
	"fmt"
)

func main() {
	TestString := "Hi, luffy!"

	Md5Inst := md5.New()
	Md5Inst.Write([]byte(TestString))
	Result := Md5Inst.Sum([]byte(""))
	fmt.Printf("%x\n\n", Result)

	Sha1Inst := sha1.New()
	Sha1Inst.Write([]byte(TestString))
	Result = Sha1Inst.Sum([]byte(""))
	fmt.Printf("%x\n\n", Result)
}

ac90afff2f35a56d02fae7ffa1627124

8fd419b1471bbbd57f3d53e60b84dae050982d22

散列表和链表一起使用
LRU 缓存淘汰算法:维护一个按照访问时间从大到小有序排列的链表结构。因为缓存大小有限,当缓存空间不够,需要淘汰一个数据的时候,我们就直接将链表头部的结点删除。

一个缓存(cache)系统主要包含下面这几个操作:
往缓存中添加一个数据;从缓存中删除一个数据;在缓存中查找一个数据。

如果将散列表和链表两种数据结构组合使用,可以将这三个操作的时间复杂度都降低到 O(1)
使用双向链表存储数据,链表中的每个结点处理存储数据(data)、前驱指针(prev)、后继指针(next)之外,还新增了一个特殊的字段 hnext。
前驱和后继指针是为了将结点串在双向链表中,hnext 指针是为了将结点串在散列表的拉链中

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值