LRU & LFU 缓存(LeetCode 146 & 460)

1 概述

本文会介绍LFULRU缓存两道题目的思路和实现。

2 LRU缓存

2.1 原题

2.2 思路

LRU是最近最少使用缓存,实现时需要基于哈希表+双向链表实现。

基于哈希表是因为题目要求的O(1)平均时间复杂度,而使用双向链表是什么原因呢?

LRU的核心操作有:

  • 访问key并调整key位置
  • 插入新key
  • 容量满时删除最久未使用的key

而使用双向链表能完美地支持上面三个核心操作:

  • 访问key调整key位置:通过哈希表定位到链表节点,并从链表中删除该节点,移动到链头,这样能做到O(1)的时间
  • 插入新key:直接插入到链头即可,O(1)
  • 容量满时删除最久未使用的key:如果访问key和插入新key按照上面的描述操作,那么需要删除的key就自然位于链尾,直接从链尾删除即可,能做到O(1)

而其他数据结构,都无法O(1)内做到这三种操作,例如:

  • 栈:插入O(1),但是只允许一端操作,无法做到动态调整
  • 队列:无法处理中间被访问的情况,中间元素被访问的话无法调整
  • 双端队列:也无法处理中间被访问的情况
  • 单向链表:无法做到O(1)删除

所以实现方式就是哈希表配合双向链表实现:

  • 哈希表里面的key是参数传过来的key
  • 哈希表里面的value是这个存储这个key的双向链表的节点

实现细节1:实现双向链表的时候,可以利用哨兵节点,可以避免处理链头链尾的特殊情况。

对于get操作:

  • 如果没有包含该key,直接返回
  • 如果有包含,从双向链表中删除该key对应的节点,并移动到链头

对于put操作:

  • 如果包含该key,覆盖旧value,从双向链表中删除该key对应的节点,并移动到链头
  • 如果不包含该key,创建该key对应的节点并插入到链头,如果大于容量,删除链尾对应的节点

实现细节2:由于删除节点和移动到链头是常用操作,可以用一个函数封装起来,避免每次手动操作出错。

2.3 动图演示

动态参数如下:

  • 容量为3
  • 操作路径为put(1),put(2),put(3),get(1),put(4)(由于动图大小限制,只能容下这么多操作)

HEADTAIL是两个哨兵,表示链头和链尾哨兵。LRU是指最近最少使用,MRU是指最近使用,实际上,MRU就是链头,LRU就是链尾。

2.4 完整代码

完整代码如下:

cpp 复制代码
// 双向链表节点
struct Node {
    // 前驱
    Node *pre;
    // 后继
    Node *next;
    // key
    int key;
    // value
    int val;
};

class LRUCache {
    // 数量
    int cnt_;
    // 容量
    int capacity_;
    // 哨兵
    Node *head_;
    Node *tail_;
    // 哈希表
    unordered_map<int, Node *> m_;

    // 将node移动到链头
    void move_to_head(Node *node) {
        node->pre = head_;
        node->next = head_->next;
        head_->next->pre = node;
        head_->next = node;
    }

    // 将node从链中移除
    void remove_node(const Node *node) {
        node->pre->next = node->next;
        node->next->pre = node->pre;
    }

public:
    explicit LRUCache(const int capacity): cnt_(0), capacity_(capacity), head_(new Node()), tail_(new Node()) {
        // 初始化,让head和tail链接
        head_->next = tail_;
        tail_->pre = head_;
    }

    int get(const int key) {
        // 判断有没有key,没有直接返回key
        if (!m_.contains(key)) {
            return -1;
        }
        // 获取当前key对应的node
        const auto node = m_[key];
        // 从链中移除这个node并移动到链头,符合题目定义
        remove_node(node);
        move_to_head(node);
        return node->val;
    }

    void put(const int key, const int value) {
        // 如果包含该key
        if (m_.contains(key)) {
            // 取出对应的node
            const auto node = m_[key];
            // 覆盖旧值
            node->val = value;
            // 类似get操作,从链中移除node并移动node到链头
            remove_node(node);
            move_to_head(node);
        } else {
            // 如果不包含,需要新增一个node
            const auto node = new Node();
            node->key = key;
            node->val = value;
            // 新的node需要移动到链头
            move_to_head(node);
            // 哈希表存储
            m_[key] = node;
            // 如果容量未满,容量+1,返回
            if (cnt_ < capacity_) {
                ++cnt_;
                return;
            }
            // 否则容量满了,删除最近最少使用的节点,也就是链尾节点
            // 从哈希表中移除
            m_.erase(tail_->pre->key);
            // 从链中移除
            remove_node(tail_->pre);
        }
    }
};

