数据结构与算法(为什么散列表经常和链表一起使用)

1>回顾基于链表的lru淘汰缓存

         基于链表实现的lru缓存淘汰算法.首先要维护一张基于时间大小排序的链表,因为缓存的空间有限.当我们需要缓存的时候,链表的空间有限,我们需要从链表的尾部淘汰掉一个数据,

        当我们需要缓存一个数据的时候,我们需要遍历这个链表,如果该链表切切实实的不存在该数据,我们需要将把该数据加到链表的尾部,如果链表中已经存在该数据,我们只需要将该数据放到链表的尾部部即可,总上所述,我们基于链表实现lru缓存淘汰算法,都需要去遍历该链表,时间复杂度为O(n)

 2>总结,一个缓存需要包括以下操作

          a:向缓存中添加一个数据

          b:向缓存中删除一个数据

         c:像缓存中查找一个元素

 以上这3种操作都需要去遍历这个链表,遍历链表的时间复杂度为O(n),如果我们结合散列表和链表结合使用,需要的复杂度仅仅是O(1),在效率上有很大的提升.

     解释:我们使用双向链表存储数据,每个节点除了数据本身,还有指向前驱节点的指针和指向后继节点的指针,初次之外还多了一个hnext,每个节点都有是通过链表解决hash冲突的,所以每个节点都会在2个拉链中,一个链指的是我们的双向链表,另一个链是散列表的拉链,前驱和后继指针是为了将节点串联在双向链表中,hnext指针是为了将节点串联在散列表中,

   问题:他是如何做到时间复杂度为O(1)的呢?

           查找数据:基于散列表的查找时间复杂度接近O(1),所以我们在缓存中查找一个数据,当找到数据之后,我们需要把该数据放到双向链表的尾部.

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

         添加数据:添加数据稍微有点麻烦,我们得 先看该数据是否在缓存中,如果已经在其中,我们需要将该数据的节点移动到双向链表的尾部,如果不在缓存中,还得看缓存是否已满,如果满了,将双向链表的头部节点删除,将该数据放在双向链表的尾部,如果没有满,就直接放在双向链表的尾部.    

     这样我们通过散列表和双向链表的结合,实现的lru缓存淘汰算法更加高效的性能,

  3>讨论讨论java中的Linkedhashmap

               linkedhashmap:底层是维护了散列表和双向链表来进行维护的,linked代表的是双向链表,并非指的是用链表法莱解决hash冲突的.

        我们看代码:

    

HashMap<Integer,Integer> map = new LinkedHashMap<>();
 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:map.entrySet()){
   System.out.println(e.getKey());
}

这里的key的打印顺序是3,1,5,2,他们都是经过hash函数,打乱进行存储的,为何打印的顺序是插入顺序呢?

   每次调用put操作,都会把数据放到链表的尾部,所以每次put后的结构应该是这样的

放开注释:  在进行第9行代码的时候,再次将键值为3放到linkedhashmap中,首先去检查链表,发现键值为3已经存在,需要将(3,11)删除,将(3,26)放到该链表的尾部

 

当第10行代码的时候,我们需要去访问键值为5的元素,发现该数据已经存在,需要将键值为5的数据放到链表的尾部

所以此时的打印顺序为1,2,3,5,这种事基于访问时间进行打印的,这种策略本身就是lru缓存淘汰策略思路

  总结:linkedhashmap可以基于插入顺序进行打印,也可以基于访问时间进行打印,基于访问时间进行打印,本身就是lru思想,

4>回到主题:散列表支持高效数据的插入,删除,操作,经过hash函数都是随机进行存储的,在进行打印是没有规律可找的,他无法支持按照一种顺序去打印数据,如果需要按照一种顺序打印散列表中的数据,我们需要将散列表中的数据存储在数组中进行排序打印才是我们想要的数据,如何是需要频繁的插入删除散列表中的数据,也需要频繁的放到数组中排序,这将是性能的消耗,基于这种问题,更希望散列表结合链表来使用,既可以解决hash冲突,又可以按照一种顺序去打印我们的数据,性能更加高效,谢谢.

没有更多推荐了,返回首页