当散列表赶上链表会发生什么呢?

      在数据结构中,散列表和链表常常会组合在一块使用,若是你对java很熟悉,你会发现LinkedHashMap这样一个经常使用的容器,也把散列表和链表组合起来使用。那散列表和链表是如何组合使用的,他们组合在一块儿能碰撞出什么火花,请跟随个人脚步,一块儿一探究竟。java

     咱们先思考这么一个问题,如何使用链表来实现LRU缓存呢?若是对LRU不熟,能够看这篇文章。页面置换算法你学会了吗?程序员

     咱们能够维护一个有序的单链表,越靠近链表尾部的结点是越早访问的。当有一个新的数据被访问时,咱们从链表头开始顺序遍历。遍历的结果有两种状况。算法

  1. 若是此数据以前就已经被缓存在链表中,咱们遍历获得这个数据对应的结点,而后将其从这个位置移动到链表的头部。缓存

  2. 若是此数据不在链表中,又会分为两种状况。若是此时缓存链表没有满,咱们直接将该结点插入链表头部。若是此时缓存链表已经满了,咱们从链表尾部删除一个结点,而后将新的数据结点插入到链表头部。数据结构

      这样咱们就用链表实现了一个LRU缓存,咱们接下来分析一下缓存访问的时间复杂度。由于无论缓存有没有满,咱们都须要遍历一遍链表,因此基于链表实现的LRU缓存,缓存访问的时间复杂度是O(n)。spa

 

 

        那有没有什么方法能够减低时间复杂度呢?咱们先来分析一下缓存的经常使用操做。对于一个缓存来讲,主要涉及如下三种操做:指针

  1. 往缓存添加一个元素。blog

  2. 从缓存中删除一个元素。get

  3. 在缓存中查找一个元素。原型

 

      这三个操做都会涉及到查找的操做,若是单纯的使用链表,时间复杂度只能是O(n)。你们都知道散列表的查找操做是O(1),那咱们能不能把散列表和链表结合起来使用,将缓存的这三个经常使用操做的时间复杂度减低到O(1)呢?答案是确定的,咱们来看一下他们是如何组合在一块儿的。

       如图所示,咱们使用双向链表来存储数据,链表中的每一个结点除了数据(data)、前驱指针(pre)、后继指针(next)以外,还新增了一个特殊的字段 hnext。这个hnext有什么做用呢?由于咱们的散列表是经过链表法解决散列冲突的,因此每一个结点会在两条链中。一个链是刚刚咱们提到的双向链表,另外一个链是散列表中的拉链。前驱和后继指针是为了将结点串在双向链表中,hnext 指针是为了将结点串在散列表的拉链中。

      了解了这个散列表和双向链表的组合存储结构以后,咱们再来看,前面讲到的缓存的三个操做,是如何作到时间复杂度是 O(1) 的?

       首先,咱们来看如何查找一个数据。咱们前面讲过,散列表中查找数据的时间复杂度接近 O(1),因此经过散列表,咱们能够很快地在缓存中找到一个数据。当找到数据以后,咱们还须要将它移动到双向链表的尾部。

       其次,咱们来看如何删除一个数据。咱们须要找到数据所在的结点,而后将结点删除。借助散列表,咱们能够在 O(1) 时间复杂度里找到要删除的结点。由于咱们的链表是双向链表,双向链表能够经过前驱指针 O(1) 时间复杂度获取前驱结点,因此在双向链表中,删除结点只须要 O(1) 的时间复杂度。

      最后,咱们来看如何添加一个数据。添加数据到缓存稍微有点麻烦,咱们须要先看这个数据是否已经在缓存中。若是已经在其中,须要将其移动到双向链表的头部;若是不在其中,还要看缓存有没有满。若是满了,则将双向链表尾部的结点删除,而后再将数据放到链表的头部;若是没有满,就直接将数据放到链表的头部。这整个过程涉及的查找操做均可以经过散列表来完成。

       其余的操做,好比删除头结点、链表尾部插入数据等,均可以在 O(1) 的时间复杂度内完成。因此,这三个操做的时间复杂度都是 O(1)。至此,咱们就经过散列表和双向链表的组合使用,实现了一个高效的、支持 LRU 缓存淘汰算法的缓存系统原型。

       因此,能够经过散列表和链表结合的方式,实现一个时间复杂度为O(1)的LRU缓存。

       更多硬核知识,请关注公众号”程序员学长"。

相关文章
相关标签/搜索