这样能通过题目但是还有一个潜在的问题就是内存泄漏,因为从链中删除节点之后,没有手动释放节点对应的内存。另一方面,析构的时候也没有释放内存,所以一个更符合实际的版本应该加上内存释放,如下所示:

cpp 复制代码
// 双向链表节点
struct Node {
    // 前驱
    Node *pre;
    // 后继
    Node *next;
    // key
    int key;
    // value
    int val;
};

class LRUCache {
    // 数量
    int cnt_;
    // 容量
    int capacity_;
    // 哨兵
    Node *head_;
    Node *tail_;
    // 哈希表
    unordered_map<int, Node *> m_;

    // 将node移动到链头
    void move_to_head(Node *node) {
        node->pre = head_;
        node->next = head_->next;
        head_->next->pre = node;
        head_->next = node;
    }

    // 将node从链中移除
    void remove_node(const Node *node) {
        node->pre->next = node->next;
        node->next->pre = node->pre;
    }

public:
    explicit LRUCache(const int capacity): cnt_(0), capacity_(capacity), head_(new Node()), tail_(new Node()) {
        // 初始化,让head和tail链接
        head_->next = tail_;
        tail_->pre = head_;
    }

    ~LRUCache() {
        // 析构的时候删除整条链
        while (head_->next != tail_) {
            const auto node = head_->next;
            head_->next = node->next;
            delete node;
        }
        // 删除两个哨兵
        delete head_;
        delete tail_;
    }

    int get(const int key) {
        // 判断有没有key,没有直接返回key
        if (!m_.contains(key)) {
            return -1;
        }
        // 获取当前key对应的node
        const auto node = m_[key];
        // 从链中移除这个node并移动到链头,符合题目定义
        remove_node(node);
        move_to_head(node);
        return node->val;
    }

    void put(const int key, const int value) {
        // 如果包含该key
        if (m_.contains(key)) {
            // 取出对应的node
            const auto node = m_[key];
            // 覆盖旧值
            node->val = value;
            // 类似get操作,从链中移除node并移动node到链头
            remove_node(node);
            move_to_head(node);
        } else {
            // 如果不包含,需要新增一个node
            const auto node = new Node();
            node->key = key;
            node->val = value;
            // 新的node需要移动到链头
            move_to_head(node);
            // 哈希表存储
            m_[key] = node;
            // 如果容量未满,容量+1,返回
            if (cnt_ < capacity_) {
                ++cnt_;
                return;
            }
            // 否则容量满了,删除最近最少使用的节点,也就是链尾节点
            // 从哈希表中移除
            m_.erase(tail_->pre->key);
            // 使用变量存储,因为remove_node()之后tail->pre就变了
            const auto tail_pre = tail_->pre;
            // 从链中移除
            remove_node(tail_->pre);
            // 释放内存
            delete tail_pre;
        }
    }
};

2.5 Java版本

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Node {
    public Node pre;
    public Node next;
    int key;
    int val;
}

public class LRUCache {
    private int cnt = 0;
    private final int capacity;
    private final Node head;
    private final Node tail;
    private final Map<Integer, Node> m = new HashMap<>();

    private void moveToHead(Node node) {
        node.pre = head;
        node.next = head.next;
        head.next.pre = node;
        head.next = node;
    }

