算法笔记——LRU和LFU缓存结构

LRU缓存结构

问题描述:

设计可以变更的缓存结构(LRU)
【题目】
设计一种缓存结构,该结构在构造时确定大小,假设大小为K,并有两个功能:
set(key,value):将记录(key,value)插入该结构。
get(key):返回key对应的value值。
【要求】
1.set和get方法的时间复杂度为O(1)。
2.某个key的set或get操作一旦发生,认为这个key的记录成了最经常使用的。
3.当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。
【举例】
假设缓存结构的实例是cache,大小为3,并依次发生如下行为:
1.cache.set("A",1)。最经常使用的记录为("A",1)。
2.cache.set("B",2)。最经常使用的记录为("B",2),("A",1)变为最不经常的。
3.cache.set("C",3)。最经常使用的记录为("C",2),("A",1)还是最不经常的。
4.cache.get("A")。最经常使用的记录为("A",1),("B",2)变为最不经常的。
5.cache.set("D",4)。大小超过了3,所以移除此时最不经常使用的记录("B",2),
加入记录 ("D",4),并且为最经常使用的记录,然后("C",2)变为最不经常使用的
记录

分析问题
  • 简单分析LRU结构需要以下几个功能
  • 必须有明确的大小K,可在构造方法中传入
  • set方法为键值对的形式,所以必须准备HasnMap,才能使得set和get方法的时间复杂度为O(1);
  • 调整某个key成为最经常用的节点,所以可以维护一个关于value的双向链表,那么这里直接做一个Node节点,value作为他的属性之一。
  • 当大小超载时,移除最不经常使用的,假设双端链表尾部就是最经常使用的节点,双端链表头部最不进场使用,所以这里直接把头节点删掉就可以了
  • 一旦对一个节点进行修改了,那么需要将修改的节点移动到双端聊表尾部。
代码
    static class Node<V>{
        V value;
        Node<V> last;
        Node<V> next;

        public Node(V value) {
            this.value = value;
        }
    }

    static class DoubleLinkedList<V>{
        Node<V> head;
        Node<V> tail;

        public DoubleLinkedList(){
            this.head = null;
            this.tail = null;
        }

        // 添加一个节点
        void addNode(Node node){
            if(head == null){
                this.head = node;
                this.tail = node;
            }else{
                this.tail.next = node;
                node.last = this.tail;
                this.tail = node;
            }
        }

        // 将某个节点node移动到链表尾部
        void moveNodeToTail(Node node){
            if(this.tail == node){
                return;
            }
            if(this.head == node){
                this.head = node.next;
                this.head.last = null;
            }else{
                node.last.next = node.next;
                node.next.last = node.last;
            }
            node.last = this.tail;
            node.next = null;
            this.tail.next = node;
            this.tail = node;
        }

        // 移除双向链表的头节点
        Node<V> removeHeadNode(){
            if(this.head == null) {
                return null;
            }
            Node<V> res = this.head;
            if(this.head == this.tail){
                this.head = null;
                this.tail = null;
            }else{
                this.head = res.next;
                res.next = null;
                this.head.last = null;
            }
            return res;
        }
    }

    public static class MyCache<K,V>{
        HashMap<K,Node<V>> keyNodeMap;
        HashMap<Node<V>,K> valueKeyMap;
        DoubleLinkedList<V> nodeList;
        int capacity;

        public MyCache(int capacity) {
            if(capacity < 1){
                throw new RuntimeException("容量不可以小于1");
            }
            this.capacity = capacity;
            this.keyNodeMap = new HashMap<>();
            this.valueKeyMap = new HashMap<>();
            this.nodeList = new DoubleLinkedList<>();
        }

        // 根据指定的K获取value
        public V get(K key){
            if(this.keyNodeMap.containsKey(key)){
                Node<V> res = this.keyNodeMap.get(key);
                this.nodeList.moveNodeToTail(res);
                return res.value;
            }
            return null;
        }
        
        // 向缓存结构中添加键值对
        public void set(K key,V value){
            if(this.keyNodeMap.containsKey(key)){
                Node<V> cur = this.keyNodeMap.get(key);
                cur.value = value;
                this.nodeList.moveNodeToTail(cur);
            }else{
                Node<V> temp = new Node<V>(value);
                this.keyNodeMap.put(key,temp);
                this.valueKeyMap.put(temp,key);
                this.nodeList.addNode(temp);
                if(this.keyNodeMap.size() == this.capacity + 1){
                    Node<V> removeValue = this.nodeList.removeHeadNode();
                    K removeKey = this.valueKeyMap.get(removeValue);
                    this.keyNodeMap.remove(removeKey);
                    this.valueKeyMap.remove(removeValue);
                }
            }
        }
    }

LFU缓存结构

LeetCode | 460. LFU缓存

