【设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构】


一、什么是LRU?

LRU是Least Recently Used的缩写,意为最近最少使用。它是一种缓存淘汰策略,用于在缓存满时确定要被替换的数据块。LRU算法认为,最近被访问的数据在将来被访问的概率更高,因此它会优先淘汰最近最少被使用的数据块,以给新的数据块腾出空间。

如图所示:

在这里插入图片描述

  1. 先来3个元素进入该队列
    在这里插入图片描述

  2. 此时来了新的元素,因为此时队列中每个元素的使用的次数都相同(都是1),所以会按照LFU的策略淘汰(即淘汰掉最老的那个)
    在这里插入图片描述

  3. 此时又来了新的元素,而且是队列是已经存在的,就会将该元素调整为最新的位置。
    在这里插入图片描述

  4. 如果此时又来了新的元素,还是”咯咯“,由于”咯咯“已经处于最新的位置,所以大家位置都不变。
    在这里插入图片描述

  5. 同理,一直进行上述的循环


二、LinkedHashMap 实现LRU缓存

以力扣的算法题为例子:


力扣146. LRU 缓存


在 jdk 官方的介绍中可以看出,该数据结构天生适合实现 LRU。

在这里插入图片描述

代码示例一:


/**
 * 利用继承 LinkedHashMap 的方式实现LRU缓存
 */


class LRUCache extends LinkedHashMap<Integer, Integer> {
   
   
	// 缓存容量
    private int capacity;

    public LRUCache(int capacity) {
   
   
    	// 初始化
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
   
   
        return super.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
   
   
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
   
   
    	// 重写比较方法
        return super.size() > capacity;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

代码示例二:

/**
 * 利用 LinkedHashMap 特点的方式实现LRU缓存
 */
 
class LRUCache {
   
   
   // 额定容量
    private final int CAPACITY;
    // 使用 LinkedHashMap 的有序排重特点达到要求
    private final 
<think>嗯,我现在需要设计一个LRU缓存数据结构,用C语言实现。用户要求get和put操作的时间复杂度都是O(1)。那我得先回忆一下LRU的原理。LRU最近最少使用,当缓存满了的时候,淘汰最久未使用的那个数据。 首先,怎么实现O(1)的get和put呢?通常,哈希表可以提供O(1)的查找,但哈希表本身无法维护元素的顺序。而链表可以方便地维护访问顺序,但查找是O(n)的。所以可能需要结合这两种数据结构。比如,使用哈希表来快速定位节点,而用双向链表来维护使用顺序。每次访问一个节点时,把它移到链表头部,这样链表尾部就是最久未使用的,需要淘汰的时候删除尾部节点。 那具体来说,数据结构应该包含一个哈希表和一个双向链表。哈希表的键是缓存的key,值是对应的链表节点。每个节点包含key、value,以及前驱和后继指针。这样,当get一个key时,通过哈希表找到节点,然后将该节点移动到链表头部,这样时间复杂度是O(1)。put的时候,如果key存在,更新值移动到头部;如果不存在,创建新节点插入头部,同时如果超过容量,就删除尾部节点,在哈希表中删除对应的key。 但C语言里没有内置的哈希表,需要自己实现。或者,可以用一个固定大小的数组,或者用开放寻址法来处理冲突。但为了简单起见,可能应该使用链表法来处理哈希冲突。或者,考虑到题目中的capacity是初始化时给定的正整数,可以设计哈希表的桶的数量为capacity,或者更大一些,以减少冲突。或者,更简单的方式,可以使用一个结构体数组来作为哈希表,每个节点包含key和对应的链表节点指针。不过这样的话,查找可能需要遍历链表,这样在哈希冲突的情况下,时间复杂度可能退化为O(n),但题目要求平均O(1),所以需要好的哈希函数和合适的哈希表大小,尽量减少冲突。 或者,是否可以使用更高效的结构?比如,C语言中可以用uthash这样的库,但用户可能需要自己实现。所以可能需要自己实现一个哈希表,或者用其他方式。 或者,或许可以采用更简单的方式,比如每个链表节点同时作为哈希表的一个元素,使用双向链表和哈希表的结合。比如,每个节点包含key、value、prev、next,然后用哈希表来保存key到节点的映射。这样,当需要查找key的时候,直接通过哈希表找到对应的节点,然后调整链表位置。 那现在问题是如何在C语言中实现这个哈希表。假设capacity是给定的,那么可以预先分配一个足够大的哈希表数组,或者用链表来处理冲突。但为了时间效率,可能需要一个高效的哈希表实现。比如,使用开放寻址法,线性探测或者双重哈希。或者,考虑到每个节点的key是整数,可以使用一个直接寻址的方式,但这样的话如果key的范围很大,比如超过capacity的话,会浪费内存,所以可能不太行。因此,可能需要使用哈希函数将key映射到哈希表的索引,比如取模运算。例如,哈希表的size可以是一个质数,比如取大于capacity的最小质数,这样可以减少冲突。 或者,可能更简单的方式是,用双向链表维护访问顺序,同时使用一个动态数组或指针数组来作为哈希表,其中每个元素指向对应的链表节点。但这样当key的范围不确定时,无法直接使用数组。所以必须使用哈希表结构。 综上,可能需要自己实现一个哈希表结构,处理key到链表节点的映射。这可能比较复杂。或者,是否有其他方法? 或者,可以简化问题,假设每个节点的key是唯一的,且哈希表的实现可以足够高效。比如,在LRU缓存中,每个节点的key是唯一的,因此可以用一个固定大小的数组,每个数组元素是一个链表头,来处理冲突。每次查找时,先计算哈希值,然后在对应的链表中查找节点。这样,在平均情况下,每个链表的长度较短,可以视为O(1)的时间。 现在,我需要考虑如何具体实现这个结构。假设哈希表的每个bucket是一个链表,每个节点保存key和对应的链表节点指针。当get操作时,先通过哈希函数找到对应的bucket,然后遍历该bucket中的链表,找到对应的key,然后获取对应的节点指针,将该节点移动到双向链表的头部。 同样,put操作时,如果key存在,则更新value移动节点到头部;如果不存在,则创建新节点,插入到双向链表的头部,将该节点添加到哈希表中。如果超过容量,则删除双向链表的尾部节点,从哈希表中删除对应的key。 这似乎可行。但具体在C语言中如何实现呢? 首先,定义双向链表节点的结构体: struct DLinkedNode { int key; int value; struct DLinkedNode* prev; struct DLinkedNode* next; }; 然后,LRUCache的结构体需要包含当前大小、容量、哈希表,以及双向链表的头尾指针。例如: typedef struct { int capacity; int size; struct DLinkedNode* head; // 哨兵头节点 struct DLinkedNode* tail; // 哨兵尾节点 // 哈希表,key到节点的映射 // 这里需要自己实现哈希表,比如一个数组,每个元素是链表头 // 这里假设哈希表的结构是哈希节点组成的链表,每个哈希节点保存key和对应的DLinkedNode指针 struct HashNode** hashTable; int hashSize; // 哈希表的大小,比如取一个质数 } LRUCache; 但是,这样的HashNode结构需要单独定义: struct HashNode { int key; struct DLinkedNode* node; struct HashNode* next; }; 然后,哈希表的每个bucket是一个HashNode的链表。当需要查找某个key时,先计算哈希值,找到对应的bucket,然后遍历该bucket的链表,找到对应的HashNode,取出DLinkedNode指针。 这可能比较麻烦,但可以实现。 或者,是否可以将哈希表直接存储为指向DLinkedNode的指针?比如,使用开放寻址法,每个键值对直接存在哈希表的槽中。但如何处理冲突? 或者,是否可以使用一个简单的数组,每个元素是一个指向DLinkedNode的指针,然后通过哈希函数将key映射到数组的索引。例如,假设数组的大小足够大,或者动态调整。这可能会有冲突的问题,但可以处理。 不过,在C语言中,实现一个高效的哈希表确实比较麻烦。或许,为了简化代码,可以采用其他方式,比如用一个固定大小的哈希表数组,每个元素是一个指针,指向对应的DLinkedNode。或者,对于本题,因为capacity是给定的,我们可以将哈希表的大小设为capacity,或者更大的值,以减少冲突的可能性。例如,使用一个较大的质数作为哈希表的大小,然后对于每个key,使用取模运算得到哈希索引。 例如,定义哈希表的结构: struct HashEntry { int key; struct DLinkedNode *node; UT_hash_handle hh; // 假设使用uthash库 }; 但是,如果用户不允许使用第三方库,那么必须自己实现哈希表。这可能比较复杂。 或者,可以换一种思路,使用双向链表来维护访问顺序,而用数组或者动态数组来存储每个key对应的节点指针。但这样,如果key的范围很大,数组会非常大,不可行。所以,回到哈希表的思路。 可能,这里为了简化问题,可以假设哈希表足够大,且使用开放寻址法处理冲突。例如,哈希表的容量设置为两倍的capacity,这样可以减少冲突的概率。或者,使用线性探测来处理冲突。 但实现起来代码量可能较大。因此,或许我应该采用链表法来处理冲突,每个bucket是一个链表。 现在,关于双向链表的操作。需要维护一个最近访问的顺序,头部是最新访问的节点,尾部是最久未使用的节点。每次访问节点时,将其移动到头部。当需要添加新节点时,插入到头部。当容量满了,删除尾部节点,同时从哈希表中删除对应的key。 那么,双向链表需要支持快速的插入头部和删除尾部的操作,这可以通过维护头尾哨兵节点来简化操作。例如,初始化时,head和tail都是哨兵节点,它们的next和prev互相指向对方。这样,插入节点到头部,只需要在head的后面插入,而尾部节点就是tail->prev。 现在,回到哈希表的实现。假设我们使用链表法处理冲突,每个bucket是一个链表,每个节点保存key和对应的DLinkedNode指针。那么,当需要查找某个key时,先计算哈希值,找到对应的bucket,然后遍历该链表查找key。如果存在,则返回对应的DLinkedNode指针;否则返回NULL。 这可能的时间复杂度取决于哈希表的负载因子。如果哈希表的bucket数量足够大,比如是capacity的两倍,那么每个链表的平均长度较低,可以视为O(1)的时间。 现在,具体实现步骤如下: 1. 定义双向链表节点结构体。 2. 定义LRUCache结构体,包含capacity、当前size、哈希表、双向链表的头尾指针。 3. 实现哈希表的创建、插入、查找、删除操作。 4. 实现LRUCache的初始化函数LRUCacheCreate。 5. 实现get和put函数。 关于哈希函数的选择,可以选择简单的取模运算。例如,哈希表的bucket数量可以是capacity的两倍,或者一个质数,比如取大于capacity的最小质数。例如,假设哈希表的大小是1024,或者动态根据capacity调整。但为了简化,可以固定哈希表的大小为一个足够大的质数,比如10007,或者让哈希表的bucket数量等于capacity的某个倍数。或者,在创建LRUCache时,根据capacity动态确定哈希表的大小。 这里,为了代码的简单,假设哈希表的大小是capacity的两倍,或者直接等于capacity。例如,在LRUCache结构体中,有一个哈希表数组,每个元素是一个链表头节点。但动态数组在C中处理起来比较麻烦,所以可能需要使用指针数组。或者,在创建LRUCache时,动态分配哈希表的内存。 或者,可以简化哈希表的实现,使用一个动态的链表数组。例如,哈希表的每个bucket是一个链表,每个节点保存key和对应的DLinkedNode指针。 但是,这样的实现可能需要较多的代码。例如,当要查找key时,需要: 1. 计算哈希值h = key % hashSize。 2. 遍历哈希表[h]处的链表,查找是否有节点的key等于目标key。 3. 如果找到,返回对应的DLinkedNode指针;否则返回NULL。 同样的,当插入一个新的key时,计算哈希值,在对应的链表中插入一个节点。当删除某个key时,同样需要找到对应的链表中的节点删除。 这可能比较麻烦,但必须实现。 现在,我需要考虑如何具体编写这些函数。 首先,定义结构体: // 双向链表节点 typedef struct DLinkedNode { int key; int value; struct DLinkedNode* prev; struct DLinkedNode* next; } DLinkedNode; // 哈希表节点,用于处理冲突的链表 typedef struct HashNode { int key; DLinkedNode* cacheNode; struct HashNode* next; } HashNode; // LRUCache结构体 typedef struct { int capacity; int size; DLinkedNode* head; // 哨兵头节点 DLinkedNode* tail; // 哨兵尾节点 HashNode** hashTable; // 哈希表数组,每个元素是HashNode指针 int hashSize; // 哈希表的大小,例如取一个质数 } LRUCache; 然后,LRUCache的初始化函数需要创建head和tail哨兵节点,初始化哈希表。 例如: LRUCache* lRUCacheCreate(int capacity) { LRUCache* cache = (LRUCache*)malloc(sizeof(LRUCache)); cache->capacity = capacity; cache->size = 0; // 初始化双向链表 cache->head = (DLinkedNode*)malloc(sizeof(DLinkedNode)); cache->tail = (DLinkedNode*)malloc(sizeof(DLinkedNode)); cache->head->prev = NULL; cache->head->next = cache->tail; cache->tail->prev = cache->head; cache->tail->next = NULL; // 初始化哈希表,这里假设哈希表的大小为capacity的两倍,比如2*capacity cache->hashSize = capacity * 2; cache->hashTable = (HashNode**)calloc(cache->hashSize, sizeof(HashNode*)); return cache; } 这里,哈希表的大小设为capacity的两倍,以减少冲突的可能性。初始化为calloc,每个元素初始化为NULL。 接下来,哈希表的查找函数。例如,查找key对应的DLinkedNode: DLinkedNode* findInHashTable(LRUCache* obj, int key) { int idx = key % obj->hashSize; HashNode* entry = obj->hashTable[idx]; while (entry != NULL) { if (entry->key == key) { return entry->cacheNode; } entry = entry->next; } return NULL; } 然后,当需要将某个key添加到哈希表中时,需要创建HashNode插入到对应的链表中: void insertIntoHashTable(LRUCache* obj, int key, DLinkedNode* node) { int idx = key % obj->hashSize; HashNode* newEntry = (HashNode*)malloc(sizeof(HashNode)); newEntry->key = key; newEntry->cacheNode = node; newEntry->next = obj->hashTable[idx]; obj->hashTable[idx] = newEntry; } 当需要从哈希表中删除某个key时: void removeFromHashTable(LRUCache* obj, int key) { int idx = key % obj->hashSize; HashNode* curr = obj->hashTable[idx]; HashNode* prev = NULL; while (curr != NULL) { if (curr->key == key) { if (prev == NULL) { obj->hashTable[idx] = curr->next; } else { prev->next = curr->next; } free(curr); return; } prev = curr; curr = curr->next; } } 这样,哈希表的操作基本实现。 接下来,双向链表的操作。当需要将某个节点移动到头部时,需要先将其从链表中断开,然后插入到head之后。 例如,函数: // 将节点移动到头部 void moveToHead(LRUCache* obj, DLinkedNode* node) { // 先从链表中移除节点 node->prev->next = node->next; node->next->prev = node->prev; // 插入到head之后 node->prev = obj->head; node->next = obj->head->next; obj->head->next->prev = node; obj->head->next = node; } // 添加新节点到头部 void addToHead(LRUCache* obj, DLinkedNode* node) { node->prev = obj->head; node->next = obj->head->next; obj->head->next->prev = node; obj->head->next = node; } // 删除尾部节点 DLinkedNode* removeTail(LRUCache* obj) { DLinkedNode* node = obj->tail->prev; node->prev->next = obj->tail; obj->tail->prev = node->prev; return node; } 然后,get函数的实现: int lRUCacheGet(LRUCache* obj, int key) { DLinkedNode* node = findInHashTable(obj, key); if (node == NULL) { return -1; } // 移动到头部 moveToHead(obj, node); return node->value; } put函数的实现: void lRUCachePut(LRUCache* obj, int key, int value) { DLinkedNode* node = findInHashTable(obj, key); if (node != NULL) { // 存在,更新值,移动到头部 node->value = value; moveToHead(obj, node); } else { // 不存在,创建新节点 DLinkedNode* newNode = (DLinkedNode*)malloc(sizeof(DLinkedNode)); newNode->key = key; newNode->value = value; // 插入到哈希表 insertIntoHashTable(obj, key, newNode); // 添加到链表头部 addToHead(obj, newNode); obj->size++; // 如果超过容量,删除尾部节点 if (obj->size > obj->capacity) { DLinkedNode* tailNode = removeTail(obj); // 从哈希表中删除对应的key removeFromHashTable(obj, tailNode->key); free(tailNode); obj->size--; } } } 这样,可能已经完成了大部分的代码。但需要注意内存管理的问题,例如在删除节点时,要同时从哈希表中删除对应的条目,释放内存。 但这里可能存在一个问题:当插入一个新节点时,如果哈希表中已经存在相同的key(虽然根据LRUCache的put逻辑,这种情况应该已经被处理,即如果key存在,则更新值),所以findInHashTable返回NULL时才创建新节点。因此,在put函数中的else分支是正确的。 另外,在哈希表的处理中,insertIntoHashTable函数会为每个新节点的key添加一个HashNode到哈希表中。当key已经存在时,put函数会先处理,所以不会出现重复。 但需要注意,当更新存在的节点时,是否要更新哈希表?不需要,因为HashNode中的key和DLinkedNode中的key是一致的,当key存在时,直接更新DLinkedNode的值即可。 现在,考虑测试情况。例如,当capacity为2,put(1,1),put(2,2),get(1)返回1,此时再put(3,3),导致key 2被淘汰。get(2)返回-1。 但是,这里可能存在一个问题:当removeTail的时候,获取的是tailNode,但此时Hash表中可能还存在对应的key。因此,需要调用removeFromHashTable来删除哈希表中的对应项。这在代码中已经处理。 现在,考虑哈希冲突的情况。例如,两个不同的key可能被哈希到同一个bucket中。当需要删除其中一个时,哈希表的remove函数能够正确找到对应的HashNode删除。 综上,这样的实现应该满足O(1)的时间复杂度,因为哈希表的查找、插入、删除平均是O(1),双向链表的操作也是O(1)。 不过,在C语言中,这样的实现可能比较复杂,特别是哈希表的部分。有没有更简便的方法? 另一种思路是,因为capacity是已知的,可以预先分配一个足够大的数组,直接以key为索引。但这样当key的范围很大时会占用过多内存,不可行。所以,必须使用哈希表。 综上,代码的大体结构应该是正确的。但需要检查是否有错误。 例如,当调用findInHashTable时,返回的是DLinkedNode指针,正确。当移动节点到头部时,是否正确处理了链表指针。例如,moveToHead函数中的操作是否正确? 假设原来的链表结构是 head <-> node1 <-> node2 <-> tail。当移动node1到头部,操作应该是将其从原位置移除,然后插入到head后面。例如: node1->prev = head; node1->next = head->next; head->next->prev = node1; head->next = node1; 这应该是正确的。 此外,当添加新节点时,使用addToHead函数,正确插入到head之后。 当删除尾部节点时,removeTail函数返回的是tail的前驱节点,然后将其从链表中删除。 此时,需要释放该节点的内存,从哈希表中删除对应的key。这部分是否正确? 是的,在put函数中,当超过容量时,删除tailNode,从哈希表中移除对应的key,free该节点。 现在,关于哈希表的内存管理:每个HashNode在插入时被创建,当对应的DLinkedNode被删除时,HashNode也应该被删除。例如,在removeFromHashTable函数中,会找到对应的HashNode释放内存。 现在,有没有可能内存泄漏?例如,当调用removeTail时,是否忘记释放HashNode? 在removeFromHashTable中,找到对应的HashNode,free掉。而DLinkedNode在removeTail之后被free。所以这部分是正确的。 此外,在LRUCache的销毁函数中,应该释放所有分配的内存,包括哈希表中的所有HashNode,双向链表的所有节点,以及LRUCache结构体本身。但用户可能没有要求实现销毁函数,但为了完整性可能需要补充。 但题目中的要求只是实现LRUCache类,即结构体和对应的函数,可能不需要处理销毁部分。但用户给出的问题中的C语言代码可能需要释放内存,否则会有内存泄漏。但在在线编程题中,可能不会检查内存泄漏,只要功能正确即可。不过,正确的实现应该处理这些。 例如,当用户调用lRUCacheFree时,需要释放所有内存: void lRUCacheFree(LRUCache* obj) { // 释放所有DLinkedNode DLinkedNode* curr = obj->head->next; while (curr != obj->tail) { DLinkedNode* temp = curr; curr = curr->next; free(temp); } free(obj->head); free(obj->tail); // 释放哈希表中的所有HashNode for (int i = 0; i < obj->hashSize; i++) { HashNode* entry = obj->hashTable[i]; while (entry != NULL) { HashNode* temp = entry; entry = entry->next; free(temp); } } free(obj->hashTable); free(obj); } 这可能比较复杂,但需要确保所有动态分配的内存都被释放。 现在,回到原来的问题,用户需要的是用C语言实现代码。综上,整个代码的结构大致如此。但需要编写完整的代码,包括所有函数和结构体定义。 但可能存在一些错误,比如哈希表的实现是否正确,或者双向链表的操作是否正确。 例如,在findInHashTable函数中,返回的是DLinkedNode指针,而哈希表的每个HashNode保存的是该指针。当调用get函数时,正确返回该节点的value,移动节点到头部。 在put函数中,当插入新节点时,是否在哈希表中正确添加了对应的HashNode? 是的,insertIntoHashTable函数被调用,将key和node添加到哈希表中。 当删除节点时,removeFromHashTable被调用,正确删除了对应的key。 现在,测试代码是否正常工作。例如,以下测试用例: capacity = 2 put(1,1); put(2,2); get(1) → 1; put(3,3); get(2) → -1; get(3) →3; put(4,4); get(1)→-1; get(3)→3; get(4)→4; 此时,在put(3,3)时,容量已满,需要删除最久未使用的节点。此时,链表中的顺序是 1被访问过,所以最近访问的是1,最久未使用的是2。所以,插入3时,删除2。接着,put(4,4)时,容量为2,此时链表中的最近访问是3,最久未使用的是1,所以删除1,插入4。get(1)返回-1。 这样,代码是否能正确处理? 在代码中,当插入新节点时,添加它到头部。当put(1,1)和put(2,2)后,链表顺序是 head <-> 2 <-> 1 <-> tail?或者头插入方式,所以插入顺序是put(1,1)时添加到头部,所以链表是 head <->1<-> tail。然后put(2,2)时,添加到头部,链表变为 head <->2<->1<-> tail。此时,get(1)将移动1到头部,链表变为 head <->1<->2<->tail。此时,当put(3,3)时,容量已满,所以删除尾部节点,即2。哈希表中删除key 2,链表变为 head <->1<->3<->tail。这样,后续get(2)返回-1。是的,这应该正确。 综上,代码逻辑正确。 现在,将上述思路转化为C代码。由于代码较长,需要注意所有函数的正确性。 可能的代码实现如下: 首先,包含必要的头文件: #include <stdio.h> #include <stdlib.h> 然后,结构体定义: typedef struct DLinkedNode { int key; int value; struct DLinkedNode* prev; struct DLinkedNode* next; } DLinkedNode; typedef struct HashNode { int key; DLinkedNode* cacheNode; struct HashNode* next; } HashNode; typedef struct { int capacity; int size; DLinkedNode* head; DLinkedNode* tail; HashNode** hashTable; int hashSize; } LRUCache; 然后,LRUCache的创建函数: LRUCache* lRUCacheCreate(int capacity) { LRUCache* cache = (LRUCache*)malloc(sizeof(LRUCache)); cache->capacity = capacity; cache->size = 0; cache->head = (DLinkedNode*)malloc(sizeof(DLinkedNode)); cache->tail = (DLinkedNode*)malloc(sizeof(DLinkedNode)); cache->head->prev = NULL; cache->head->next = cache->tail; cache->tail->prev = cache->head; cache->tail->next = NULL; // 设置哈希表的大小为capacity的两倍,或者选择一个质数,这里简化处理 cache->hashSize = capacity * 2; cache->hashTable = (HashNode**)calloc(cache->hashSize, sizeof(HashNode*)); return cache; } 然后,辅助函数: DLinkedNode* findInHashTable(LRUCache* obj, int key) { int idx = key % obj->hashSize; HashNode* entry = obj->hashTable[idx]; while (entry != NULL) { if (entry->key == key) { return entry->cacheNode; } entry = entry->next; } return NULL; } void insertIntoHashTable(LRUCache* obj, int key, DLinkedNode* node) { int idx = key % obj->hashSize; HashNode* newEntry = (HashNode*)malloc(sizeof(HashNode)); newEntry->key = key; newEntry->cacheNode = node; newEntry->next = obj->hashTable[idx]; obj->hashTable[idx] = newEntry; } void removeFromHashTable(LRUCache* obj, int key) { int idx = key % obj->hashSize; HashNode* curr = obj->hashTable[idx]; HashNode* prev = NULL; while (curr != NULL) { if (curr->key == key) { if (prev == NULL) { obj->hashTable[idx] = curr->next; } else { prev->next = curr->next; } free(curr); return; } prev = curr; curr = curr->next; } } void moveToHead(LRUCache* obj, DLinkedNode* node) { // 先断开node node->prev->next = node->next; node->next->prev = node->prev; // 插入到head后面 node->prev = obj->head; node->next = obj->head->next; obj->head->next->prev = node; obj->head->next = node; } void addToHead(LRUCache* obj, DLinkedNode* node) { node->prev = obj->head; node->next = obj->head->next; obj->head->next->prev = node; obj->head->next = node; } DLinkedNode* removeTail(LRUCache* obj) { DLinkedNode* node = obj->tail->prev; node->prev->next = obj->tail; obj->tail->prev = node->prev; return node; } 实现get函数: int lRUCacheGet(LRUCache* obj, int key) { DLinkedNode* node = findInHashTable(obj, key); if (node == NULL) { return -1; } moveToHead(obj, node); return node->value; } 实现put函数: void lRUCachePut(LRUCache* obj, int key, int value) { DLinkedNode* node = findInHashTable(obj, key); if (node != NULL) { node->value = value; moveToHead(obj, node); } else { DLinkedNode* newNode = (DLinkedNode*)malloc(sizeof(DLinkedNode)); newNode->key = key; newNode->value = value; insertIntoHashTable(obj, key, newNode); addToHead(obj, newNode); obj->size++; if (obj->size > obj->capacity) { DLinkedNode* tailNode = removeTail(obj); removeFromHashTable(obj, tailNode->key); free(tailNode); obj->size--; } } } 销毁函数: void lRUCacheFree(LRUCache* obj) { // 释放所有DLinkedNode DLinkedNode* curr = obj->head->next; while (curr != obj->tail) { DLinkedNode* tmp = curr; curr = curr->next; free(tmp); } free(obj->head); free(obj->tail); // 释放哈希表 for (int i = 0; i < obj->hashSize; i++) { HashNode* entry = obj->hashTable[i]; while (entry != NULL) { HashNode* tmp = entry; entry = entry->next; free(tmp); } } free(obj->hashTable); free(obj); } 这样,完整的代码应该能够实现LRU缓存的功能。需要注意的是,哈希表的实现可能会影响性能,但根据平均情况,假设哈希表的冲突足够少,可以满足O(1)的时间复杂度要求。 可能存在的改进点: 1. 哈希表的大小可以选择一个更大的质数,以减少冲突的概率。 2. 在哈希表中,可以使用更高效的冲突处理方法,比如开放寻址法,但实现可能更复杂。 3. 可以优化moveToHead函数,避免在节点已经是头部的情况下进行不必要的操作,但通常这样的优化对整体性能影响不大。 综上,这个实现应该满足题目的要求。</think>以下是使用C语言实现LRU缓存数据结构,结合哈希表和双向链表以达到O(1)时间复杂度: ```c #include <stdio.h> #include <stdlib.h> // 双向链表节点 typedef struct DLinkedNode { int key; int value; struct DLinkedNode* prev; struct DLinkedNode* next; } DLinkedNode; // 哈希表节点(用于处理冲突) typedef struct HashNode { int key; DLinkedNode* cacheNode; struct HashNode* next; } HashNode; // LRU缓存结构体 typedef struct { int capacity; int size; DLinkedNode* head; // 哨兵头节点 DLinkedNode* tail; // 哨兵尾节点 HashNode** hashTable; // 哈希表数组 int hashSize; // 哈希表大小 } LRUCache; // 创建LRU缓存 LRUCache* lRUCacheCreate(int capacity) { LRUCache* cache = (LRUCache*)malloc(sizeof(LRUCache)); cache->capacity = capacity; cache->size = 0; // 初始化双向链表 cache->head = (DLinkedNode*)malloc(sizeof(DLinkedNode)); cache->tail = (DLinkedNode*)malloc(sizeof(DLinkedNode)); cache->head->prev = NULL; cache->head->next = cache->tail; cache->tail->prev = cache->head; cache->tail->next = NULL; // 初始化哈希表 cache->hashSize = capacity * 2; // 简化处理哈希表大小 cache->hashTable = (HashNode**)calloc(cache->hashSize, sizeof(HashNode*)); return cache; } // 哈希表辅助函数 DLinkedNode* findInHashTable(LRUCache* obj, int key) { int idx = key % obj->hashSize; HashNode* entry = obj->hashTable[idx]; while (entry != NULL) { if (entry->key == key) { return entry->cacheNode; } entry = entry->next; } return NULL; } void insertIntoHashTable(LRUCache* obj, int key, DLinkedNode* node) { int idx = key % obj->hashSize; HashNode* newEntry = (HashNode*)malloc(sizeof(HashNode)); newEntry->key = key; newEntry->cacheNode = node; newEntry->next = obj->hashTable[idx]; obj->hashTable[idx] = newEntry; } void removeFromHashTable(LRUCache* obj, int key) { int idx = key % obj->hashSize; HashNode* curr = obj->hashTable[idx]; HashNode* prev = NULL; while (curr != NULL) { if (curr->key == key) { if (prev == NULL) { obj->hashTable[idx] = curr->next; } else { prev->next = curr->next; } free(curr); return; } prev = curr; curr = curr->next; } } // 链表操作辅助函数 void moveToHead(LRUCache* obj, DLinkedNode* node) { // 断开原链接 node->prev->next = node->next; node->next->prev = node->prev; // 插入头部 node->prev = obj->head; node->next = obj->head->next; obj->head->next->prev = node; obj->head->next = node; } void addToHead(LRUCache* obj, DLinkedNode* node) { node->prev = obj->head; node->next = obj->head->next; obj->head->next->prev = node; obj->head->next = node; } DLinkedNode* removeTail(LRUCache* obj) { DLinkedNode* node = obj->tail->prev; node->prev->next = obj->tail; obj->tail->prev = node->prev; return node; } // 获取缓存值 int lRUCacheGet(LRUCache* obj, int key) { DLinkedNode* node = findInHashTable(obj, key); if (node == NULL) return -1; moveToHead(obj, node); // 更新为最近使用 return node->value; } // 插入缓存值 void lRUCachePut(LRUCache* obj, int key, int value) { DLinkedNode* node = findInHashTable(obj, key); if (node != NULL) { // 存在则更新 node->value = value; moveToHead(obj, node); } else { // 不存在则新建 DLinkedNode* newNode = (DLinkedNode*)malloc(sizeof(DLinkedNode)); newNode->key = key; newNode->value = value; insertIntoHashTable(obj, key, newNode); addToHead(obj, newNode); obj->size++; // 超过容量则淘汰尾部 if (obj->size > obj->capacity) { DLinkedNode* tailNode = removeTail(obj); removeFromHashTable(obj, tailNode->key); free(tailNode); obj->size--; } } } // 释放缓存 void lRUCacheFree(LRUCache* obj) { // 释放链表节点 DLinkedNode* curr = obj->head->next; while (curr != obj->tail) { DLinkedNode* tmp = curr; curr = curr->next; free(tmp); } free(obj->head); free(obj->tail); // 释放哈希表 for (int i = 0; i < obj->hashSize; i++) { HashNode* entry = obj->hashTable[i]; while (entry != NULL) { HashNode* tmp = entry; entry = entry->next; free(tmp); } } free(obj->hashTable); free(obj); } ``` ### 实现说明 1. **数据结构设计**: - **双向链表**:维护缓存项的访问顺序,头部为最近使用,尾部为最久未使用。 - **哈希表**:通过链表法处理冲突,实现O(1)时间复杂度的键值查找。 2. **核心操作**: - **`lRUCacheGet`**:通过哈希表查找键,若存在则将节点移至链表头部。 - **`lRUCachePut`**:若键存在则更新值移至头部;若不存在则创建新节点。若容量超限,则删除链表尾部节点。 3. **时间复杂度**: - **哈希表操作**:平均O(1)时间完成查找、插入和删除。 - **链表操作**:移动节点、插入头部、删除尾部均只需O(1)时间。 ### 使用示例 ```c int main() { LRUCache* cache = lRUCacheCreate(2); lRUCachePut(cache, 1, 1); lRUCachePut(cache, 2, 2); printf("%d\n", lRUCacheGet(cache, 1)); // 返回 1 lRUCachePut(cache, 3, 3); // 淘汰键 2 printf("%d\n", lRUCacheGet(cache, 2)); // 返回 -1 lRUCacheFree(cache); return 0; } ``` 该实现通过哈希表与双向链表的结合,高效地满足LRU缓存的O(1)操作要求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值