簡單的LRU Cache設計與實現


要求:
設計並實現一個LRU緩存的數據結構,支持get和set操作

get(key):若緩存中存在key,返回對應的value,否則返回-1

set(key,value):若緩存中存在key,替換其value,否則插入key及其value,如果插入時緩存已經滿了,應該使用LRU算法把最近最久沒有使用的key踢出緩存。

設計1:

cache使用數組,每個key再關聯一個時間戳,時間戳可以直接用個long long類型表示,在cache中維護一個最大的時間戳:

  • get的時候把key的時間戳變為最大時間戳+1
  • set的時候,數據從前往后存儲
    如果key存在,更新key的時間戳為當前cache中最大的時間戳+1,並更新value;
    如果key不存在,
                     若緩存滿,在整個緩存中查找時間戳最小的key,其存儲位置作為新key的存儲位置,設置key的時間戳為最大時間戳+1
                     若緩存未滿,設置key的時間戳為最大時間戳+1,存儲位置為第一個空閑位置

分析下時間空間復雜度,get的時候,需要從前往后找key,時間為O(N),set的時候,也要從前往后找key,當緩存滿的時候,還得找到時間戳最小的key,時間復雜度為O(N)。除了緩存本身,並沒有使用其他空間,空間復雜度為O(1)。 這個速度顯然是比較慢的,隨着數據量的增大,get和set速度越來越慢。可能有人會想到用哈希表作為底層存儲,這樣get的時間復雜度確實可以減低為O(1),set的時候,只要緩存沒有滿,也可以在O(1)的時間完,但在緩存滿的時候,依然需要每次遍歷找時間戳最小的key,時間復雜度還是O(N)。

設計2:

cache底層使用單鏈表,同時用一個哈希表存儲每個key對應的鏈表結點的前驅結點,並記錄鏈表尾結點的key

  • get時,從哈希表中找到key對應的鏈表結點,挪到鏈表頭,更新指向尾結點的key
  • set時,如果key存在,那么找到鏈表結點,並挪到鏈表頭,更新指向尾結點的key
              如果key不存在,
                                  若緩存滿,重用鏈表尾結點,設置新key和value,並挪到鏈表頭,更新指向尾結點的key
                              若緩存未滿,直接插入結點到鏈表頭,若是第一結點,更新指向尾結點的key

get,set時間復雜度O(1),總的空間復雜度O(N)。比前面的設計好一點。下面的再來看下關於設計2的兩個實現

實現1,自定義鏈表

為了方便鏈表的插入與刪除,使用了帶頭結點head的鏈表,所以真正有效的第一個結點是head->next。另外,只是簡單的實現,沒有容錯,不支持並發,簡單的內存管理

ps. 用雙向鏈表來實現會簡單寫,這里用單鏈表和哈希表共同實現了雙向鏈表的功效,也就是哈希除了用來查找,還指示了key對應的結點的前驅結點。

struct Node{
    int _key;
    int _value;
    Node* _next;
    Node(int key,int value,Node* next):_key(key),_value(value),_next(next){}
};

class LRUCache{
public:
    LRUCache(int capacity) {
        _capacity   = capacity;
        _size       = 0;
        _last       = 0;
        _cur_begin  = _begin = (char *) malloc(sizeof(Node)*(capacity+1));
        _head       = new (_cur_begin) Node(0,0,NULL);//在指定內存上構造對象
        _cur_begin += sizeof(Node);
    }

    ~LRUCache(){
        if(_begin!=NULL){
            while(_cur_begin > _begin){
                _cur_begin -= sizeof(Node);
                ((Node*)_cur_begin)->~Node();//先釋放內存上的對象
            }
            free(_begin);//再釋放內存
        }
    }

