每日一题——LRU缓存机制的C语言实现详解

server/2025/2/27 6:46:02/

LRU缓存机制的C语言实现详解

    • 参考
    • 1. 数据结构设计
      • 双向链表节点
      • 哈希表节点
      • 哈希表
      • LRU缓存结构
    • 2. 初始化哈希表和双向链表
      • 哈希函数
      • 初始化哈希表
      • 初始化双向链表
      • 创建LRU缓存
    • 3. 更新双向链表
    • 4. 实现Get操作
    • 5. 实现Put操作
      • 更新节点值
      • 删除最久未使用节点
      • 插入或更新节点
    • 6. 释放缓存
      • 释放双向链表
      • 释放哈希表
      • 释放LRU缓存
    • 7. 完整代码及测试
      • 测试用例
      • 输出结果
    • 总结

参考

建议先参考下面的视频

在这里插入图片描述

1. 数据结构设计

双向链表节点

typedef struct DulNode {int key;           // 缓存项的键int value;         // 缓存项的值struct DulNode* pre;  // 前驱节点指针struct DulNode* next; // 后继节点指针
} DulNode;
  • 作用:维护缓存项的使用顺序,最近访问的节点靠近链表头部,未使用的节点靠近尾部。

哈希表节点

typedef struct HashTableNode {DulNode* node;          // 指向双向链表中的节点struct HashTableNode* next; // 处理哈希冲突的链表指针
} HashTableNode;
  • 作用:通过哈希表快速定位缓存项,键为key,值为对应的双向链表节点。

哈希表

typedef struct HashTable {int capacity;         // 哈希表容量(桶数量)hash_cal_f hash_func; // 哈希函数指针HashTableNode** bucket; // 桶数组(存储链表头节点)
} HashTable;
  • 作用:以O(1)时间复杂度查找键是否存在,结合双向链表实现LRU策略。

LRU缓存结构

typedef struct {int cur_node;       // 当前缓存中的节点数HashTable* hash_table; // 哈希表DulNode* head;      // 双向链表头节点(哨兵)DulNode* tail;      // 双向链表尾节点(哨兵)
} Solution;
  • 作用:整合哈希表和双向链表,封装LRU缓存的核心操作。

2. 初始化哈希表和双向链表

哈希函数

int hash_cal(int capacity, int val) {return val % capacity; // 简单取模哈希
}
  • 设计意图:将键值映射到哈希表的桶索引,均匀分布以减少冲突。

初始化哈希表

void InitHashTable(Solution* lruCache, int capacity) {HashTable* hash_table = malloc(sizeof(HashTable));hash_table->capacity = capacity;hash_table->hash_func = hash_cal;hash_table->bucket = calloc(capacity, sizeof(HashTableNode*));lruCache->hash_table = hash_table;
}
  • 关键点:动态分配桶数组并初始化为NULL,避免野指针问题。

初始化双向链表

void InitDoubleList(Solution* lruCache) {// 创建头尾哨兵节点并连接成环lruCache->head = (DulNode*)malloc(sizeof(DulNode));lruCache->tail = (DulNode*)malloc(sizeof(DulNode));lruCache->head->pre = lruCache->tail;lruCache->head->next = lruCache->tail;lruCache->tail->pre = lruCache->head;lruCache->tail->next = lruCache->head;
}
  • 设计优势:哨兵节点简化链表操作,无需处理头尾边界条件。

创建LRU缓存

Solution* SolutionCreate(int capacity) {Solution* lruCache = malloc(sizeof(Solution));lruCache->cur_node = 0;InitHashTable(lruCache, capacity);InitDoubleList(lruCache);return lruCache;
}
  • 流程:分配内存后,初始化哈希表和双向链表。

3. 更新双向链表

void DulLinkLinkUpdateNode(DulNode* head, DulNode* node) {if (node->pre == head) return; // 已在头部无需处理// 摘除节点node->pre->next = node->next;node->next->pre = node->pre;// 插入到头部node->pre = head;node->next = head->next;head->next->pre = node;head->next = node;
}
  • 作用:将节点移动到链表头部,表示最近被访问。

4. 实现Get操作

int SolutionGet(Solution* obj, int key) {if (obj->cur_node == 0) return -1;int h_index = obj->hash_table->hash_func(obj->hash_table->capacity, key);HashTableNode* hash_node = obj->hash_table->bucket[h_index];// 遍历哈希冲突链表while (hash_node && hash_node->node->key != key) hash_node = hash_node->next;if (hash_node) {DulLinkLinkUpdateNode(obj->head, hash_node->node); // 更新链表return hash_node->node->value;}return -1;
}
  • 时间复杂度:O(1)(哈希表平均情况)。

5. 实现Put操作

更新节点值

void LRUCacheUpdateNode(Solution* obj, int key, int value) {// 查找并更新值,随后移动节点到头部int h_index = obj->hash_table->hash_func(obj->hash_table->capacity, key);HashTableNode* hash_node = obj->hash_table->bucket[h_index];while (hash_node && hash_node->node->key != key) hash_node = hash_node->next;if (hash_node) {hash_node->node->value = value;DulLinkLinkUpdateNode(obj->head, hash_node->node);}
}

删除最久未使用节点

void LRUCacheDelLastNode(Solution* obj) {DulNode* del_node = obj->tail->pre; // 尾节点的前驱为LRU节点// 从哈希表中删除int h_index = obj->hash_table->hash_func(obj->hash_table->capacity, del_node->key);HashTableNode *pre = NULL, *cur = obj->hash_table->bucket[h_index];while (cur && cur->node->key != del_node->key) {pre = cur;cur = cur->next;}if (cur) {if (pre) pre->next = cur->next;else obj->hash_table->bucket[h_index] = cur->next;free(cur);}// 从链表中删除del_node->pre->next = del_node->next;del_node->next->pre = del_node->pre;free(del_node);obj->cur_node--;
}

插入或更新节点

void SolutionPut(Solution* obj, int key, int value) {if (obj->cur_node >= obj->hash_table->capacity)LRUCacheDelLastNode(obj); // 缓存满时删除LRU节点if (SolutionGet(obj, key) != -1) { // Key存在则更新LRUCacheUpdateNode(obj, key, value);return;}// 创建新节点并插入链表头部DulNode* new_dul_node = malloc(sizeof(DulNode));new_dul_node->key = key;new_dul_node->value = value;new_dul_node->pre = obj->head;new_dul_node->next = obj->head->next;obj->head->next->pre = new_dul_node;obj->head->next = new_dul_node;// 插入哈希表HashTableNode* new_hash_node = malloc(sizeof(HashTableNode));new_hash_node->node = new_dul_node;int h_index = obj->hash_table->hash_func(obj->hash_table->capacity, key);new_hash_node->next = obj->hash_table->bucket[h_index];obj->hash_table->bucket[h_index] = new_hash_node; // 头插法obj->cur_node++;
}

6. 释放缓存

释放双向链表

void DulLinkLinkFree(DulNode* head) {DulNode* cur = head->next;while (cur != head) {DulNode* tmp = cur;cur = cur->next;free(tmp);}free(head->pre); // 释放尾节点free(head);
}

释放哈希表

void HashTableNodeFree(Solution* obj) {for (int i = 0; i < obj->hash_table->capacity; i++) {HashTableNode* cur = obj->hash_table->bucket[i];while (cur) {HashTableNode* tmp = cur;cur = cur->next;free(tmp);}}free(obj->hash_table->bucket);
}

释放LRU缓存

void SolutionFree(Solution* obj) {DulLinkLinkFree(obj->head);HashTableNodeFree(obj);free(obj->hash_table);free(obj);
}

7. 完整代码及测试

测试用例