    private void removeNode(Node node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    public LRUCache(int capacity) {
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.pre = head;
        this.capacity = capacity;
    }

    public int get(int key) {
        Node node = m.get(key);
        if(node == null){
            return -1;
        }
        removeNode(node);
        moveToHead(node);
        return node.val;
    }

    public void put(int key, int value) {
        Node node = m.get(key);
        if(node != null){
            node.val = value;
            removeNode(node);
            moveToHead(node);
        }else{
            node = new Node();
            node.key = key;
            node.val = value;
            moveToHead(node);
            m.put(key,node);
            if(cnt < capacity){
                ++cnt;
                return;
            }
            m.remove(tail.pre.key);
            removeNode(tail.pre);
        }
    }
}

2.6 Go版本

go 复制代码
var m map[int]*Node
var head *Node
var tail *Node
var capacity int
var cnt int

type Node struct {
	Key  int
	Val  int
	Pre  *Node
	Next *Node
}

type LRUCache struct {
}

func removeNode(node *Node) {
	pre, next := node.Pre, node.Next
	pre.Next, next.Pre = next, pre
}

func moveToHead(node *Node) {
	next := head.Next
	node.Next = next
	next.Pre = node
	head.Next = node
	node.Pre = head
}

func Constructor(cap int) LRUCache {
	capacity, cnt = cap, 0
	head, tail = &Node{-1, -1, nil, nil}, &Node{-1, -1, nil, nil}
	head.Next, tail.Pre = tail, head
	m = make(map[int]*Node)
	return LRUCache{}
}

func (this *LRUCache) Get(key int) int {
	node, exist := m[key]
	if !exist {
		return -1
	}
	removeNode(node)
	moveToHead(node)
	return node.Val
}

func (this *LRUCache) Put(key int, value int) {
	node, exist := m[key]
	if exist {
		node.Val = value
		removeNode(node)
		moveToHead(node)
	} else {
		node = &Node{key, value, nil, nil}
		moveToHead(node)
		m[key] = node
		if cnt < capacity {
			cnt++
			return
		}
		delete(m, tail.Pre.Key)
		removeNode(tail.Pre)
	}
}

3 LFU缓存

3.1 原题

3.2 思路

LFU实际上是在LRU的基础上增加了一个频率的限制:

  • 当需要移除缓存时,优先移除频率最低的数据
  • 当频率最低的数据有多个时,采用LRU的规则,移除最近最少使用数据

由于频率是会变动的,并且频率之间的变化幅度是1,所以可以使用一个频率双向链表来维护频率的变动:

  • 频率增加1就相当于插入一条频率加1的链
  • 频率减1就相当于删除一条原来频率的链(当原来频率的链没有其他元素时)
  • 可以O(1)时间访问到频率最低的链

在频率链的基础上,每个频率链的节点维护的实际上是一条LRU链,根据前面的LRU规则进行维护。

这样就形成频率链+LRU链的两条双向链表的结构:

  • 频率链用于维护频率
  • LRU链用于维护最近最少使用的数据

实现的时候还需要注意以下几点:

  • 一般只需要两个类即可,一个是频率链节点,一个是LRU链节点,当然也可以用四个类,新增LRU链类和频率链类,但是这样代码实现会更复杂
  • 建议像之前LRU一样,将插入链头和移除节点的操作单独一个函数,对频率链节点类也一样
  • LFU代码量大,非常容易出错,建议小心验证,明确每个变量名和函数名

3.3 整体结构

和前面提到的一样,使用两个双向链表:

  • 频率链头连接着最大的频率,链尾连接着最小的频率
  • 每个频率链节点连着一个完整的LRU
  • LRU链和之前的一样,遵循最近最少使用原则进行淘汰

本来想展示动图的,由于太小限制了5M,没有一个效果还可以的动图就不放了。

3.4 完整代码

带了非常详细的注释:

cpp 复制代码
// 频率链节点,C++独有的前向声明
struct FreqListNode;

// LRU链节点
struct LRUListNode {
    // LRU前驱
    LRUListNode *pre;
    // LRU后继
    LRUListNode *next;
    // 当前LRU链位于哪个频率链节点
    FreqListNode *freq_list;
    // key
    int key;
    // value
    int value;
};

// 频率链节点
struct FreqListNode {
    // LRU链头节点
    LRUListNode *head_lru_list;
    // LRU链尾节点
    LRUListNode *tail_lru_list;
    // +1频率链,也就是上面的频率链
    FreqListNode *up_freq_list;
    // -1频率链,也就是下面的频率链
    FreqListNode *down_freq_list;
    // 频率
    int freq;

    // 频率初始化为0
    FreqListNode() : head_lru_list(new LRUListNode()), tail_lru_list(new LRUListNode()), up_freq_list(nullptr),
                     down_freq_list(nullptr), freq(0) {
        // 初始化LRU链
        head_lru_list->next = tail_lru_list;
        tail_lru_list->pre = head_lru_list;
    }

