走进缓存算法-最不经常使用(LFU)

含义

这个缓存算法使用一个计数器来记录条目被访问的频率,通过使用LFU缓存算法,最低访问数的条目首先被移除若存在多个最低访问数的条目,那么移除加入缓存时间最久的条目。

这个缓存算法有缺陷:它无法对一个最初拥有极高访问率而后长时间不访问的条目负责。

拥有方法

🍟构造函数

public LFUCache(int capacity) {
    // 初始化
}

🎃获取

获取方法时间复杂度为O(1),若对应键不存在返回-1

public int get(int key) {
}

🎈添加

添加方法时间复杂度为O(1)
若对应键不存在则加入缓存
如果容量已满则删除使用频率最低的条目,若存在多个使用频率最低的条目则删除最长时间未使用的条目
若存在则更新值

public void put(int key, int value) {
}

📄空类

class LFUCache {

    public LFUCache(int capacity) {
    }

    public int get(int key) {
    }

    public void put(int key, int value) {
    }
}

实现思路

双向链表+双哈希表+最低使用频率
一个哈希表存储键与节点,另一个存储频率与相同频率的链表
链表从越靠左代表越先加入。对于获取方法我们拿到最低使用频率的链表并返回首个节点即可
每一次的get操作都对频率+1
put操作

  • 原本存在。修改值并且频率+1
  • 原本不存在。
    • 判断是否达到容量阈值,达到则删除一个最不常用的条目
    • 添加新元素到频率为1的链表中

下面给出结构图

节点Node

class Node {
    private int key, val, freq;
    private Node pre, next;

    public Node() {
        this(-1, -1, 0);
    }

    public Node(int key, int val, int freq) {
        this.key = key;
        this.val = val;
        this.freq = freq;
    }
}

字段解释:

  • key:缓存的键
  • val:缓存的值
  • freq:缓存的使用频率
  • pre:链表中上一个节点
  • next:链表中下一个节点

链表DoublyLinkedList

class DoublyLinkedList {
    private final Node head, tail;
    private int size;

    public DoublyLinkedList() {
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.pre = head;
        size = 0;
    }

    /**
    * 链表是否为空
    */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
    * 获取链表的首个节点
    */
    public Node getFirst() {
        return head.next;
    }

    /**
    * 在链表尾部新增节点
    */
    public void add(Node node) {
        node.pre = tail.pre;
        node.next = tail;
        node.pre.next = node;
        tail.pre = node;
        size++;
    }

    /**
    * 删除某一个节点
    */
    public void remove(Node node) {
        Node pre = node.pre, next = node.next;
        next.pre = pre;
        pre.next = next;
        size--;
    }
}

字段解释

  • head:辅助头结点
  • tail:辅助尾节点

头尾两个辅助节点是方便不判空,也就是每一个节点都有前后两个节点。
对于链表的顺序,从左到右排列是加入的时间先后。对于一条链表中最先加入的条目就为首个条目。
附上几张图帮助理解

链表结构


我们看到除了辅助头尾节点之外每一个节点都连接着它的上一个与下一个节点

链表添加


将原末尾节点断开连接至新节点,然后将新节点连接至辅助尾节点即可

链表删除


将节点的前后断卡然后,将前后的节点连接起来即可

完整代码

class LFUCache {
    /**
     * 缓存容量
     */
    private final int capacity;
    /**
     * 最小频率
     */
    private int minFreq;
    /**
     * 频率哈希表。结构:key=freq,val=list
     */
    private final Map<Integer, DoublyLinkedList> freqMap;
    /**
     * 键哈希表。结构:key=条目key,val=node
     */
    private final Map<Integer, Node> keyMap;

    public LFUCache(int capacity) {
        // init
        this.capacity = capacity;
        minFreq = 0;
        freqMap = new HashMap<>();
        keyMap = new HashMap<>();
    }

    public int get(int key) {
        // 若容量为0,或者不存在直接返回-1
        if (capacity == 0 || !keyMap.containsKey(key)) {
            return -1;
        }
        Node node = keyMap.get(key);
        // 从原链表中取出
        DoublyLinkedList doublyLinkedList = freqMap.get(node.freq);
        doublyLinkedList.remove(node);
        if (doublyLinkedList.isEmpty()) {
            // 当前频率无节点,删除
            freqMap.remove(node.freq);
            // 当前最小频率与删除的相同那么最小频率需改变
            if (minFreq == node.freq) {
                minFreq++;
            }
        }
        // 更新节点频率
        node.freq++;
        // 加入新链表
        DoublyLinkedList defaultList = freqMap.getOrDefault(node.freq, new DoublyLinkedList());
        defaultList.add(node);
        freqMap.putIfAbsent(node.freq, defaultList);

        return node.val;
    }

    public void put(int key, int value) {
        // 容量为0,不做操作
        if (capacity == 0) {
            return;
        }
        // 原本存在
        if (keyMap.containsKey(key)) {
            Node node = keyMap.get(key);
            // 除需要更新值以外其他与get操作无异
            int oldVal = get(key);
            node.val = value;
            return;
        }
        // 删除节点
        if (keyMap.size() == capacity) {
            DoublyLinkedList doublyLinkedList = freqMap.get(minFreq);
            Node first = doublyLinkedList.getFirst();
            doublyLinkedList.remove(first);
            if (doublyLinkedList.isEmpty()) {
                // 当前频率无节点,删除
                freqMap.remove(minFreq);
            }
            // 删除节点
            keyMap.remove(first.key);
        }
        // 新增节点
        Node node = new Node(key, value, 1);
        keyMap.put(key, node);
        minFreq = 1;
        DoublyLinkedList doublyLinkedList = freqMap.getOrDefault(minFreq, new DoublyLinkedList());
        doublyLinkedList.add(node);
        freqMap.putIfAbsent(minFreq, doublyLinkedList);
    }

