LeetCode算法题解:LFU Cache

原题:https://leetcode.com/problems/lfu-cache/?tab=Description

题目要求

设计并实现一个数据结构,满足LFU (Least Frequently Used) Cache特性,实现两种操作:get和put

  • get(key):返回指定key对应的value,如果指定key在cache中不存在,返回-1
  • put(key, value):设置指定key的value,如果key不存在,则插入该key-value对。如果cache空间已满,则将最少使用的key-value对移除,如果存在多个key-value对的使用次数相同,则将上次访问时间最早的key-value对移除。
public class LFUCache {

    public LFUCache(int capacity) {
        
    }
    
    public int get(int key) {
        
    }
    
    public void put(int key, int value) {
        
    }
}

进阶要求

以O(1)时间复杂度实现get和put操作

思路解析

这道题的要求可以分解成两部分

  1. 实现一个key-value存储结构,能够通过key快速找到对应的value
  2. 在cache已满时,快速定位到访问次数最低且上次访问时间最早的key,将其移除

上述两种计算都应以O(1)的时间复杂度实现。

对于第1点要求,毫无争议地应使用哈希表来实现,哈希表的插入时间复杂度永远是O(1),在不发生哈希冲突的前提下,查找的时间复杂度也是O(1)。这里我们可以偷个懒,直接把HashMap拿来用。

对于第2点要求,最基本思路是记录每个key的访问次数和上次访问时间,在cache已满时找到访问次数最少的key,如果有多个key访问次数一样,再去找这些key里上次访问时间最早的一个进行移除。

很显然,通过遍历去找访问次数最少的key是不行的,这样时间复杂度就是O(n)了。我们可以考虑构建一个链表,确保访问次数最少的key位于链表的尾部:

图片.png

设定每次新插入的key的访问次数为0,那么插入时只需要将key置于链表的尾部,同时在移除key时只要移除链表尾部的key就行了,这样插入和移除的时间复杂度都是O(1)了。

然而,可能存在多个key的访问次数一样的情况,这种情况下不得不去遍历这些key,找到上次访问时间最早的一个。这样一来,时间复杂度就超过了O(1)。

对此,我们可以考虑把结构改成两层链表:

图片.png

外层链表的每个节点代表一组拥有同样访问次数的key,每个节点自身也是一个链表,内层链表确保上次访问时间最早的key位于内层链表的尾部。

在这一数据结构下,我们在插入key时判断外层链表尾部元素的freq是否为0,如果是,将key插入该内层链表的头部,如果否,生成一个只包含key的外层链表,插入到外层链表的尾部。在访问key时,将该key移动到外层链表的下一个节点的头部。这样一来,在移除key时,只需要移除外层链表尾部元素的尾部元素即可,插入、访问、移除的时间复杂度都是O(1)。

代码解析

定义内层链表的元素对象Node:

private static class Node {
    int key;
    int value;
    int frequency = 0; //访问次数
    Node next; //下一元素
    Node prev; //前一元素
    NodeQueue nq;  //所属的外层链表元素
    
    Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}

定义外层链表的元素对象NodeQueue:

private static class NodeQueue {
    NodeQueue next; //下一元素
    NodeQueue prev;  //前一元素
    Node tail;  //尾部Node
    Node head;  //头部Node
    
    public NodeQueue(NodeQueue next, NodeQueue prev, Node tail, Node head) {
        this.next = next;
        this.prev = prev;
        this.tail = tail;
        this.head = head;
    }
}

定义LFU Cache:

import java.util.HashMap;

public class LFUCache {
    private NodeQueue tail;  //链表尾部的NodeQueue
    private int capacity;  //容量
    private HashMap<Integer, Node> map;  //存储key-value对的HashMap

    //构造方法
    public LFUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<Integer, Node>(capacity);
    }
}

接下来,实现整个数据结构中最关键的算法:将Node右移

