LeetCode146——LRU Cache——DS Design

这是一道别具一格的题目,不同于一般的考察算法的问题,这道题是一道综合性颇强的设计类问题。题目给定要实现的功能简介,并对期望的算法时间复杂度做出了要求,下面是这道题目的具体要求:
在这里插入图片描述
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)));
        }
    }

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

Guess you like

Origin blog.csdn.net/zzy980511/article/details/115956971