    // 移除LRU链
    void remove_lru_list(const LRUListNode *lru_list) {
        lru_list->pre->next = lru_list->next;
        lru_list->next->pre = lru_list->pre;
    }

    // 移动或插入lru链节点到lru链头
    void move_or_insert_lru_list_node_to_head(LRUListNode *lru_list) {
        lru_list->pre = head_lru_list;
        lru_list->next = head_lru_list->next;
        head_lru_list->next->pre = lru_list;
        head_lru_list->next = lru_list;
    }

    // lru链是否为空
    [[nodiscard]] bool lru_list_empty() const {
        return head_lru_list->next == tail_lru_list;
    }
};

class LFUCache {
    // 总节点数
    int node_cnt_;
    // 容量
    int capacity_;
    // 频率链头
    FreqListNode *top_freq_list_;
    // 频率链尾
    FreqListNode *bottom_freq_list_;
    // key对应的lru链节点
    unordered_map<int, LRUListNode *> m_;

    // 移除频率链
    void remove_freq_list(const FreqListNode *freq_list) {
        freq_list->down_freq_list->up_freq_list = freq_list->up_freq_list;
        freq_list->up_freq_list->down_freq_list = freq_list->down_freq_list;
    }

    // 插入频率链,在down_list上面插入new_list
    void insert_freq_list(FreqListNode *down_freq_list, FreqListNode *new_freq_list) {
        new_freq_list->up_freq_list = down_freq_list->up_freq_list;
        new_freq_list->down_freq_list = down_freq_list;
        down_freq_list->up_freq_list->down_freq_list = new_freq_list;
        down_freq_list->up_freq_list = new_freq_list;
    }

public:
    explicit LFUCache(const int capacity) : node_cnt_(0), capacity_(capacity), top_freq_list_(new FreqListNode()),
                                            bottom_freq_list_(new FreqListNode()) {
        // 初始化频率链
        top_freq_list_->down_freq_list = bottom_freq_list_;
        bottom_freq_list_->up_freq_list = top_freq_list_;
    }

    int get(const int key) {
        // 没有就直接返回
        if (!m_.contains(key)) {
            return -1;
        }
        // 获取对应的lru链节点
        const auto lru_list_node = m_[key];
        // 获取对应的频率链
        const auto freq_list = lru_list_node->freq_list;
        // 获取频率
        const int freq = freq_list->freq;
        // 先从原来的频率链移除这个lru链节点,因为访问链,所以肯定需要从当前的频率链移除,并将后续频率+1
        freq_list->remove_lru_list(lru_list_node);
        // 如果上面的频率链之前已经创建过了,并且频率刚好是当前的频率+1
        if (freq_list->up_freq_list->freq == freq + 1) {
            // 设置lru链节点对应的频率链,也就是将当前的lru链节点移动到上面的频率链
            lru_list_node->freq_list = freq_list->up_freq_list;
            // 根据lru的访问方式插入到lru链头
            freq_list->up_freq_list->move_or_insert_lru_list_node_to_head(lru_list_node);
        } else {
            // 如果没有创建过,就要创建一条新的频率链
            const auto new_freq_list = new FreqListNode();
            // 赋值频率链
            lru_list_node->freq_list = new_freq_list;
            // 新频率链频率+1
            new_freq_list->freq = freq + 1;
            // 新频率链插入lru节点到lru链头
            new_freq_list->move_or_insert_lru_list_node_to_head(lru_list_node);
            // 新插入频率链
            insert_freq_list(freq_list, new_freq_list);
        }
        // 如果旧的频率链已经没有lru节点,删除旧的频率链
        if (freq_list->lru_list_empty()) {
            remove_freq_list(freq_list);
        }
        // 返回value
        return lru_list_node->value;
    }

