菜单

LRU Cache的简单实现

2015年7月23日 - 算法

Cache这个东西可以说无处不在,处理器中的TLB,Linux系统中的高速页缓存,还有很多人熟知的开源软件memcached,都是cache的一种实现。LRU是Least Recently Used的缩写,即最近最少使用,是常用cache算法中的一种。因为cache的存储空间相对于后端存储来说更有限,将cache空间和后端存储空间映射后,还需要一些算法来解决cache满的问题并保证效率,LRU就是在cache满了以后,将最近最少访问到的内容移除,然后将新的内容放入cache,新的内容也成为了最近最常访问的内容。

将最常访问的内容放在最前面,最少访问的放在最后面,而且内容在一定范围内可以不断增长,这种东西用链表作为存储结构比较合适。如果是单链表,即使我们知道了要访问节点指针的值,还是需要遍历到该节点的前驱,然后将该节点移到链表头,成为最常访问的节点,而如果是双向链表,只要知道了要访问节点的指针,不需要遍历找到其前驱就可以将其从链表中的某个位置移动到链表头,时间复杂度为O(1)。因此为了效率,我们应该使用双向链表,虽然双向链表每个节点多了一个指针大小的开销,但是我觉得既然用cache某种程度上默认了应以一定空间代价去换时间。在访问一个节点时,是通过键值进行访问的,那又如何快速获取到某个键值所对应节点的指针呢?用哈希吧。通过哈希表,获取一个键值所对应节点的地址的时间复杂度为O(1)。通过使用哈希和双向链表,访问cache的整体效率也是O(1)。

使用哈希表会有冲突的问题,就是不同的键值映射到了哈希表中相同的位置,也就是映射到哈希数组中同一个下标。解决这个冲突问题可以使用开放地址法,即我们将不同键值对应到同一下标的节点内容以单链表的形式连接起来。这样倒是解决了问题,但是如果很多键值都对应到同一个下标,链表变得很长时,访问哈希表获取节点指针的时间复杂度又从O(1)几乎退化成了O(n),在某些情况下会有很大的性能下降。如果这样,可以试着使用二叉树存放键值及其对应的节点指针,这样就不会因为数据的增多而使获取键值对应节点指针的时间复杂度恶化。下面是我用第一种方法实现的一个简单LRU
Cache代码:

struct kvNode
{
int key;
int value;
struct kvNode* prev;
struct kvNode* next;
};

struct ListTable
{
int capacity;
int count;
struct kvNode* head;
struct kvNode* tail;
};

struct hashNode
{
struct kvNode* linkNode;
struct hashNode* prev;
struct hashNode* next;
};

struct hashTable
{
int capacity;
struct hashNode* *pNode;
};

struct ListTable* pTable;
struct hashTable* pHash;

void lruCacheInit(int capacity) {
if(capacity <= 0) return;

    pTable = malloc(sizeof(*pTable));

pTable->capacity = capacity;

pTable->count = 0;

pTable->head = NULL;

pTable->tail = NULL;

pHash = malloc(sizeof(*pHash));

pHash->capacity = capacity;

pHash->pNode = malloc(sizeof(*pHash->pNode)*capacity);

for(int i = 0; i < capacity; ++i)
{
pHash->pNode[i] = NULL;
}
}

void lruCacheFree() {

if(pTable)
{
struct kvNode* p = pTable->head;
while(p)
{
struct kvNode* next = p->next;
free(p);
p = next;
}

free(pTable);
}

if(pHash) free(pHash);
}

int lruCacheGet(int key) {
if(pHash)
{
struct hashNode* p = pHash->pNode[key%pHash->capacity];
if(!p) return -1;
while(p && p->linkNode->key != key) p = p->next;
if(!p)
{
return -1;
}
if(p->linkNode == pTable->head)
{
return p->linkNode->value;
}

if(p->linkNode->prev)
{
p->linkNode->prev->next = p->linkNode->next;
}

if(p->linkNode->next)
{
p->linkNode->next->prev = p->linkNode->prev;
}

pTable->head->prev = p->linkNode;
p->linkNode->next = pTable->head;
p->linkNode->prev = NULL;

return p->linkNode->value;
}
return -1;
}

void lruCacheSet(int key, int value) {
if(value <= 0) return;

if(pHash)
{
int index = key%pHash->capacity;
struct hashNode* p = pHash->pNode[index];
while(p && p->linkNode->key != key)
{
p = p->next;
}

if(p)
{
p->linkNode->value = value;
if(pTable->head == p->linkNode) return;

if(p->linkNode->prev)
{
p->linkNode->prev->next = p->linkNode->next;
}

if(p->linkNode->next)
{
p->linkNode->next->prev = p->linkNode->prev;
}

pTable->head->prev = p->linkNode;
p->linkNode->next = pTable->head;
p->linkNode->prev = NULL;
}
else
{
struct hashNode* phashNode = malloc(sizeof(*phashNode));
phashNode->linkNode = NULL;
phashNode->prev = NULL;
phashNode->next = NULL;

struct kvNode* pkvInsert = malloc(sizeof(*pkvInsert));

phashNode->linkNode = pkvInsert;

pkvInsert->key = key;
pkvInsert->value = value;
pkvInsert->prev = NULL;
pkvInsert->next = pTable->head;

if(pTable->head)
{
pTable->head->prev = pkvInsert;
}

else
{
pTable->tail = pkvInsert;
}

pTable->head = pkvInsert;


if(pHash->pNode[index])
{
pHash->pNode[index]->prev = phashNode;
}

phashNode->next = pHash->pNode[index];
pHash->pNode[index] = phashNode;

if(pTable->count == pTable->capacity)
{
struct kvNode* pkvDel = pTable->tail;
pTable->tail = pTable->tail->prev;

if(pTable->tail)pTable->tail->next = NULL;
struct hashNode* p = pHash->pNode[pkvDel->key%pHash->capacity];

while(p && p->linkNode->key != pkvDel->key) p = p->next;
if(p)
{
if(p->prev) p->prev->next = p->next;
if(p->next) p->next->prev = p->prev;
free(p);
}

free(pkvDel);
}
else pTable->count++;
}
}
}

发表评论