    private static class Node {
        private int key, val, freq;
        private Node pre, next;

        public Node() {
            this(-1, -1, 0);
        }

        public Node(int key, int val, int freq) {
            this.key = key;
            this.val = val;
            this.freq = freq;
        }
    }

    private static class DoublyLinkedList {
        private final Node head, tail;
        private int size;

        public DoublyLinkedList() {
            head = new Node();
            tail = new Node();
            head.next = tail;
            tail.pre = head;
            size = 0;
        }

        public boolean isEmpty() {
            return size == 0;
        }

        public Node getFirst() {
            return head.next;
        }

        public void add(Node node) {
            node.pre = tail.pre;
            node.next = tail;
            node.pre.next = node;
            tail.pre = node;
            size++;
        }

        public void remove(Node node) {
            Node pre = node.pre, next = node.next;
            next.pre = pre;
            pre.next = next;
            size--;
        }
    }
}

🎉恭喜学会了LFU
此处只讲了双哈希表+双向链表的做法,还可以用哈希表+平衡二叉树的方式来实现
对于Java平衡二叉树直接使用TreeSet即可
平衡二叉树使用核心:排序的首要条件是频率次要条件是加入时间
对于排序方法,建议实现接口Comparator<Integer>。亦或者可以在TreeSet的构造方法中写

本文完。
如有不对的地方还望指教。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LFU(Least Frequently Used)算法是一种缓存淘汰算法,它根据缓存中每个条目的使用频率来选择要淘汰的条目。以下是一个简单的 LFU 算法的 C 语言实现: ```c #include <stdio.h> #include <stdlib.h> typedef struct node { int key; int value; int freq; struct node* prev; struct node* next; } Node; typedef struct { int size; int capacity; Node* head; Node* tail; Node** freqList; } LFUCache; Node* createNode(int key, int value) { Node* node = (Node*) malloc(sizeof(Node)); node->key = key; node->value = value; node->freq = 1; node->prev = NULL; node->next = NULL; return node; } LFUCache* createLFUCache(int capacity) { LFUCache* cache = (LFUCache*) malloc(sizeof(LFUCache)); cache->size = 0; cache->capacity = capacity; cache->head = NULL; cache->tail = NULL; cache->freqList = (Node**) calloc(capacity + 1, sizeof(Node*)); return cache; } void deleteNode(LFUCache* cache, Node* node) { if (node == cache->head) { cache->head = node->next; } else if (node == cache->tail) { cache->tail = node->prev; cache->tail->next = NULL; } else { node->prev->next = node->next; node->next->prev = node->prev; } cache->size--; cache->freqList[node->freq] = NULL; free(node); } void insertNode(LFUCache* cache, Node* node) { if (cache->head == NULL) { cache->head = node; cache->tail = node; } else { node->next = cache->head; cache->head->prev = node; cache->head = node; } cache->size++; cache->freqList[node->freq] = node; } int get(LFUCache* cache, int key) { Node* node = cache->freqList[key]; if (node == NULL) { return -1; } node->freq++; if (node != cache->head) { deleteNode(cache, node); insertNode(cache, node); } return node->value; } void put(LFUCache* cache, int key, int value) { if (cache->capacity == 0) { return; } Node* node = cache->freqList[key]; if (node != NULL) { node->value = value; node->freq++; if (node != cache->head) { deleteNode(cache, node); insertNode(cache, node); } } else { node = createNode(key, value); if (cache->size == cache->capacity) { Node* tail = cache->tail; deleteNode(cache, tail); } insertNode(cache, node); } } void printCache(LFUCache* cache) { Node* node = cache->head; while (node != NULL) { printf("(%d:%d:%d) ", node->key, node->value, node->freq); node = node->next; } printf("\n"); } int main() { LFUCache* cache = createLFUCache(2); put(cache, 1, 1); put(cache, 2, 2); printCache(cache); printf("%d\n", get(cache, 1)); printCache(cache); put(cache, 3, 3); printCache(cache); printf("%d\n", get(cache, 2)); printf("%d\n", get(cache, 3)); put(cache, 4, 4); printf("%d\n", get(cache, 1)); printf("%d\n", get(cache, 3)); printf("%d\n", get(cache, 4)); printCache(cache); return 0; } ``` 在这个实现中,LFUCache 结构体维护了缓存的大小和容量,以及缓存中每个访问频率的节点。Node 结构体表示缓存中的每个元素。createNode 函数用于创建新节点,insertNode 函数用于将节点插入到缓存中,deleteNode 函数用于从缓存中删除节点,get 函数用于获取缓存中指定键的值,并将节点的访问频率加一,put 函数用于将新元素放入缓存中。最后,printCache 函数用于打印缓存中的所有元素。 在 main 函数中,我们创建一个大小为 2 的 LFU 缓存,向其中插入两个元素,打印缓存中的所有元素,然后获取键为 1 的元素,再次打印缓存中的所有元素,向缓存中插入一个新元素,打印缓存中的所有元素,获取键为 2 和 3 的元素,向缓存中插入一个新元素,并打印缓存中的所有元素。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值