    void put(int key, int value) {
        // 如果已经有key
        if (m_.contains(key)) {
            // 覆盖新值
            m_[key]->value = value;
            // 进行get操作,相当于频率+1以及调整lru链
            // 如果不直接调用get的话这里会额外多不少代码
            get(key);
            return;
        }
        // 如果没有,创建新的lru链节点
        const auto lru_list_node = new LRUListNode();
        lru_list_node->key = key;
        lru_list_node->value = value;

        // 如果节点数量大于等于容量
        if (node_cnt_ >= capacity_) {
            // 需要移除一个节点,也就是频率链尾中的lru链尾节点
            const auto freq_list = bottom_freq_list_->up_freq_list;
            // 取lru链尾
            const auto lru_list = freq_list->tail_lru_list->pre;
            // 删除
            freq_list->remove_lru_list(lru_list);
            // 如果删除这个节点之后lru链为空,删除这个频率链
            if (freq_list->lru_list_empty()) {
                remove_freq_list(freq_list);
            }
            // 从哈希表中删除
            m_.erase(lru_list->key);
        } else {
            // 否则直接+1
            ++node_cnt_;
        }

        // 新的节点需要加入频率链尾
        const auto freq_list = bottom_freq_list_->up_freq_list;
        // 插入哈希表
        m_[key] = lru_list_node;
        // 如果频率为1,直接在这个lru链中插入节点即可
        if (freq_list->freq == 1) {
            lru_list_node->freq_list = freq_list;
            freq_list->move_or_insert_lru_list_node_to_head(lru_list_node);
        } else {
            // 如果频率不为1,说明最少的频率不是1,需要创建一条频率为1的频率链
            const auto new_freq_list = new FreqListNode();
            lru_list_node->freq_list = new_freq_list;
            new_freq_list->freq = 1;
            // 将节点插入到这条频率为1的频率链中对应的lru链
            new_freq_list->move_or_insert_lru_list_node_to_head(lru_list_node);
            // 插入整条频率链
            insert_freq_list(bottom_freq_list_, new_freq_list);
        }
    }
};

当然,这个代码也有内存泄漏的问题,修改方式和前面介绍的LRU类似,手动delete即可。

另一方面,实际上这里的top_freq_list_在代码中没有任何的引用,所以可以删去(别的实现方式可能会用到),这是一个小的优化点,这里为了代码的完整性就保留下来了。

3.5 Java版本

java 复制代码
import java.util.HashMap;
import java.util.Map;

class LRUListNode {
    LRUListNode pre;
    LRUListNode next;
    FreqListNode freqList;
    int key;
    int val;
}

class FreqListNode {
    LRUListNode headLruList;
    LRUListNode tailLruList;
    FreqListNode upFreqList;
    FreqListNode downFreqList;
    int freq;

    public FreqListNode() {
        headLruList = new LRUListNode();
        tailLruList = new LRUListNode();
        upFreqList = downFreqList = null;
        freq = 0;
        headLruList.next = tailLruList;
        tailLruList.pre = headLruList;
    }

    void removeLruList(LRUListNode lruList) {
        lruList.pre.next = lruList.next;
        lruList.next.pre = lruList.pre;
    }

    void moveOtInsertLruListNodeToHead(LRUListNode lruListNode) {
        lruListNode.pre = headLruList;
        lruListNode.next = headLruList.next;
        headLruList.next.pre = lruListNode;
        headLruList.next = lruListNode;
    }

    boolean lruListEmpty() {
        return headLruList.next == tailLruList;
    }
}

public class LFUCache {
    private int nodeCount;
    private final int capacity;
    private final FreqListNode topFreqList;
    private final FreqListNode bottomFreqList;
    Map<Integer, LRUListNode> map = new HashMap<>();

    private void removeFreqList(FreqListNode freqList) {
        freqList.downFreqList.upFreqList = freqList.upFreqList;
        freqList.upFreqList.downFreqList = freqList.downFreqList;
    }

    private void insertFreqList(FreqListNode downFreqList, FreqListNode newFreqList) {
        newFreqList.upFreqList = downFreqList.upFreqList;
        newFreqList.downFreqList = downFreqList;
        downFreqList.upFreqList.downFreqList = newFreqList;
        downFreqList.upFreqList = newFreqList;
    }

    public LFUCache(int capacity) {
        nodeCount = 0;
        this.capacity = capacity;
        topFreqList = new FreqListNode();
        bottomFreqList = new FreqListNode();
        topFreqList.downFreqList = bottomFreqList;
        bottomFreqList.upFreqList = topFreqList;
    }

