LeetCode146——LRU Cache——DS Design

updated on October 14th, 2022

回顾这道题的时候发现原始版本的代码中,每次插入或者删除一个节点时就会立即申请或者删除原有节点,其实这是不必要的,尤其是将节点转移到链表的头部时。这样反复的申请新节点再删除的操作会使得效率降低。

另外,对链表进行的操作应该只涉及指针操作,所以传入的参数应该只有指向链表的指针,不要传入key和value。

修改后的完整代码如下:

// declaration of ListNode
struct Node
{
    int Key, Value;
    Node* Prev;
    Node* Next;

    // ctor defined here
    Node() : Key(0), Value(0), Prev(nullptr), Next(nullptr){}
    Node(int K, int V) : Key(K), Value(V), Prev(nullptr), Next(nullptr){} 
};

class LRUCache {
    //the capacity of cache
    int Capacity;

    // the current length of list, equal to the size of cache
    int CurrentSize;

    // double linked list's head & tail pointer 
    Node* Head;
    Node* Tail;

    // hash map can decrease the get operation finished in O(1) time complexity
    map<int, Node*> HashMap;

public:
    LRUCache(int capacity) : Capacity(capacity), CurrentSize(0)
    {
        // initialize double linked list
        Head = new Node();
        Tail = new Node();

        // modify the head & tail's pointer
        Head->Next = Tail;
        Tail->Prev = Head;
    }

    // insert a new node to head of double linked list and hashmap, return the new inserted node
    void insertNode(Node* NewNode)
    {
        // a set of operations to modify the pointers
        NewNode->Next = Head->Next;
        NewNode->Prev = Head->Next->Prev;
        Head->Next->Prev = NewNode;
        Head->Next = NewNode;
        ++CurrentSize;
    }   

    // remove a node from double linked list, return the deleted key
    void deleteNode(Node* DelNode)
    {
        // modify pointers
        DelNode->Next->Prev = DelNode->Prev;
        DelNode->Prev->Next = DelNode->Next;
        --CurrentSize;
    }

    // move the Present node to the head of list,
    // indicating the element has been used recently
    void moveToHead(Node* Temp)
    {
        // delete the element and insert it to head of list
        deleteNode(Temp);

        // don't forget to write the new pointer to hashmap
        insertNode(Temp);
    }

    Node* removeTail()
    {
        Node* Res = Tail->Prev;
        deleteNode(Tail->Prev);
        return Res;
    }

    int get(int key) 
    {
        // find the corresponding node according to the key
        // if found...
        if(HashMap.find(key) != HashMap.end())
        {
            // get the corresponding value of the key
            int Res = HashMap[key]->Value;

            // update the element to recently used
            moveToHead(HashMap[key]);
            return Res;
        }
        // not found...
        return -1;
    }
    
    void put(int key, int value) 
    {
        // if the element is in the cache, update the value and move it to head
        if(HashMap.find(key) != HashMap.end())
        {
            HashMap[key]->Value = value;
            moveToHead(HashMap[key]);
        }

        // if the element is not in the cache and the cache is not full, insert the element
        else if(HashMap.find(key) == HashMap.end() && CurrentSize < Capacity)
        {
            Node* NewNode = new Node(key, value);
            // insert a new item to hashmap
            HashMap.insert(make_pair(key, NewNode));
            insertNode(NewNode);
        }

        // if the element is not in the cache and the cache is full, LRU works here
        else if(HashMap.find(key) == HashMap.end() && CurrentSize == Capacity)
        {
            // remove the LRU element and insert the new element
            Node* Temp = removeTail();
            HashMap.erase(Temp->Key);
            delete Temp;                            // prevent memory leak
            
            Node* NewNode = new Node(key, value);
            HashMap.insert(make_pair(key, NewNode));
            insertNode(NewNode);
        }
    }
};

以下是原始博客

这是一道别具一格的题目,不同于一般的考察算法的问题,这道题是一道综合性颇强的设计类问题。题目给定要实现的功能简介,并对期望的算法时间复杂度做出了要求,下面是这道题目的具体要求:
在这里插入图片描述
LRU(Least Recently Used,最近最少使用)对于对计算机稍有了解的人来说都不陌生,在cache和页表等等很多系统机制的替换策略中均有它的身影。这道题让我们实现一个模拟的LRU机制,并实现其中的get(获取数据),put(放置数据)两种操作,并且希望我们在O(1)的时间复杂度中完成,是一道综合考察数据结构和设计能力的题目。

其实看到(key, value)这样的键值对形式,很多人会本能的想到用hash实现的map。这也是我一开始的想法,但只使用单一的map没法做到更新操作也在常数时间内完成。比如,我尝试使用map<int, time_t>来存储某一个键和对应插入cache的时间,结果发现这样没法做到在O(1)时间置换元素。反过来使用time_t做为键,因为map自动保持有序,这时候我们是可以立刻找到LRU的元素,但是由于作为时间的关键字没法修改,那么就不能保证某个元素访问后对元素序列的实时更新

