LeetCode - 460. LFU Cache(LFU缓存算法,二维链表解决)

LeetCode - 460. LFU Cache(LFU缓存算法,二维链表解决)

题目链接
题目

在这里插入图片描述

解析

设计使用两个链表:

  • 一个大链表表示键值对出现次数的链表
  • 小链表表示这条链表的次数都一样,但是由于我们的添加可以决定添加的顺序,可以达到在次数相同的情况下,先移除最近最少使用的键值对;

下面的代码中的方法:

  • addNodeFromHead(),大链表的结点(本身也是一个小链表)在头部添加一个键值对);
  • deleteNode(),大链表的结点(本身也是一个小链表) 中删除一个结点;
  • LFU结构中的move()方法,表示要当我操作了一个键值对,对应的次数增加,就需要调整结点的位置,move()方法就是从原来的小链表(大链表的结点)移动到一个新的小链表(也就是次数比原来次数+1)的新小链表;
  • modifyHeadList()表示的当我删除一个结点(也就是在旧的小链表中删除一个之后)有可能这个小链表本来只有这一个元素,所以要销毁这个小链表,为什么要写成boolean型的,因为等下在下面的代码中,在move的时候,要重新连接一个大链表的结点要知道preList,所以需要判断;
    这里写图片描述

这里写图片描述

代码有点长,都写了注释:

class LFUCache {

    //小链表(挂在下面的)
    private class Node {
        public Integer key;  //map中push的key
        public Integer value;   //map中对应的value
        public Integer times;     //  操作的次数

        public Node up;   //小链表的上一个
        public Node down;  //小链表的下一个

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

    //大的链表的结点结构  (每一个结点都是一个小链表)
    private class NodeList {
        public Node head;  //大链表的头部指针
        public Node tail;   //大链表的尾部指针

        public NodeList pre;   //大链表的前一个结点
        public NodeList next;  //大链表的下一个结点

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

        public boolean isEmpty() {//返回这个小链表(小链表本身又是大链表的结点)是不是空的
            return head == null;
        }

        //小链表的头部添加结点
        public void addNodeFromHead(Node newHead) {
            newHead.down = head;
            head.up = newHead;
            head = newHead;
        }

        //删除小链表中的任意一个结点
        public void deleteNode(Node node) {
            if (head == tail) { //只有一个结点
                head = null;
                tail = null;
            } else {
                if (head == node) { //删除的是小链表的头部
                    head = head.down; //头结点变成下一个
                    head.up = null;   //头结点的上一个 置空
                } else if (tail == node) {  //删除的是小链表的尾部
                    tail = tail.up;
                    tail.down = null;
                } else {  //删除的是链表的中间
                    node.up.down = node.down;
                    node.down.up = node.up;
                }
            }
            //完全断链
            node.up = null;
            node.down = null;
        }
    }


    private int capacity;  //最大容量
    private int size;   //当前容量
    // key 对应的node   node是在小链表上面的
    private HashMap<Integer, Node> kNMap;
    //Node对应的NodeList的头是哪个  就是任何一个小结点都能查到在大链表的哪个小链表上
    private HashMap<Node, NodeList> heads;  //一个链表对应的大链表的结点是哪个
    public NodeList headList; //整个大链表的头部  动态的头部  不一定就是1作为头


    public LFUCache(int capacity) {
        this.capacity = capacity;
        size = 0;
        kNMap = new HashMap<>();
        heads = new HashMap<>();
        headList = null;
    }


