散列表(下):为什么散列表和链表经常会一起使用?

 【思考题】:散列表和链表都是如何组合起来使用的?散列表和链表为什么经常放在一起使用?

1、LRU缓存淘汰策略

        借助散列表,可以把LRU缓存淘汰算法的时间复杂度降为O(1)。但之前讲过使用链表来实现LRU缓存淘汰算法的时间复杂度是O(n)。维护一个按照访问时间从小到大有序排列的链表结构,因为缓存大小有限,当缓存空间不够,需要淘汰一个数据的时候,直接将链表头部的结点删除。

       当要缓存某个数据的时候,先在链表中查找这个数据,如果没找到就直接把数据放在链表的尾部,如果找到了,就把它移动到链表的尾部。因为查找数据需要遍历链表,所以时间复杂度是O(n)。

       一个缓存系统主要包括下面几个操作:

  • 往缓存中添加一个数据
  • 从缓存中删除一个数据
  • 在缓存中查找一个数据

        这三个操作都要涉及“查找”操作,时间复杂度是O(n),如果将散列表和链表结合使用,可以将这三个操作的时间复杂度都降低为O(1)。具体结构如下图:

       

         从上面的我们可以看到,散列表是借助双向链表来存储数据,链表中每个结点处有存储数据(data),前驱指针(prev),后继指针(next),特殊指针(hnext)。

        因为散列表是借助链表来解决散列冲突的,所以每个结点会在两条链中。一个是双向链表,另一个是散列表中的拉链。前驱和后继指针是为了将结点串在双向链表中,hnext指针是为了将结点串在散列表的拉链当中。

       在这种散列表和双向链表组合的存储结构下,前面讲到的三个缓存操作的时间复杂度就是O(1)。

       在缓存中查找一个数据。前面讲过在散列表中查找一个数据的时间复杂度接近O(1),所以通过散列表,可以快速在缓存中找到一个数据。找到这个数据以后,需要将它移动到双向链表的尾部。

       在缓存中删除一个数据。需要找到数据所在的结点,然后将结点删除。借助散列表,我们可以在O(1)的时间复杂度里找到要删除的结点。因为我们用的是双向链表,双向链表可以通过前驱指针O(1)时间复杂度获取前驱结点,所以在双向链表中,删除结点只需要O(1)的时间复杂度。

      在缓存中添加一个数据。如果要添加的数据已经在缓存中,将这个数据移动到双向链表的尾部;如果缓存已经满了,则将双向链表头部的结点删除,然后再将数据放到链表的尾部;如果没有满,就把数据直接放到双向链表的尾部。整个过程涉及的查找操作,都可以通过散列表来完成。时间复杂度是O(1)。

 

2、Redis有序集合

        在讲到跳表的时候,有说过Redis有序集合不仅使用了跳表,还使用了散列表。在有序集合中,每个成员有两个重要的属性,key(键值)和score(分值)。我们不仅会通过score来查找数据,也会通过key来查找数据。

       细化一下Redis 有序集合的操作:

  • 添加一个成员对象
  • 按照键值来删除一个成员对象
  • 按照键值来查找一个成员对象
  • 按照分值区间,比如查找积分在[100,356]之间的成员对象
  • 按照分值从小到大排序成员变量

     如果仅仅按照分值将成员对象组织成跳表结构,按照键值来删除、查找成员对象就会很慢,所以可以再按照键值构建一个散列表,这样按照key来删除、查找一个成员对象的时间复杂度就变为O(1)。

 

3、为什么散列表和链表会经常一起使用?

        散列表虽然支持高效的数据插入、删除、查找操作,但是散列表中的数据都是通过散列函数打乱之后无规律存储的,也就无法按照某种顺序快速遍历数据。如果希望按照某种顺序遍历散列表中的数据,需要将散列表中的数据拷贝到数组当中,然后排序,再遍历。

        因为散列表是动态的数据结构,不停的有数据的插入、删除,所以我们希望按照顺序遍历散列表的时候,都需要先排序,那效率势必会很低,为了解决这个问题,所以就会经常讲散列表和链表结合在一起使用。

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值