lru

打算从基础开始复习数据结构了,这篇文章就首先来说说链表(Linked List)这个数据结构。学习链表的作用与应用场景?

首先我们应该都知道啥叫链表了,查找慢,插入快,现在我们主要说一种链表的常用场景,也就是LRU缓存淘汰算法。

缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中经常会用到,最让我们熟悉的其实就是Redis了。Redis在之前的文章中也已经介绍过了,有想去稍微看看的可以去点击我的Redis版块去看。

缓存的大小有限,在缓存被用满的时候,哪些数据应该被清理出去,哪些数据应该被保留?这就需要缓存淘汰策略来决定了。常见的策略有FIFO(先进先出),LFU(最少使用),LRU(最近最少使用)。

回到正题,我们的问题就是:如何用链表来实现LRU缓存淘汰策略?

链表的多种结构

相比于顺序表(数组),链表是一种稍微复杂一点的数据结构。对于初学者来说,掌握起来也要比数组稍微难一点。这两种是非常常用的数据结构,下面就来介绍一下。

我们先从底层的存储结构上来看一看。

在这里插入图片描述

从图中我们直观的看出,数组需要一块连续的内存空间存储,对内存的要求比较高,如果内存空间不连续,往往即使内存足够大也会失败。

而链表相反,它不需要连续的内存空间,通过“指针”将一组零散的内存块 串联起来使用,所以如果我们申请的都是100MB大小的链表,根本不会有问题。

我们刚刚讲到,链表通过指针将零散的内存块串联在一起。其中,我们把内存块成为链表的结点。为了将所有的结点串在一起,每个节点的数据串在一起,所以每个链表的结点,不光要存储当前结点的数据以外,还要存储下一个结点的地址

在这里插入图片描述
从上面的链表的图中可以看出,只有两个节点是比较特殊的,第一个结点叫做头结点,第二个结点叫做尾结点

头结点就是用来记录链表的基地址,有了它,我们就可以遍历到整条链表。
尾结点的特殊地方在于,指针指向的是空地址NULL,表示这是链表的最后一个节点。

和顺序表相同,链表也支持数据的查找、插入和删除操作。

我们知道,在进行数组的插入、删除操作的时候,为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是O(n)。而在链表中插入或者删除一个数据,由于链表的存储本身就不是连续的,所以只需要考虑相邻结点的指针改变,对应的时间复杂度为O(1)。

在这里插入图片描述

但是,如果要是访问某一个元素,就会很吃力了,因为内存不是连续存储的,所以我们需要从头开始遍历找到要找的元素,时间复杂度也就是O(n)

单链表结束了,下面来介绍一下循环链表双向链表

循环链表

循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在于尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。

而循环链表的尾结点指向头结点,代表成环。

在这里插入图片描述

和单链表相比,循环链表的优点是从链尾到链头比较方便,不用O(n)遍历了。

双向链表

双向链表比单链表多了一个前驱结点pre,指向前一个结点。

在这里插入图片描述

从图中可以看出,双向链表需要额外的两个空间存储后继结点和前驱节点的地址所以如果存储同样多的数据,双向链表要比单链表占用更多的数据,而唯一多出来的特点就是双向遍历。

从结构上说,双向链表和单向链表一样插入和删除都很快。

但从某些情况上的插入和删除说,双向链表比单向链表更加高效与简单

首先来看删除操作

在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:

  • 删除结点中“值等于某个给定值”的结点;

对于第一种情况,不管是单链表还是双向链表,为了找到值等于给定值的结点,都需要从头结点开始一个个依次遍历。

单纯删除是O(1),遍历是O(n) ,根据时间复杂度的加法法则,总时间复杂度为O(n)

  • 删除给定指针指向的结点。

对于第二种情况,我们如果要删除一个结点,就要直到指向他的节点,让其指向下一个结点,这样双向链表就比单链表有优势多了。

同理,只要是需要牵扯到上一个节点任务的操作,双向链表都要比单链表要高效。可以多了解LinkedHashMap这个容器。

实际上,还有一个更加重要的知识点需要掌握,那就是用空间换时间的设计思想。前提是你的内存空间充足,那么我们就可以选择空间复杂度高,时间复杂度低的操作。

链表vs数组

在这里插入图片描述

数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果没有足够的连续空间,就会OOM所以声明空间的时候很浪费时间。

这时你会说ArrayList不是动态的吗,然而我们也知道,他在分配空间的时候,都是先让你往里面add,到了极限空间的时候,再插入,就会申请一片区域给你扩容,这时很浪费资源的。

除了上面说的这些以外,如果你的代码对于内存的连续性和容量有高标准,那是可以是使用数组的。因为链表需要消耗额外的存储空间存储下一个结点的指针,所以内存消耗会翻倍。而且,频繁插入,更是会产生空间碎片,频繁的GC会让吞吐量下降,这也是很不好的。

所以选择技术要酌情。

如何实现LRU缓存淘汰算法

假如我们有一个有序单链表,越靠近链表尾部的结点就是越早之前访问的。当有一个新的数据被访问的时候,我们就从链表头开始顺序遍历链表。

  1. 如果此数据的缓存在链表中,我们遍历得到这个数据对应的结点,并且将其从原来的位置删除,然后再插入到链表的头部。
  2. 如果此数据没有在缓存链表中,又可以分为两种情况:
  • 缓存未满,头插法
  • 缓存已满,尾结点删除,然后头插

这样就实现了一个LRU缓存。

class LRUCache {

    Map<Integer,Integer> map ;
    int capacity;
    public LRUCache(int capacity) {
        map = new LinkedHashMap<>(capacity);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        if(!map.containsKey(key)){
            return -1;
        }
        int value = map.get(key);
        map.remove(key);
        map.put(key,value);
        return value;
    }
    
    public void put(int key, int value) {
        if(map.containsKey(key)){
            map.remove(key);
            map.put(key,value);
        }else{
            if(map.size()==capacity){
                map.remove(map.keySet().iterator().next());
            }
            map.put(key,value);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值