LRU动态淘汰缓存算法

目录

实验目的及内容

解题思路

实验代码及注释

测试结果截图

心得体会

实验目的及内容

设计实现一个LRU缓存淘汰算法( Least Recently Used,最近最少使用原则:若缓存满了,首先删除最久没有使用过的数据),要求算法时间复杂度为O(1)。(提示:双向链表+哈希表

解题思路

1.LRU算法的基本思想(图解)

LRU 缓存淘汰算法是一种常用淘汰策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是最「有用的」,很久都没用过的数据应该是 无用的,内存满了就优先删那些很久没用过的数据。

所以,若是用链表来存储缓存数据,应有如下规则:

  • 每次新插入数据的时候将新数据插到链表的头部
  • 每次缓存命中(即数据被访问),则将数据移到链表头部
  • 链表满的时候,就将链表尾部的数据丢弃
初始缓存链表(右边是头)
访问用户1信息后(右边是头)
依次添加5/6号缓存,2号缓存被淘汰(右边是头)

2.数据结构设计

这题实际上需要我们自己来设计数据结构“哈希链表”:这个数据结构实现了两个基本 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。!注意,存入get 和 查找put 必须都是 O(1) 的时间复杂度!

基于以上的需求分析,我们会有以下思路:

双向链表来存储所有“缓存数据”,方便修改和删除(时间复杂度为O(1));

哈希表来实现对双向链表中数据的快速查找;为了方便,可以用C++的<unordered_map>模板容器实现哈希表结构,为此,可以定义如下模板:unordered_map<int, ListNode*> cache,其中key是int型,表示用户序号,而值是ListNode*指针,指向存储该用户缓存信息的链节点地址;

最后将哈希表和双向链表结合,构成“哈希链表”:借助哈希表赋予链表快速查找的特性:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。既能实现O(1)的高效查找,又可以实现高效的删除;

实验代码及注释

特别注意:当缓存容量已满,我们不仅仅要删除最后一个 ListNode 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 ListNode 得到。所以我们的ListNode节点不仅要存储value还要存储对应的key值!

#include <iostream>
#include <unordered_map>
using namespace std;
// 双向链节点
struct ListNode
{
    int key;   // 键
    int value; // 值
    ListNode *prev;
    ListNode *next;
    ListNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
// 定义LRU缓存类
class LRUCache
{
private:
    int capacity;                       // 缓存容量
    unordered_map<int, ListNode *> map; // 哈希表,用于快速查找结点
    ListNode *head;                     // 双向链表的头结点
    ListNode *tail;                     // 双向链表的尾结点

    // 将结点添加到链表头部
    void addToHead(ListNode *node)
    {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    // 删除链表中的结点
    void removeNode(ListNode *node)
    {
        node->prev->next = node->next;
        node->next->prev = node->prev;
        // delete node;
    }

    // 将结点移动到链表头部(每次用户访问)
    void moveToHead(ListNode *node)
    {
        removeNode(node);
        addToHead(node);
    }

public:
    // 构造函数,初始化LRU缓存对象,创建一个空的双向链表
    LRUCache(int capacity)
    {
        this->capacity = capacity;
        head = new ListNode(-1, -1);
        tail = new ListNode(-1, -1);
        head->next = tail;
        tail->prev = head;
    }

    // 析构函数
    ~LRUCache()
    {
        // 循环遍历cache中的每个键值对,使用delete删除每个节点并释放其内存
        for (auto &pair : map) // auto& 是范围循环中的类型推断机制,用于自动推断迭代变量 pair
        {
            delete pair.second; // pair.first是键,pair.second是值(表示缓存结点的指针)
        }
        // 释放cache中的每个ListNode对象的内存
        map.clear(); // 这个函数仅仅清楚元素的值,指针所占用的内存没有释放!
        // 清空整个unordered_map,以确保所有ListNode对象的内存都被正确释放
        delete head;
        delete tail;
    }

    // 添加或更新缓存
    void put(int key, int value)
    {
        if (map.find(key) == map.end())
        { // 从头到尾遍历链表,如果返回 map.end(),表示未找到指定的键
            if (map.size() >= capacity)
            { // 缓存已满,删除最久未使用的结点(尾结点的前一个节点)
                ListNode *lastNode = tail->prev;
                map.erase(lastNode->key); // 从哈希表 map 中删除指定的键 key 对应的键值对
                removeNode(lastNode);     // 从双向链表移除这个结点
                delete lastNode;
            }
            // 创建新节点
            ListNode *newNode = new ListNode(key, value);
            map[key] = newNode; // 将键值对(key, newNode)插入或更新到unordered_map容器 map 中
            addToHead(newNode);
        }
        else
        {                              // 缓存中已存在该键,更新值并将结点移到链表头部,更新最近访问的结点
            ListNode *node = map[key]; // 从哈希表 map 中获取给定键 key 对应的结点指针
            node->value = value;       // 更新该键对应结点的值
            moveToHead(node);
        }
    }
    // 获取缓存中的值
    int get(int key)
    {
        // map.find(key) 的返回值是一个迭代器,它指向一个键为 key 的结点(键值对)
        if (map.find(key) == map.end())
        {
            return -1; // 表示缓存中不存在该键
        }

        ListNode *node = map[key];
        moveToHead(node); // 将结点移动到链表头部
        return node->value;
    }
    // 打印缓存内容
    void print()
    {
        ListNode *p;
        for (p = head->next; p->next != tail; p = p->next)
        {
            cout << "(" << p->key << "," << p->value << ")->";
        }
        cout << "(" << p->key << "," << p->value << ")";
    }
};
int main()
{
    // 定义一个容量为5的LRUCache
    LRUCache cache(5);
    cout << "the capacity of cache is 5!\n";

    // 添加用户1至用户4的缓存信息
    cache.put(1, 100);
    cache.put(2, 200);
    cache.put(3, 300);
    cache.put(4, 400);
    cout << "the current cache is:";
    cache.print();

    // 访问用户1
    cout << "\n\n访问用户1的缓存信息\n";
    cout << "用户1的值: " << cache.get(1) << endl;
    cout << "the current cache is:";
    cache.print();

    // 先后添加用户5和用户6的缓存,此时用户2将被淘汰
    cout << "\n\n加入用户5的缓存信息\n";
    cache.put(5, 500);
    cout << "the current cache is:";
    cache.print();
    cout << "\n\n加入用户6的缓存信息\n";
    cache.put(6, 600);
    cout << "the current cache is:";
    cache.print();
    // 访问用户2,应返回-1
    cout << "\n\n访问用户2的缓存信息\n";
    cout << "用户2的值: " << cache.get(2) << endl;
    // 访问用户6
    cout << "\n访问用户6的缓存信息\n";
    cout << "用户6的值: " << cache.get(6) << endl;
    return 0;
}

测试结果截图

心得体会

这题实际上需要我们自己来设计数据结构“哈希链表”,借助哈希表赋予链表快速查找的特性:可以快速查找某个 key 是否存在缓存(链表)中,同时借助链表实现快速删除、添加节点。既能实现O(1)的高效查找,又可以实现高效的删除;

最后感谢同学的友情分享,对本次博客撰写帮助颇多!

实现LRU缓存淘汰算法的一种常用方法是使用哈希表和双向链表结合实现。具体实现步骤如下: 1. 使用一个哈希表来存储缓存中的键值对,其中键为缓存的键,值为该键对应的节点在双向链表中的指针。 2. 使用一个双向链表来存储缓存中的键值对,每个节点包含该节点对应的键、值以及前驱和后继指针。 3. 当有新的键值对被访问时,首先在哈希表中查找该键是否存在,如果存在,则将该键所对应的节点移到链表头部,表示最近被访问过;如果不存在,则在哈希表和链表中分别添加该键值对以及节点,并将该节点插入到链表头部。 4. 当缓存空间不足时,淘汰链表尾部的节点,并在哈希表中删除对应的键值对。 下面是一个Python实现的LRU缓存淘汰算法的代码示例: ```python class LRUCache: def __init__(self, capacity: int): self.capacity = capacity self.cache = {} self.head = Node(0, 0) self.tail = Node(0, 0) self.head.next = self.tail self.tail.prev = self.head def get(self, key: int) -> int: if key in self.cache: node = self.cache[key] self._remove(node) self._add(node) return node.val else: return -1 def put(self, key: int, value: int) -> None: if key in self.cache: self._remove(self.cache[key]) node = Node(key, value) self.cache[key] = node self._add(node) if len(self.cache) > self.capacity: node = self.head.next self._remove(node) del self.cache[node.key] def _add(self, node): p = self.tail.prev p.next = node node.prev = p node.next = self.tail self.tail.prev = node def _remove(self, node): p = node.prev n = node.next p.next = n n.prev = p class Node: def __init__(self, key, val): self.key = key self.val = val self.prev = None self.next = None ``` 在这个实现中,我们使用了一个双向链表来维护缓存中节点的顺序,其中head和tail分别是链表的头节点和尾节点。同时,我们使用了一个哈希表来查询节点是否存在以及快速删除节点。当有新的节点被访问时,我们将其移到链表头部,并且当缓存空间不足时,我们淘汰链表尾部的节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值