int main() {Solution* cache = SolutionCreate(2);SolutionPut(cache, 1, 1);SolutionPut(cache, 2, 2);printf("Get(1) -> %d\n", SolutionGet(cache, 1)); // 1SolutionPut(cache, 3, 3); // 淘汰2printf("Get(2) -> %d\n", SolutionGet(cache, 2)); // -1SolutionFree(cache);return 0;
}

输出结果

Get(1) -> 1
Get(2) -> -1

总结

  • 核心机制:通过双向链表维护访问顺序,哈希表实现快速查找。
  • 时间复杂度GetPut操作均摊时间复杂度为O(1)。
  • 优化点:哈希表采用链地址法处理冲突,双向链表使用哨兵节点简化操作。
  • 适用场景:高频访问的缓存系统,需快速淘汰最久未使用数据。
    反正以我的能力远远不能解决问题。还是只能勉强把代码看懂。这题还是很难得,感觉没有一天时间,完全搞不定。放弃了,放弃了。

http://www.ppmy.cn/server/170962.html

相关文章

内容中台的企业CMS架构是什么?

企业CMS模块化架构 现代企业内容管理系统的核心在于模块化架构设计&#xff0c;通过解耦内容生产、存储、发布等环节构建灵活的技术栈。动态/静态发布引擎整合技术使系统既能处理实时更新的产品文档&#xff0c;也能生成高并发的营销落地页&#xff0c;配合版本控制机制确保内…

20.<Spring图书管理系统①(登录+添加图书)>

PS&#xff1a;关于接口定义 接口定义&#xff0c;通常由服务器提供方来定义。 1.路径&#xff1a;自己定义 2.参数&#xff1a;根据需求考虑&#xff0c;我们这个接口功能完成需要哪些信息。 3.返回结果&#xff1a;考虑我们能为对方提供什么。站在对方角度考虑。 我们使用到的…

JAVA面试常见题_基础部分_springboot面试题

问题一 什么是 Spring Boot&#xff1f; 多年来&#xff0c;随着新功能的增加&#xff0c;spring 变得越来越复杂。只需访问 https://spring.io/projects 页面&#xff0c;我们就会看到可以在我们的应用程序中使用的所有 Spring 项目的不同功能。如果必须启动一个新的 Sprin…

C++ STL(二)deque

目录 deque&#xff08;双端队列&#xff09; 内存结构 验证地址不连续 使用详解 构造函数 元素访问 容量操作 修改 迭代器 迭代器失效 code实例 实现一个简单的deque deque&#xff08;双端队列&#xff09; std::deque&#xff08;双端队列&#xff09;是 C 标准…

word中对插入的图片修改背景色

关于对word中插入的图片修改背景色的问题&#xff0c;网上查了好多都无效&#xff0c;可能是由于word版本的问题&#xff0c;本人word版本为2019版&#xff0c;亲测有效的修改图片背景色为透明的小技巧&#xff1a; 选中图片-设置图片格式-最右面图标&#xff0c;选择图片校正…

JavaEE 编写Java程序,实现简单的echo程序(网络编程TCP实践练习)

Java TCP 客户端/服务器开发深度解析 一、客户端代码全注释 public class TcpClient {private Socket socket null; // TCP通信核心对象// 构造函数&#xff1a;建立TCP连接public TcpClient(String ip, int port) throws IOException {// 创建Socket时会自动进行三次握手s…

20250221 NLP

1.向量和嵌入 https://zhuanlan.zhihu.com/p/634237861 encoder的输入就是向量&#xff0c;提前嵌入为向量 二.多模态文本嵌入向量过程 1.文本预处理 文本tokenizer之前需要预处理吗&#xff1f; 是的&#xff0c;文本tokenizer之前通常需要对文本进行预处理。预处理步骤可…

深度学习批次数据处理的理解

基础介绍 在计算机视觉深度学习网络中&#xff0c;在训练阶段数据输入通常是一个批次&#xff0c;即不是一次输入单张图片&#xff0c;而是一次性输入多张图片&#xff0c;而神经网络的结构内部一次只能处理一张图片&#xff0c;这时候很自然就会考虑为什么要这样的输入&#…