    public int get(int key) {
        LRUListNode lruListNode = map.get(key);
        if (lruListNode == null) {
            return -1;
        }
        FreqListNode freqList = lruListNode.freqList;
        int freq = freqList.freq;
        freqList.removeLruList(lruListNode);
        if (freqList.upFreqList.freq == freq + 1) {
            lruListNode.freqList = freqList.upFreqList;
            freqList.upFreqList.moveOtInsertLruListNodeToHead(lruListNode);
        } else {
            FreqListNode newFreqList = new FreqListNode();
            lruListNode.freqList = newFreqList;
            newFreqList.freq = freq + 1;
            newFreqList.moveOtInsertLruListNodeToHead(lruListNode);
            insertFreqList(freqList, newFreqList);
        }
        if (freqList.lruListEmpty()) {
            removeFreqList(freqList);
        }
        return lruListNode.val;
    }

    public void put(int key, int value) {
        LRUListNode lruListNode = map.get(key);
        if (lruListNode != null) {
            lruListNode.val = value;
            get(key);
            return;
        }
        lruListNode = new LRUListNode();
        lruListNode.key = key;
        lruListNode.val = value;

        if (nodeCount >= capacity) {
            FreqListNode freqList = bottomFreqList.upFreqList;
            LRUListNode lruList = freqList.tailLruList.pre;
            freqList.removeLruList(lruList);
            if (freqList.lruListEmpty()) {
                removeFreqList(freqList);
            }
            map.remove(lruList.key);
        } else {
            ++nodeCount;
        }

        FreqListNode freqList = bottomFreqList.upFreqList;
        map.put(key, lruListNode);
        if (freqList.freq == 1) {
            lruListNode.freqList = freqList;
            freqList.moveOtInsertLruListNodeToHead(lruListNode);
        } else {
            FreqListNode newFreqList = new FreqListNode();
            lruListNode.freqList = newFreqList;
            newFreqList.freq = 1;
            newFreqList.moveOtInsertLruListNodeToHead(lruListNode);
            insertFreqList(bottomFreqList, newFreqList);
        }
    }
}

3.6 Go版本

go 复制代码
type LRUListNode struct {
	Pre, Next *LRUListNode
	FreqList  *FreqListNode
	Key       int
	Val       int
}

type FreqListNode struct {
	HeadLruList, TailLruList *LRUListNode
	UpFreqList, DownFreqList *FreqListNode
	Freq                     int
}

func FreqConstructor() FreqListNode {
	headLruList, tailLruList := &LRUListNode{}, &LRUListNode{}
	headLruList.Next, tailLruList.Pre = tailLruList, headLruList
	return FreqListNode{headLruList, tailLruList, nil, nil, 0}
}

func (freqList *FreqListNode) RemoveLruList(lruList *LRUListNode) {
	lruList.Pre.Next, lruList.Next.Pre = lruList.Next, lruList.Pre
}

func (freqList *FreqListNode) moveOrInsertLruListNodeToHead(lruList *LRUListNode) {
	lruList.Pre, lruList.Next = freqList.HeadLruList, freqList.HeadLruList.Next
	freqList.HeadLruList.Next.Pre, freqList.HeadLruList.Next = lruList, lruList
}

func (freqList *FreqListNode) lruListEmpty() bool {
	return freqList.HeadLruList.Next == freqList.TailLruList
}

type LFUCache struct {
	nodeCount, capacity         int
	topFreqList, bottomFreqList *FreqListNode
	m                           map[int]*LRUListNode
}

func Constructor(capacity int) LFUCache {
	topFreqList, bottomFreqList := FreqConstructor(), FreqConstructor()
	topFreqList.DownFreqList, bottomFreqList.UpFreqList = &bottomFreqList, &topFreqList
	return LFUCache{0, capacity, &topFreqList, &bottomFreqList,
		map[int]*LRUListNode{}}
}

func (lfuCache *LFUCache) removeFreqList(freqList *FreqListNode) {
	freqList.DownFreqList.UpFreqList, freqList.UpFreqList.DownFreqList = freqList.UpFreqList, freqList.DownFreqList
}

func (lfuCache *LFUCache) insertFreqList(downFreqList, newFreqList *FreqListNode) {
	newFreqList.UpFreqList, newFreqList.DownFreqList = downFreqList.UpFreqList, downFreqList
	downFreqList.UpFreqList.DownFreqList, downFreqList.UpFreqList = newFreqList, newFreqList
}

func (lfuCache *LFUCache) Get(key int) int {
	lruListNode, ok := lfuCache.m[key]
	if !ok {
		return -1
	}
	freqList := lruListNode.FreqList
	freq := freqList.Freq
	freqList.RemoveLruList(lruListNode)
	if freqList.UpFreqList.Freq == freq+1 {
		lruListNode.FreqList = freqList.UpFreqList
		freqList.UpFreqList.moveOrInsertLruListNodeToHead(lruListNode)
	} else {
		newFreqList := FreqConstructor()
		lruListNode.FreqList, newFreqList.Freq = &newFreqList, freq+1
		newFreqList.moveOrInsertLruListNodeToHead(lruListNode)
		lfuCache.insertFreqList(freqList, &newFreqList)
	}
	if freqList.lruListEmpty() {
		lfuCache.removeFreqList(freqList)
	}
	return lruListNode.Val
}

func (lfuCache *LFUCache) Put(key int, value int) {
	lruListNode, ok := lfuCache.m[key]
	if ok {
		lfuCache.m[key].Val = value
		lfuCache.Get(key)
		return
	}
	lruListNode = &LRUListNode{}
	lruListNode.Key, lruListNode.Val = key, value
	if lfuCache.nodeCount >= lfuCache.capacity {
		freqList := lfuCache.bottomFreqList.UpFreqList
		lruList := freqList.TailLruList.Pre
		freqList.RemoveLruList(lruList)
		if freqList.lruListEmpty() {
			lfuCache.removeFreqList(freqList)
		}
		delete(lfuCache.m, lruList.Key)
	} else {
		lfuCache.nodeCount++
	}

	freqList := lfuCache.bottomFreqList.UpFreqList
	lfuCache.m[key] = lruListNode
	if freqList.Freq == 1 {
		lruListNode.FreqList = freqList
		freqList.moveOrInsertLruListNodeToHead(lruListNode)
	} else {
		newFreqList := FreqConstructor()
		lruListNode.FreqList, newFreqList.Freq = &newFreqList, 1
		newFreqList.moveOrInsertLruListNodeToHead(lruListNode)
		lfuCache.insertFreqList(lfuCache.bottomFreqList, &newFreqList)
	}
}

4 总结

本文详细地介绍了LRULFU的底层实现,并提供了对应的代码。

实际上,LRULFU只是最简单的缓存淘汰算法之一,现实应用中基本不会直接使用,因为:

