转载部分

数据结构与算法之美-散列表(下)

2021-05-21  本文已影响0人  沉江小鱼

前言:本篇文章只是记录王争的数据结构与算法之美的学习笔记,写下来能强迫自己系统的再过一遍,加深理解。这门课以实际开发中遇到的问题为例,引入解决问题涉及到的的数据结构和算法,但不会讲的太细,最好结合一本实体书进行学习。

散列表和链表经常会被放在一起使用,这是为什么呢?它们是如何组合起来使用的呢?

1. 散列表和链表为什么经常组合使用?

散列表虽然支持非常高效的插入、删除、查找操作,但是散列表中的数据都是散列函数打乱之后无规律存储的,想要顺序遍历的话肯定不行,所以我们将散列表和链表(跳表)结合在一起使用,这样就可以按顺序遍历散列表中的数据了。

2. 如何组合使用

2.1 LRU 缓存淘汰算法

单链表实现 LRU 缓存淘汰算法:
当要缓存某个数据的时候,需要现在链表中查找这个数据,如果没有找到,则将数据放到链表尾部;
如果找到了,则将其移动到链表尾部。这样查找数据需要遍历链表,所以单纯使用链表实现的 LRU 缓存淘汰算法的时间复杂度就是 O(n)。

一个缓存系统主要包含以下操作:

这三个操作都涉及到“查找”操作,如果只使用单链表的话,时间复杂度只能是 O(n),所以我们可以使用散列表查询时间复杂度为O(1)的特点,将散列表和链表两种数据结构组合使用,可以将这三个操作时间复杂度都降低为 O(1),如下图:


image.png

可以看到上图中时散列表 + 双向链表的组合,链表中的每个结点有存储数据 data、前驱指针 prev、后继指针 next、hnext指针。双向链表的 prev 和 next 指针时纵向指针,维护的是数据缓存的时间线,将结点串在双向链表中,hnext指针时为了将结点串在散列表的拉链中。

这里可能需要维护一个双向链表的尾指针,这样才能快速的将结点移动到链表的尾部。

YYKit中的 YYMemoryCache 就是使用这种结构实现的,有时间要去看下源码,学习一下。

2.2 Redis 有序集合

比如用户积分排行榜功能,我们可以通过用户 ID 查找积分信息,也可以通过积分区间来查找用户 ID 或者姓名信息。这里包含 ID、姓名和积分的用户信息,就是成员对象,用户 ID 就是 key,积分就是 score。

细化 Redis 有序集合的操作:

如果只是按照分值score将成员对象组织成跳表结构,那按照键值key 来删除、查询成员对象就会很慢,所以可以再按照键值key构建一个散列表(无序的散列表,适合按照 key 值查询,效率更高),这样按照 key 来删除,查找一个成员对象的时间复杂度就变成了 O(1),同时借助跳表结构,其他操作也很高效。

2.3 LinkedHashMap

LinkedHashMap是通过散列表 + 双向链表实现的,不仅支持按照插入顺序遍历数据,还支持按照访问顺序遍历数据。


// 10是初始大小,0.75是装载因子,true是表示按照访问时间排序
HashMap<Integer, Integer> m = new LinkedHashMap<>(10, 0.75f, true);
m.put(3, 11);
m.put(1, 12);
m.put(5, 23);
m.put(2, 22);

m.put(3, 26);
m.get(5);

for (Map.Entry e : m.entrySet()) {
  System.out.println(e.getKey());
}

打印结果是 1 2 3 5。

每次 put()函数执行时,都会将数据添加到链表的尾部,所以,前四个操作完成之后,链表的数据是下面这样的:


image.png

在第 8 行代码中,再次将键值为 3 的数据放入到 LinkedHashMap 的时候,会先查找这个键值是否已经有了,然后,再将已经存在的 (3,11) 删除,并且将新的 (3,26) 放到链表的尾部。所以,这个时候链表中的数据就是下面这样:


image.png

当第 9 行代码访问到 key 为 5 的数据的时候,我们将被访问到的数据移动到链表的尾部。所以,第 9 行代码之后,链表中的数据是下面这样:


image.png

最后打印出来的数据就是 1 2 3 5 了,LinkedHashMap 本身就是一个支持 LRU 缓存淘汰策略的缓存系统,实现原理是一模一样的。

LinkedHashMap 是通过双向链表 + 散列表这两种数据结构组合实现的。Linked 指的是双向链表。

上一篇下一篇

猜你喜欢

热点阅读