    int get(int key) {

        int value             = -1;//初始時假設key對應的結點不存在

        Node* pre_node_of_key = umap_prenodes[key];//key對應的結點的前驅結點

        if(pre_node_of_key !=NULL){//key結點存在

            Node* node             = pre_node_of_key->_next;//key對應的結點
            pre_node_of_key->_next = node->_next;
            if(pre_node_of_key->_next!=NULL){
                umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;
            }

            node->_next            = _head->_next;
            if(node->_next!=NULL){//node有后繼,更新后繼的前驅結點
                umap_prenodes[node->_next->_key] = node;
            }

            _head->_next           = node;
            umap_prenodes[key]     = _head;              

            /*更新_last*/
            if(_last == key ){
                _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); 
            }

            value = node->_value;
        }
        return value;
    }

    void set(int key, int value) {
        Node* node            = NULL;
        Node* pre_node_of_key = umap_prenodes[key];//key對應的結點的前驅結點

        if(pre_node_of_key != NULL){//key對應的結點存在,孤立key對應的結點,也就是從鏈表中把結點取出來,重新鏈接鏈表

            node                   = pre_node_of_key->_next;//key對應的結點
            pre_node_of_key->_next = node->_next;

            if(pre_node_of_key->_next!=NULL){
                umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;//更新前驅
            }

            node->_value           = value; //重置結點值

            /*更新_last*/
            if(_last == key ){
                _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); 
            }
        }else{//結點不存在

            if(_capacity == 0){//緩沖區為空
                return ;
            }

            if(_size == _capacity){//緩存滿,重用最后一個結點

                Node* pre_node_of_last    = umap_prenodes[_last];//最后一個結點的前驅結點

                umap_prenodes[pre_node_of_last->_next->_key] = NULL;
                
                node                      = new (pre_node_of_last->_next) Node(key,value,NULL);//重用最后一個結點

                pre_node_of_last->_next   = NULL;//移出最后一個結點

                _last = ( pre_node_of_last == _head ? key : pre_node_of_last->_key ); //更新指向最后一個結點的key

            }else{//緩沖未滿,使用新結點

                node    = new (_cur_begin) Node(key,value,NULL);
                _cur_begin += sizeof(Node);
                _size++;
                if(_size==1){
                    _last = key;
                }
            }
        }

        /*把node插入到第一個結點的位置*/
        node->_next            = _head->_next;
        if(node->_next!=NULL){//node有后繼,更新后繼的前驅結點
            umap_prenodes[node->_next->_key] = node;
        }
        _head->_next           = node;
        umap_prenodes[key]     = _head;  

    }

private:
    int   _size;
    int   _capacity;
    int   _last;//_last是鏈表中最后一個結點的key
    Node* _head;
    unordered_map<int,Node*> umap_prenodes;//存儲key對應的結點的前驅結點,鏈表中第一個結點的前驅結點為_head
    
    char* _begin;//緩存的起始位置 
    char* _cur_begin;//用於分配結點內存的起始位置
};

實現2,使用stl的list

這個版本的實現來自LeetCode discuss

class LRUCache{
    size_t m_capacity;
    unordered_map<int,  list<pair<int, int>>::iterator> m_map; //m_map_iter->first: key, m_map_iter->second: list iterator;
    list<pair<int, int>> m_list;                               //m_list_iter->first: key, m_list_iter->second: value;
public:
    LRUCache(size_t capacity):m_capacity(capacity) {
    }
    int get(int key) {
        auto found_iter = m_map.find(key);
        if (found_iter == m_map.end()) //key doesn't exist
            return -1;
        m_list.splice(m_list.begin(), m_list, found_iter->second); //move the node corresponding to key to front
        return found_iter->second->second;                         //return value of the node
    }
    void set(int key, int value) {
        auto found_iter = m_map.find(key);
        if (found_iter != m_map.end()) //key exists
        {
            m_list.splice(m_list.begin(), m_list, found_iter->second); //move the node corresponding to key to front
            found_iter->second->second = value;                        //update value of the node
            return;
        }
        if (m_map.size() == m_capacity) //reached capacity
        {
           int key_to_del = m_list.back().first; 
           m_list.pop_back();            //remove node in list;
           m_map.erase(key_to_del);      //remove key in map
        }
        m_list.emplace_front(key, value);  //create new node in list
        m_map[key] = m_list.begin();       //create correspondence between key and node
    }
};

通過兩個版本的實現,可以看到,使用stl的容器代碼非常簡潔,但也不是說自定義鏈表版本的實現就不好,如果從並發的角度來說,自定義的結構,在實現並發時,鎖的粒度會小一點,而直接使用stl容器,鎖的粒度為大一點,因為,使用stl,必須鎖定一個函數,而使用自定義結構可以只鎖定某個函數內部的某些操作,而且更方便實現無鎖並發。另外,從leetcode的測試結果來看,這兩個版本的性能差不多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM