复杂度:O(logn);
跳表的更新:插入数据时,可以选择将这个数据插入到部分索引中,可以选择一个随机函数,产生随机数K,边将索引添加到第一到第K级索引中。
Redis为何选择跳表来实现有序集合?和红黑树有什么区别?
对于插入、删除、查找两者的复杂度是一样的,但是对于按照区间查找来说,红黑树效率没有跳表高。
对于区间查找,跳表可以做到O(logn)的复杂度来定位起点和终点,然后顺序遍历即可。
跳表实现相对简单、理解容易,也可以改变索引构建策略,平衡执行效率和内存消耗。
散列函数:MD5、SHA、CRC等哈希算法。
散列冲突
如何设计散列函数?
手机号码取后几位为散列值、Word单词拼写检查将每个字母ASCII码值进位相加,再根据散列表大小求余、取模。直接寻址法、平方取中、折叠法、随机数法。
装载因子过大怎么办?
动态扩容,动态扩容需要重新通过散列函数计算数据的存储位置,所以会导致在动态扩容的时候时间复杂度变为O(n),为了避免动态扩容时导致耗时过长,可以选择将扩容后的数据迁移操作分散开,每次插入只迁移部分数据,这样平均下来每次时间复杂度也是O(1)。
查找时可以在新旧两个散列表中都进行查找。
开放寻址法:要求装载因子上限不能太大,所以比较浪费内存。数据量较小,装载因子小的时候,适合开放寻址法;
链表法:内存利用率高。链表要存储指针,存储较小对象时,比较消耗内存,另外链表中的节点时零散的,对CPU的缓存不友好。可以对链表进行改造为其他高效的数据结构,如跳表、红黑树等。所以链表法比较适合存储大对象,可以支持更多的优化策略。
如何设计一个工业级的散列表?工业级的散列表有哪些特征?
设计思路:
LRU缓存淘汰算法
Redis有序集合
有序集合成员有两个重要属性,key(键值)和score(分值)。所以一般需要根据key查找数据,通过score进行范围查找。
用户ID和积分score,可以按照score从小到大构建一个跳表。这样按照score进行范围查找。另外使用key构建一个散列表,查找成员对象复杂度变成O(1)。
Java LinkedhashMap
LinkedHashMap 是通过双向链表和散列表这两种数据结构组合实现的。LinkedHashMap 中的“Linked”实际上是指的是双向链表,并非指用链表法解决散列冲突。
思考题
假设猎聘网有 10 万名猎头,每个猎头都可以通过做任务(比如发布职位)来积累积分,然后通过积分来下载简历。假设你是猎聘网的一名工程师,如何在内存中存储这 10 万个猎头 ID 和积分信息,让它能够支持这样几个操作:
按照猎头ID组件散列表,这样按照ID快速查找、删除,另外按照积分进行排序组件跳表,这样可以进行区间和范围查找
哈希函数的应用
如何保存用户的密码?
如果只是将用户的密码进行加密后保存,黑客也可以使用字典攻击进行破解。因此需要维护一个常用密码的字典表,将字典中的密码进行哈希计算再哈希,针对字典攻击,可以引入盐(salt),和用户密码组合在一起,增加密码的复杂度。另外除了hash+salt,大多公司采用无论密码长度多少,计算字符串的hash函数时间都固定,进一步减少风险。
区块链的设计思路
区块链每个区块分为两部分:区块头和区块体,区块头保存自己区块体和上一个区块体的哈希值。所以只要任意一个区块被修改过,后面所有区块的哈希值就不对了。
五、哈希算法在分布式系统中的应用