看了题解之后发现,这道题采用了两种数据结构相互组合的形式,即双向链表+hashmap。其中双向链表维护了真正的cache存储机制,最靠近头部的数据节点表示刚刚访问过最靠近尾部的数据节点表示最近最久未使用,这样的数据结构保证了我们可以在O(1)时间内快速的找到要置换的元素。这样的链表支持顺序访问,但是不支持随机访问,随机访问是指在常数时间内快速找到要查找的元素而无需遍历,所以我们还是得用上hashmap,让这两个数据结构相互"关照",也就是相互可查询
在这里插入图片描述
实现这一点我们需要做的就是,在hashmap中存储指向某一个结点的指针,而双向链表中存储指向某一个hashmap项目的键key,这样两者都可以在O(1)时间之内完成对对方的查询。

下面开始数据结构的定义,首先是链表结点的数据成员和构造函数:

// declaration of ListNode
struct Node
{
    int Key, Value;
    Node* Prev;
    Node* Next;

    // ctor defined here
    Node() : Key(0), Value(0), Prev(nullptr), Next(nullptr){}
    Node(int K, int V) : Key(K), Value(V), Prev(nullptr), Next(nullptr){} 
};

然后定义的是hashmap,我们可以直接使用STL中的unordered_map,这里不能使用map还是为了要提升效率,因为map中自动要维护容器的有序性,但hash表中项目的有序性对我们来说没什么意义,为了不降低效率,这里使用unordered_map。

 // hash map can decrease the [get] operation finished in O(1) time complexity
 unordered_map<int, Node*> HashMap;

之后是类中一些数据成员的声明,包含我们要维护的双向链表的头指针和尾指针,为了方便操作,我们还引入两个伪结点(dummy node)来保证操作一致性,它们本身不存储值。

//the capacity of cache
int Capacity;

// the current length of list, equal to the size of cache
int CurrentSize;

// double linked list's head & tail pointer 
Node* Head;
Node* Tail;

接下来是构造函数的定义,类的构造函数使用了初始值列表来对Capacity和CurrentSize进行初始化,同时初始化了头尾伪结点:

	LRUCache(int capacity) : Capacity(capacity), CurrentSize(0)
	{
        	// initialize double linked list
        	Head = new Node();
        	Tail = new Node();

        	// modify the head & tail's pointer
        	Head->Next = Tail;
        	Tail->Prev = Head;
	}

然后要自己维护双向链表,就要自定义一些基本操作,下面这个函数就用来向链表头部(插入位置是确定的)插入一个结点,来表示一个键值对刚刚被使用过,返回插入的新结点的指针,便于向hashmap中进一步插入或修改此指针值:

	// insert a new node to head of double linked list and hashmap,
	// return the new inserted node
    Node* insertElement(int Key, int Value)
    {
        // generate a new node
        Node* NewNode = new Node(Key, Value);

        // a set of operations to modify the pointers
        NewNode->Next = Head->Next;
        NewNode->Prev = Head->Next->Prev;
        Head->Next->Prev = NewNode;
        Head->Next = NewNode;

        ++CurrentSize;
        return NewNode;
    }   

然后是依照键值Key删除结点值的操作,返回被删除的键值便于从hashmap中也删除此键值:

	// remove a node from double linked list, return the deleted key
    int deleteElement(int Key)
    {
        // get the node pointer to be deleted
        Node* DelNode = HashMap[Key];

        // modify pointers
        DelNode->Next->Prev = DelNode->Prev;
        DelNode->Prev->Next = DelNode->Next;

        // delete the node from list
        delete DelNode;

        --CurrentSize;
        return Key;
    }

上面两个操作均涉及一系列指针修改和对CurrentSize的修改。
然后定义一个打包操作,也就是将一个结点从它所在的位置移动到头部,表示对某个元素访问之后的更新,千万注意要记住修改hashmap中的指针值,因为我们新分配了一个结点,指针值发生了变化:

	// move the Present node to the head of list,
    // indicating the element has been used recently
    void moveToHead(int Key)
    {
        // get the corresponding value of the key
        int Value = HashMap[Key]->Value;

        // delete the element and insert it to head of list
        deleteElement(Key);

        // don't forget to write the new pointer to hashmap
        HashMap[Key] = insertElement(Key, Value);
    }

定义完上述操作之后,我们就可以来实现cache的get和put操作了,首先是get操作:

	int get(int key) 
    {
        // find the corresponding node according to the key
        // if found...
        if(HashMap.find(key) != HashMap.end())
        {
            // get the corresponding value of the key
            int Res = HashMap[key]->Value;

            // update the element to recently used
            moveToHead(key);
            return Res;
        }
        // not found...
        return -1;
    }

其次是put操作:

	void put(int key, int value) 
    {
        // if the element is in the cache, update the value and move it to head
        if(HashMap.find(key) != HashMap.end())
        {
            HashMap[key]->Value = value;
            moveToHead(key);
        }

        // if the element is not in the cache and the cache is not full, insert the element
        else if(HashMap.find(key) == HashMap.end() && CurrentSize < Capacity)
            // insert a new item to hashmap
            HashMap.insert(make_pair(key, insertElement(key, value)));
            
        
        // if the element is not in the cache and the cache is full, LRU works here
        else if(HashMap.find(key) == HashMap.end() && CurrentSize == Capacity)
        {
            // remove the LRU element and insert the new element
            HashMap.erase(deleteElement(Tail->Prev->Key));
            HashMap.insert(make_pair(key, insertElement(key, value)));
        }
    }

这就是这道题的完整代码,可以看到需要一些特殊的数据结构设计技巧和方法来达到我们的效果,上面这是我看了思路之后自己写的代码,提交之后发现效率表现一般,应该有更多的优化细节可以施展,在此留个坑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值