经典内存算法——LRU、LFU

姓名:谭旭东

学号:19011210016

嵌牛导读:经典算法——LRU、LFU

嵌牛鼻子:Java

嵌牛提问:内存排除算法

转载链接:http://182.92.190.128/archives/lrulfu(我的博客)


# 一、LRU算法

## 1. 题目描述

- Least Recently Used——最近最少使用

- 淘汰机制:按照上一次使用时间进行淘汰

- 题目出处:[面试题 16.25. LRU 缓存](https://leetcode-cn.com/problems/lru-cache-lcci/)

- 关键实现:

- capacity:容量,大于容量选择最久未使用资源淘汰

- put():增加新资源

- get():获取/使用资源,会更新资源使用状况

## 2. 思路

- 使用双端队列,久未使用的资源放在队首,新加入/新使用的资源放在队尾

- 淘汰时删除队首元素即可

- 数据结构

- HashMap:用于查询

- LinkedList:用于构建链表

## 3. 代码

### (1)使用JAVA中LinkedHashMap

- 细节:

- 删除元素时,使用迭代器获取队首元素

```java

// 1. 使用JAVA自带API:LinkedHashMap

class LRUCache {

    int capacity;

    Map<Integer, Integer> map;

    public LRUCache(int capacity) {

        this.capacity = capacity;

        map = new LinkedHashMap<>();

    }

    public int get(int key) {

        if (!map.containsKey(key)) {

            return -1;

        }

        // 先删除旧的位置,再放入新位置

        Integer value = 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);

            return;

        }

        map.put(key, value);

        // 超出capacity,删除最久没用的,利用迭代器删除第一个

        if (map.size() > capacity) {

            map.remove(map.entrySet().iterator().next().getKey());

        }

    }

}

```

### (2)自建数据结构:HashMap + LinkedList

- 代码细节:

- ① moveToTail功能单独封装,put和get中都需要调用

- ② 在put中调用get实现复用

```java

// 2. 自己构造 LinkedHashMap + LinkedList(双链表)

class LRUCache2 {

    private int capacity;

    private Map<Integer, ListNode> map;

    private ListNode head;

    private ListNode tail;

    public LRUCache2(int capacity) {

        this.capacity = capacity;

        this.map = new HashMap<>();

        this.head = new ListNode(-1, -1);

        this.tail = new ListNode(-1, -1);

        head.next = tail;

        tail.pre = head;

    }

    public int get(int key) {

        if (!map.containsKey(key))

            return -1;

        ListNode node = map.get(key);

        deleteNode(node);  // 链表中删除节点

        moveToTail(node);  // 节点移动到链表尾部

        return node.val;

    }

    public void put(int key, int value) {

        if (get(key) != -1) {      // 已经存在,改变值:在get内部会移动到尾部

            map.get(key).val = value;

            return;

        }

        ListNode node = new ListNode(key, value);

        map.put(key, node);

        moveToTail(node);      // 放到尾部去

        if (map.size() > capacity) {

            map.remove(head.next.key);

            deleteNode(head.next);

        }

    }

    private void moveToTail(ListNode node) {

        node.next = tail;

        node.pre = tail.pre;

        tail.pre.next = node;

        tail.pre = node;

    }

    private void deleteNode(ListNode node) {

        node.pre.next = node.next;

        node.next.pre = node.pre;

    }

    class ListNode {

        int key;

        int val;

        ListNode pre;

        ListNode next;

        public ListNode(int key, int val) {

            this.key = key;

            this.val = val;

            this.pre = null;

            this.next = null;

        }

    }

}

```

# 二、LFU算法

## 1. 题目描述

- Least Frequently Used——最近使用频次最少

- 淘汰机制:

- ① 按照使用次数

- ② 如果使用次数相同,按照使用时间淘汰

- 题目出处:[LeetCode.460. LFU 缓存](https://leetcode-cn.com/problems/lfu-cache/)

- 关键实现:

- put()

- get()

## 2. 思路

- 数据结构:

- ① 第一张HashMap,为了查询资源

![image.png](http://182.92.190.128/upload/2021/08/image-e4b03c91719e4d8688c06b19bc2cfb06.png)

- ② 第二张HashMap,key值按照频次从小到大,value值为双向链表,保存了同频次的节点

![image.png](http://182.92.190.128/upload/2021/08/image-1edc13775f8d411596fac41252681de3.png)

## 3. 代码

```java

import java.util.HashMap;

import java.util.Map;

/**

*    自定义的LFU缓存类

*/

public class LFUCache {

    /**

    *    双链表中的链表节点对象

    */

    protected static class Node{

        //对应输入的key

        private final int key;


        //对应输入的value

        private int value;


        //被访问的频率

        private int freq;


        //指向前一个节点的指针

        protected Node pre;


        //指向后一个节点的指针

        protected Node next;


        public Node(int key, int value, int freq) {

            this.key = key;

            this.value = value;

            this.freq = freq;

        }


        public Node(int key, int value, int freq, Node pre, Node next) {

            this.key = key;

            this.value = value;

            this.freq = freq;

            this.pre = null;

            this.next = null;

        }


        public void updateValue(int value) {

            this.value = value;

        }


        public void incrFreq() {

            ++this.freq;

        }


        public int getKey() {

            return this.key;

        }


        public int getValue() {

            return this.value;

        }


        public int getFreq() {

            return this.freq;

        }


        public static final Node createEmptyNode() {

            return new Node(-1,-1,-1,null,null);

        }

    }

    /**

    *  自定义的双向链表类

    */

    protected static class LinkedList {

        //双向链表的头结点

        private final Node head;


        //双向链表的尾节点

        private final Node tail;

        public LinkedList() {

            this.head = Node.createEmptyNode();

            this.tail = Node.createEmptyNode();

            this.head.next = this.tail;

            this.tail.pre = this.head;

        }


        /**

        * 将指定的节点插入到链表的第一个位置

        * @param node 将要插入的节点

        */

        public void insertFirst(Node node) {

            if(node==null) {

                throw new IllegalArgumentException();

            }

            node.next = this.head.next;

            this.head.next.pre = node;

            node.pre = this.head;

            this.head.next = node;

        }


        /**

        * 从链表中删除指定的节点

        * @param node 将要删除的节点

        */

        public void deleteNode(Node node) {

            if(node==null) {

                throw new IllegalArgumentException();

            }

            node.pre.next = node.next;

            node.next.pre = node.pre;

            node.pre = null;

            node.next = null;

        }


        /**

        * 从链表中获取最后一个节点

        * @return 双向链表中的最后一个节点,如果是空链表则返回None

        */

        public Node getLastNode() {

            if(this.head.next==this.tail) {

                return Node.createEmptyNode();

            }

            return this.tail.pre;

        }


        /**

        * 判断链表是否为空,除了head和tail没有其他节点即为空链表

        * @return 链表不空返回True,否则返回False

        */

        public boolean isEmpty() {

            return this.head.next==this.tail;

        }

    }

    //key->Node 这种结构的哈希表

    private final Map<Integer,Node> keyMap = new HashMap<Integer,Node>();

    //freq->LinkedList 这种结构的哈希表

    private final Map<Integer,LinkedList> freqMap = new HashMap<Integer,LinkedList>();

    //缓存的最大容量

    private final int capacity;

    //记录缓存中最低频率

    private int minFreq = 0;

    public LFUCache(int capacity) {

//  if(capacity<=0) {

//  throw new IllegalArgumentException();

//  }

        this.capacity = capacity;

    }


    /**

    * 获取一个元素,如果key不存在则返回-1,否则返回对应的value,同时更新被访问元素的频率

    * @param key 要查找的关键字

    * @return 如果没找到则返回-1,否则返回对应的value

    */

    public int get(int key) {

        if(!this.keyMap.containsKey(key)) {

            return -1;

        }

        Node node = this.keyMap.get(key);

        this.increment(node);

        return node.getValue();

    }


    /**

    * 插入指定的key和value,如果key存在则更新value,同时更新频率,

    * 如果key不存并且缓存满了,则删除频率最低的元素,并插入新元素。否则,直接插入新元素

    * @param key 要插入的关键字

    * @param value 要插入的值

    */

    public void put(int key, int value) {

        if(this.keyMap.containsKey(key)) {

            Node node = this.keyMap.get(key);

            node.updateValue(value);

            this.increment(node);

        }

        else {

            if(this.capacity==0) {

                return;

            }

            if(this.keyMap.size()==this.capacity) {

                this.remoteMinFreqNode();

            }

            Node node = new Node(key,value,1);

            this.increment(node,true);

            this.keyMap.put(key, node);

        }

    }


    /**

    *  更新节点的访问频率

    * @param node 要更新的节点

    */

    private void increment(Node node) {

        increment(node,false);

    }


    /**

    * 更新节点的访问频率

    * @param node 要更新的节点

    * @param isNewNode 是否是新节点,新插入的节点和非新插入节点更新逻辑不同

    */

    private void increment(Node node,boolean isNewNode) {

        if(isNewNode) {

            this.minFreq = 1;

            this.insertToLinkedList(node);

        }

        else {

            this.deleteNode(node);

            node.incrFreq();

            this.insertToLinkedList(node);

            if(!this.freqMap.containsKey(this.minFreq)) {

                ++this.minFreq;

            }

        }

    }


    /**

    * 根据节点的频率,插入到对应的LinkedList中,如果LinkedList不存在则创建

    * @param node 将要插入到LinkedList的节点

    */

    private void insertToLinkedList(Node node) {

        if(!this.freqMap.containsKey(node.getFreq())) {

            this.freqMap.put(node.getFreq(), new LinkedList());

        }

        LinkedList linkedList = this.freqMap.get(node.getFreq());

        linkedList.insertFirst(node);

    }


    /**

    * 删除指定的节点,如果节点删除后,对应的双链表为空,则从__freqMap中删除这个链表

    * @param node 将要删除的节点

    */

    private void deleteNode(Node node) {

        LinkedList linkedList = this.freqMap.get(node.getFreq());

        linkedList.deleteNode(node);

        if(linkedList.isEmpty()) {

            this.freqMap.remove(node.getFreq());

        }

    }


    /**

    * 删除频率最低的元素,从freqMap和keyMap中都要删除这个节点,

    * 如果节点删除后对应的链表为空,则要从__freqMap中删除这个链表

    */

    private void remoteMinFreqNode() {

        LinkedList linkedList = this.freqMap.get(this.minFreq);

        Node node = linkedList.getLastNode();

        linkedList.deleteNode(node);

        this.keyMap.remove(node.getKey());

        if(linkedList.isEmpty()) {

            this.freqMap.remove(node.getFreq());

        }

    }

}

```

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • LRU究竟是个什么东西呢,听上去是那么的高大上。Least Recently Used就是LRU的真面目,翻译...
    宫若石阅读 976评论 0 1
  • 单例定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。饿汉模式public class Singleto...
    小杨不想努力了阅读 367评论 0 4
  • 设原始数据规模为n,需要采样的数量为k 先选取数据流中的前k个元素,保存在集合A中; 从第j(k + 1 <= j...
    Impossible安徒生阅读 290评论 0 0
  • 面试腾讯遇到的算法题。 众所周知,LRU本质就是一个哈希表+双向链表的组合数据结构,java中linkedHash...
    AspirantPeng阅读 536评论 0 0
  • 第三章 Java 1.HashMap 1)HashMap的数据结构? 哈希表结构(链表散列:数组+链表)实现,结合...
    Amy木婉清阅读 359评论 0 0