题目描述

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。它应该支持以下操作:get 和 put。
- get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
- put(key, value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近 最少使用的键。
【项的使用次数】就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。

基本思路
  • 同LRU结构,将键值对存储在Node节点中。
  • 创建一个双向链表,链表中每一个位置都是一个双向链表,所有具有相同操作次数的节点node放在同一个位置重新构成双向链表
  • 所以当插入一个元素时,如果该元素存在,则需要将其操作次数加1,然后将其添加到大链表的新的位置中;如果不存在,则创建该节点,默认操作次数为1
  • 获取一个元素时,该Node的操作次数也加1,然后调整到大链表新的位置
  • 剩下的问题就是扣边界了。
代码
// 首先定义节点类型
class Node{
    int key;
    int value;
    int times;
    Node up;
    Node down;

    public Node(int key, int value, int times){
        this.key = key;
        this.value = value;
        this.times = times;
    }
}

//定义NodeList类,LFU包含一个由NodeList构成的双端链表
class NodeList{
    Node head;
    Node tail;
    NodeList last;
    NodeList next;

    public NodeList(Node node){
        this.head = node;
        this.tail = node;
    }

    // 将一个节点node插入此nodeList的头步
    public void addNodeFromHead(Node newNode){
        newNode.down = this.head;
        this.head.up = newNode;
        this.head = newNode;
    }

    // 判断当前的nodeList是否为null
    public boolean isEmpty(){
        return this.head == null;
    }

    // 从当前的nodeList中删除一个node
    public void deleteNode(Node node){
        if(this.head == this.tail){
            this.head = null;
            this.tail = null;
        }else{
            if(this.head == node){
                this.head = node.down;
                this.head.up = null;
            }else if(this.tail == node){
                this.tail = node.up;
                this.tail.down = null;
            }else{
                node.up.down = node.down;
                node.down.up = node.up;
            }
        }
        node.up = null;
        node.down = null;
    }
}

class LFUCache {
    // 定义成员变量
    int capacity;
    int size;
    HashMap<Integer,Node> records;
    HashMap<Node,NodeList> heads;
    NodeList headList;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.records = new HashMap<Integer,Node>();
        this.heads = new HashMap<Node,NodeList>();
        this.headList = null;
    }
    
    public int get(int key) {
        int res  = -1;
        if(records.containsKey(key)){
            Node node = records.get(key);
            res = node.value;
            node.times++;
            NodeList curNodeList = heads.get(node);
            move(node,curNodeList);
        }
        return res;
    }
    
    public void put(int key, int value) {
        if(this.capacity == 0) {
            return;
        }
        // 如果当前的key指向的节点存在
        if(records.containsKey(key)){
            Node node = records.get(key);
            node.value = value;
            node.times++;
            NodeList curNodeList = heads.get(node);  // 获取当前的node存在的NodeList
            move(node,curNodeList);    // 将node移动到新的NodeList中,并调正curNodeList
        }
        // 如果当前的key指向的节点不存在
        else{
            //r如果容量超限
            if(this.size == this.capacity){
                //删除headList的tail
                Node tail = this.headList.tail;
                this.headList.deleteNode(tail);
                modifyHeadList(this.headList); // 更新当前的headList,如果为null调整
                records.remove(tail.key);
                heads.remove(tail);
                this.size--;
            }
            // 创建新的Node进行插入
            Node node = new Node(key,value,1);
            if(this.headList == null){
                this.headList = new NodeList(node);
            }else{
                if(this.headList.head.times == node.times){
                    this.headList.addNodeFromHead(node);
                }else{
                    NodeList newList = new NodeList(node);
                    newList.next = this.headList;
                    this.headList.last = newList;
                    this.headList = newList;
                }
            }
            records.put(key,node);
            heads.put(node,this.headList);
            this.size++;
        }
    }

    private void move(Node node, NodeList oldNodeList){
        // 首先从原来的NodeList中删除node节点
        oldNodeList.deleteNode(node);
        NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.last : oldNodeList;  // 调整oldNodeList
        NodeList nextList = oldNodeList.next;
        if(nextList == null){
            // 新建一个Nodelist,把node扔进去
            NodeList newList = new NodeList(node);
            if(preList != null){
                preList.next = newList;
            }
            newList.last = preList;
            if(this.headList == null){
                this.headList = newList;
            }
            heads.put(node,newList);
        }else{
            if(nextList.head.times == node.times){
                // 将node放到头步
                nextList.addNodeFromHead(node);
                heads.put(node,nextList);
            }else{
                NodeList newList =  new NodeList(node);
                if(preList != null){
                    preList.next = newList;
                }
                newList.last = preList;
                newList.next = nextList;
                nextList.last = newList;
                if(this.headList == nextList){
                     this.headList = newList;
                }
                heads.put(node,newList);
            }
        }
    }

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

推荐阅读更多精彩内容

  • 理论总结 它要解决什么样的问题? 数据的访问、存取、计算太慢、太不稳定、太消耗资源,同时,这样的操作存在重复性。因...
    jiangmo阅读 2,835评论 0 11
  • +++Categories = ["iOS",]Tags = ["缓存算法","LRU",]date = "201...
    行业碧油鸡阅读 1,577评论 0 3
  • 缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。 本文结合实际开发经验,从...
    Java小生阅读 797评论 1 3
  • LRU 最近最少使用 设计可以变更的缓存结构(LRU)【题目】设计一种缓存结构,该结构在构造时确定大小,假设大小为...
    cool_cz阅读 367评论 0 0
  • 1我们都听过 cache,当你问他们是什么是缓存的时候,他们会给你一个完美的答案,可是他们不知道缓存是怎么构建的,...
    织田信长阅读 1,490评论 1 20