    public void put(int key, int value) {
        if (capacity == 0) return;  //注意特判
        if (kNMap.containsKey(key)) {  //如果已经存在  就要更新值
            Node node = kNMap.get(key);
            node.value = value;
            node.times++;
            NodeList curNodeList = heads.get(node); //找到属于哪一个大链表
            /**
             * move方法
             * 就是在一个大链表中,和自己的上下级解耦,然后放到下一个词频链表中
             * 比如说现在是5 times链上的,则从5times链中拿出来,如果6存在,放到6times链的头部(头插法)
             * 如果6不存在,建出6times的链表
             */
            move(node, curNodeList);
        } else {  //kNMap中不存在,是新插入的  没包含
            //要先判断容量够不够
            if (size == capacity) {  //已经满了  要删掉一个结点
                //要删掉的就是作为大链表的 头部的尾结点  (次数最少的 用了最久的)
                Node deNode = headList.tail;
                headList.deleteNode(deNode);
                /**
                 * 如果我删掉了  这个deNode  有可能我整个大链表的headList都没有东西了,整个大Node要删掉,要更新大headList
                 * 又因为加入了新节点,所以又要更新headList
                 * 先删再加
                 */
                modifyHeadList(headList);
                kNMap.remove(deNode.key); //不要忘记在kNMap中删掉
                heads.remove(deNode);
                size--;

            }

            Node node = new Node(key, value, 1); //新建  次数为1

            if (headList == null) {    //整个大链表都不存在
                headList = new NodeList(node);  //建出大链表的头部
            } else {   //  已经有了大链表的头部
                /**
                 * 如果有 次数为1的头  就直接添加到大头的头部
                 * 如果没有次数为1大头  就建一个大头   然后添加到大头的尾部
                 */
                if (headList.head.times.equals(1)) { //大链表的头的头 的次数是 1 也就是说 有为times的小链表
                    headList.addNodeFromHead(node);  //加到这里
                } else {   //没有times为 1 的小链表  要自己建一个
                    NodeList newList = new NodeList(node);  //建出一个times为1的小链表
                    newList.next = headList;
                    headList.pre = newList;
                    headList = newList;  //更新大头
                }
            }

            //最后再添加这条记录
            kNMap.put(key, node);
            heads.put(node, headList); //这个结点所在的头 肯定是在大头的头部  也就是times一定是1  因为已经判断了不是1(已经存在的情况)
            size++;
        }
    }

    /**
     * 解耦原来的链表  并放入到一个新的链表中
     *
     * @param node        这个node
     * @param oldNodeList node 的原来属于的list
     */
    private void move(Node node, NodeList oldNodeList) {
        oldNodeList.deleteNode(node); //老链表你自己先删掉
        /**
         * 因为你的老链表删掉了一个结点  是不是有可能 连老链表都没了
         *要判断老链表  是否还存在
         * modifyHeadList(oldNodeList)   返回true就是说老链表都被删掉了  所以要去找老链表的前一个链表
         * 返回false就是说老链表还存在  preList就是老链表
         */
        NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.pre : oldNodeList;
        NodeList nextList = oldNodeList.next; //要去的地方  新家

        if (nextList == null) {  //你的oldNodeList是大链表的最后一个
            NodeList newList = new NodeList(node); //建一个  放在最后
            if (preList != null) {
                preList.next = newList;
            }
            newList.pre = preList;

            if (headList == null) {  //本来就是空的就要给headList一个交代
                headList = newList;
            }
            heads.put(node, newList); //换新家了
        } else { //不是最后一个不是times最高的

            if (nextList.head.times.equals(node.times)) { //下一个存在  就直接挂在下一个的头部
                nextList.addNodeFromHead(node);
                heads.put(node, nextList);
            } else {   //下一个不是 times + 1的   要自己新建一个node  然后左右两边重新连接好
                NodeList newList = new NodeList(node);

                if (preList != null) preList.next = newList;
                newList.pre = preList;

                newList.next = nextList;
                nextList.pre = newList;

                if (headList == nextList) { //这个是也要更换头  
                    headList = newList;
                }
                heads.put(node, newList);
            }
        }
    }

    /**
     * 这个方法的调用时机是  把一个node从一个nodelist中删掉 ,然后判断是不是要不这个nodelist给删掉
     * 就是在delete之后, 要不要把整个小链表删掉
     *
     * @param nodeList
     */
    private boolean modifyHeadList(NodeList nodeList) {
        if (nodeList.isEmpty()) {  //为空了才要删掉整个大链表中的这个结点
            if (headList == nodeList) {     //要删的这个  是整个大链表的头部
                headList = headList.next;        //新的头部是老头部的下一个
                if (headList != null) {       //新链表不为空
                    headList.pre = null;
                }
            } else {    //要删的不是头
                nodeList.pre.next = nodeList.next;
                if (nodeList.next != null) {
                    nodeList.next.pre = nodeList.pre;
                }
            }
            return true; //也就是 这个是要整个都要删掉的
        }
        return false;  //不空的话(也就是不只一个)就不要删    留着
    }

    public Integer get(int key) {
        if (capacity == 0) return -1;  //特判一下
        if (!kNMap.containsKey(key)) {
            return -1;
        }
        Node node = kNMap.get(key); //获取结点所在的原来的链表
        node.times++;
        NodeList curNodeList = heads.get(node);  //找到这个结点属于的小链表
        move(node, curNodeList);
        return node.value;  //返回对应的node的值
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值