  • LRU中,对突发性的遍历访问敏感,例如扫描一遍大列表,就很有可能把真正的热点数据挤出去
  • LFU中,初期频率低的热点难以进入长久存在,而长期运行后频率会膨胀,越来越难以淘汰

现实会使用改良后的算法,例如W-TinyLFU,结合了滑动窗口、TinyLFU和分段LRU,有兴趣的读者可以自行去了解。

5 附录

相关推荐
七夜zippoe2 天前
缓存策略:从本地到分布式架构设计与Python实战
分布式·python·缓存·lfu·lru
_OP_CHEN22 天前
【C++数据结构进阶】吃透 LRU Cache缓存算法:O (1) 效率缓存设计全解析
数据结构·数据库·c++·缓存·线程安全·内存优化·lru
EXtreme351 个月前
链表进化论:C语言实现带哨兵位的双向循环链表,解锁O(1)删除的奥秘
c语言·数据结构·性能优化·双向链表·编程进阶·链表教程
天赐学c语言1 个月前
12.2 - LRU缓存 && C语言内存布局
c++·算法·lru·内存布局
Light603 个月前
LinkedList 头尾插入与随机访问的隐蔽陷阱—— 领码课堂|Java 集合踩坑指南(6):
java·开发语言·性能优化·deque·双向链表·linkedlist·fail-fast
XueminXu4 个月前
Flink-SQL计算中的维表连接使用举例
sql·flink·lru·system_time·proctime·连接维表·关联维表
Pretend° Ω4 个月前
LRU缓存详解:用C语言实现高效数据管理
运维·c语言·spring·缓存·lru·双向链表
KarrySmile4 个月前
Day12--HOT100--23. 合并 K 个升序链表,146. LRU 缓存,94. 二叉树的中序遍历
数据结构·链表·二叉树·递归·hot100·lru·灵茶山艾府
triticale4 个月前
【计算机组成原理】LRU计数器问题
cache·计算机组成原理·lru