private void oneStepUp(Node n) {
    n.frequency++; //访问次数+1
    boolean singleNodeQ = false; //为true时,代表此NodeQueue中只有一个Node元素
    if(n.nq.head == n.nq.tail)
        singleNodeQ = true;  
    if(n.nq.next != null) {
        if(n.nq.next.tail.frequency == n.frequency) {
            //右侧NodeQueue的访问次数与Node当前访问次数一样,将此Node置于右侧NodeQueue的头部
            removeNode(n); //从当前NodeQueue中删除Node
            //把Node插入到右侧NodeQueue的头部
            n.prev = n.nq.next.head;
            n.nq.next.head.next = n;
            n.nq.next.head = n;
            n.nq = n.nq.next;
        } else if(n.nq.next.tail.frequency > n.frequency) {
            //右侧NodeQueue的访问次数大于Node当前访问次数,则需要在两个NodeQueue之间插入一个新的NodeQueue
            if(!singleNodeQ) {
                removeNode(n);
                NodeQueue nnq = new NodeQueue(n.nq.next, n.nq, n, n);
                n.nq.next.prev = nnq;
                n.nq.next = nnq;
                n.nq = nnq;
            }
            //如果当前NodeQueue中只有一个Node,那么其实不需要任何额外操作了
        }
    } else {
        //此NodeQueue的next == null,说明此NodeQueue已经位于外层链表头部了,这时候需要往外侧链表头部插入一个新的NodeQueue
        if(!singleNodeQ) {
            removeNode(n);
            NodeQueue nnq = new NodeQueue(null, n.nq, n, n);
            n.nq.next = nnq;
            n.nq = nnq;
        }
        //同样地,如果当前NodeQueue中只有一个Node,不需要任何额外操作
    }
}

移除Node的方法:

private Node removeNode(Node n) {
    //如果NodeQueue中只有一个Node,那么移除整个NodeQueue
    if(n.nq.head == n.nq.tail) {
        removeNQ(n.nq);
        return n;
    }
    if(n.prev != null)
        n.prev.next = n.next;
    if(n.next != null)
        n.next.prev = n.prev;
    if(n.nq.head == n)
        n.nq.head = n.prev;
    if(n.nq.tail == n)
        n.nq.tail = n.next;
    n.prev = null;
    n.next = null;
    return n;
}

private void removeNQ(NodeQueue nq) {
    if(nq.prev != null)
        nq.prev.next = nq.next;
    if(nq.next != null)
        nq.next.prev = nq.prev;
    if(this.tail == nq)
        this.tail = nq.next;
}

接下来实现get和put方法:

get方法非常简单了,到HashMap中拿到key对应的Node,并且将该Node右移:

public int get(int key) {
    Node n = map.get(key);
    if(n == null)
        return -1;
    oneStepUp(n);
    return n.value;
}

put方法:

public void put(int key, int value) {
    if(capacity == 0)
        return;
    
    Node cn = map.get(key);
    //key已存在的情况下,更新value值,并将Node右移
    if(cn != null) {
        cn.value = value;
        oneStepUp(cn);
        return;
    }
    //cache已满的情况下,把外层链表尾部的内层链表的尾部Node移除
    if(map.size() == capacity) {
        map.remove(removeNode(this.tail.tail).key);
    }
    //插入新的Node
    Node n = new Node(key, value);
    if(this.tail == null) {
        //tail为null说明此时cache中没有元素,直接把Node封装到NodeQueue里加入
        NodeQueue nq = new NodeQueue(null, null, n, n);
        this.tail = nq;
        n.nq = nq;
    } else if(this.tail.tail.frequency == 0) {
        //外层链表尾部元素的访问次数是0,那么将Node加入到外层链表尾部元素的头部
        n.prev = this.tail.head;
        this.tail.head.next = n;
        n.nq = this.tail;
        this.tail.head = n;
    } else {
        //外层链表尾部元素的访问次数不是0,那么实例化一个只包含此Node的NodeQueue,加入外层链表尾部
        NodeQueue nq = new NodeQueue(this.tail, null, n, n);
        this.tail.prev = nq;
        this.tail = nq;
        n.nq = nq;
    }
    //最后把key和Node存入HashMap中
    map.put(key, n);
}

运行结果:

图片.png

还不错,击败了99.81%的选手。不过LeetCode的代码运行时间统计不太稳定,这段代码跑起来的成绩差不多在140~170ms之间。

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

推荐阅读更多精彩内容