页面置换算法--LFU算法实现-O时间复杂度

Posted master-dragon

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了页面置换算法--LFU算法实现-O时间复杂度相关的知识,希望对你有一定的参考价值。

LFU: least frequently used (LFU) page-replacement algorithm

若有读者看到,希望在理解思路后,自己多敲几遍(收获会较大)


leetcode题目地址

https://leetcode.com/problems/lfu-cache/?tab=Description

题目描述

Design and implement a data structure for Least Frequently Used (LFU) cache. It should support the following operations: get and put.

get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value) - Set or insert the value if the key is not already present. When the cache reaches its capacity, it should invalidate the least frequently used item before inserting a new item. For the purpose of this problem, when there is a tie (i.e., two or more keys that have the same frequency), the least recently used key would be evicted.

Follow up:
Could you do both operations in O(1) time complexity?

Example:

LFUCache cache = new LFUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // returns 1
cache.put(3, 3);    // evicts key 2
cache.get(2);       // returns -1 (not found)
cache.get(3);       // returns 3.
cache.put(4, 4);    // evicts key 1.
cache.get(1);       // returns -1 (not found)
cache.get(3);       // returns 3
cache.get(4);       // returns 4

题目讨论,各种解决方案

https://leetcode.com/problems/lfu-cache/?tab=Solutions

ac1

数据结构设计

java版本

// 双向链表的节点
class Node 
    public int key, val;
    public Node next, prev;
    public Node(int k, int v) 
        this.key = k;
        this.val = v;
    


class DoubleList 
    private Node head, tail; // 头尾虚节点
    private int size; // 链表元素数

    public DoubleList() 
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    

    // O(1)
    // 头插
    public void addFirst(Node x) 
        x.next = head.next;
        x.prev = head;
        head.next.prev = x;
        head.next = x;
        size++;
    

    public Node getFirst() 
        if (tail.prev == head) 
            return null;
        
        return head.next;
    

    public Node getLast() 
        if (tail.prev == head) 
            return null;
        
        return tail.prev;
    

    // O(1)
    // 删除链表中的 x 节点(x 一定存在)
    public void remove(Node x) 
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    

    // O(1)
    // 删除链表中最后一个节点,并返回该节点
    public Node removeLast() 
        if (tail.prev == head) 
            return null;
        
        Node last = tail.prev;
        remove(last);
        return last;
    

    // 返回链表长度
    public int size()  return size; 

    public boolean isEmpty()  return size == 0; 


class LFUCache 

    Map<Integer, DoubleList> freMap;
    Map<Integer, Integer> keyFreMap;
    Map<Integer, Node> keyNodeMap;
    int cap;
    int totalSize;
    int minFre;

    public LFUCache(int capacity) 
        cap = capacity;
        freMap = new HashMap<>();
        keyFreMap = new HashMap<>();
        keyNodeMap = new HashMap<>();
        minFre = 0;
        totalSize = 0;
    

    // 删除原来就存在的一个key
    public void removeExistKey(int key)
        int fre = keyFreMap.get(key);
        DoubleList doubleList = freMap.get(fre);
        // 删除老的
        Node oldNode = keyNodeMap.get(key);
        doubleList.remove(oldNode);
        keyNodeMap.remove(key);
        keyFreMap.remove(key);
        if(doubleList.isEmpty())
            freMap.remove(fre);
            if(minFre == fre)
                minFre ++;
            
        

        totalSize --;
    

    // 添加新节点,fre为新节点的fre, node为新Node
    public void addNewNode(int fre, Node node)
        // 设置新的
        DoubleList doubleList = freMap.getOrDefault(fre, new DoubleList());
        doubleList.addFirst(node);
        keyNodeMap.put(node.key, doubleList.getFirst());
        freMap.put(fre, doubleList);
        keyFreMap.put(node.key, fre);

        totalSize ++;
    

    public int get(int key) 
        if(!keyFreMap.containsKey(key))
            return -1;
        
        int fre = keyFreMap.get(key);
        Node oldNode = keyNodeMap.get(key);
        int retVal = oldNode.val;
        // 删除旧的
        removeExistKey(key);
        // 设置新的
        addNewNode(fre + 1, oldNode);
        return retVal;
    

    public void put(int key, int value) 
        if(cap == 0)
            return;
        
        if(!keyFreMap.containsKey(key)) // put新元素
            if(totalSize == cap)
                // 需要删除
                DoubleList doubleList = freMap.get(minFre);
                Node removeNode = doubleList.getLast();
                removeExistKey(removeNode.key);
            
            Node newNode = new Node(key, value);
            // 添加新节点,其频率是1
            addNewNode(1, newNode);
            minFre = 1;
        else 
            int fre = keyFreMap.get(key);
            // 删除旧的
            removeExistKey(key);
            // 设置新的
            Node newNode = new Node(key, value);
            addNewNode(fre + 1, newNode);
        
    


ac代码

class LFUCache 
public:
	int size;
	int cap;
	int minfreq;
	map<int,pair<int,int>> m;//key to pair<value,freq>  
    map<int,list<int>::iterator> mIter;//key to list location , key在 list中的位置一个iterator 
    map<int,list<int>> fm;//freq to list , list存放的是所有的key, 最后的key是最近访问过的,头部的是最近没有访问的(淘汰)

public:

    LFUCache(int capacity) 
       cap = capacity;  
       size = 0;
    
    
    int get(int key) 
		if(m.count(key) == 0)
			return -1;
		
		//key 频率加1,删除原来其在fm中的位置,插入到新的位置
		fm[m[key].second].erase(mIter[key]); 
		m[key].second ++;
		fm[m[key].second].push_back(key);

		mIter[key] = --fm[m[key].second].end(); // 当前key所在的位置

		if(fm[minfreq].size() == 0) //上面的步骤处理后,可能最小频率已经删除了数据,所以需要判断
			minfreq ++;
		
		return m[key].first;
	
    
    void put(int key, int value) 
		if(cap <= 0)
			return ;

		/*
			调用成员方法get
			如果不存在,返回-1;
			如果已经存在,那么就会修改频数,删除旧的位置,添加到新的位置,但是值仍然是原来的,需要修改
		*/
		int storeValue = get(key); 
		if(storeValue != -1)
		
			m[key].first = value;
			return; // 直接返回
		

		// 不存在的情况, 已经满了,需要删除频率最小,最近都没有访问过的那个key
		if(size >= cap)
			m.erase(fm[minfreq].front());
			mIter.erase(fm[minfreq].front());
			fm[minfreq].pop_front();
			size --;
		
        
        pair<int, int> pr(value, 1);
		m[key] = pr;
		fm[1].push_back(key);
		mIter[key] = --fm[1].end();
		minfreq = 1;
		size ++;
    
;

ac2

参考
https://discuss.leetcode.com/topic/78833/c-89ms-beats-99-8-using-unordered_map-list-of-list

数据结构设计

ac代码

class LFUCache 
public:
    struct info
    
        int val;
        list<pair<int, list<int>>>::iterator it_pair;
        list<int>::iterator it_key;
    ;

    LFUCache(int capacity) 
        cap_ = capacity;
    

    int get(int key) 
        auto it = map_.find(key);
        
        if (it == map_.end())
        
            return -1;
        
        else
        
            visit(it, key);
            return it->second.val;
        
    

    void put(int key, int value) 
        auto it = map_.find(key);
        
        // 已经存在,需要更新value,改变其频数,在map等中的信息都要更改
        if (it != map_.end())
        
            visit(it, key);
            it->second.val = value;
        
        else
        
            if (cap_ == 0) return;

            // del
            if (map_.size() == cap_)
            
                auto it = list_.front().second.begin();
                map_.erase(*it);
                list_.front().second.erase(it);

                if (list_.front().second.size() == 0)
                    list_.erase(list_.begin());
            
            // insert
            if (list_.empty() || list_.front().first != 1)
            
				list<int> li;
				li.push_back(key);
				pair<int,list<int>> pr(1, li);
                
				list_.push_front(pr);

				//list_.push_front(1, key);
            
            else
            
				// 最小的频数list中插入一个key, 插入到最后面表示是最近访问到的
                list_.front().second.push_back(key);
            

			info in;
			in.val = value;
			in.it_pair = list_.begin();
			in.it_key = std::prev(list_.front().second.end());

			map_[key] = in;

            //map_[key] = value, list_.begin(), std::prev(list_.front().second.end());
        
    

protected:
    void visit(unordered_map<int, info>::iterator it, int key)
    
        auto it_pair = it->second.it_pair; // 在list中的哪个频数上面
        auto it_key = it->second.it_key; // 在map中的位置
        int count = it_pair->first + 1; // 频数加1

        it_pair->second.erase(it_key); // 删除该key,如果对应频数没有key了, list_要将这个记录直接删除,并把it指向下一个记录
        if (it_pair->second.size() == 0)
            it_pair = list_.erase(it_pair);
        else
        
			std::advance(it_pair, 1); // it_pair往前移动
		

        if (it_pair == list_.end() || it_pair->first != count)
        
            // list_没有该新的频数,就构造出来,插入到对应的位置
			list<int> li;
			li.push_back(key);
			pair<int,list<int>> pr(count, li);
               
			it_pair = list_.insert(it_pair, pr);
			//it_pair = list_.insert(it_pair, count, key);
        
        else
        
            it_pair->second.push_back(key);
        
		// 更新info信息
        it->second.it_pair = it_pair;
        it->second.it_key = std::prev(it_pair->second.end());
    

    int cap_;
    list<pair<int, list<int>>> list_;
    unordered_map<int, info> map_;
;

同样的ac2思想,仿照ac1代码形式改写,还是可以ac,不过ac时间更长(可能是C++ stl相关操作的影响)

class LFUCache 
public:
	
	struct Info
    
        int val;
        list<pair<int, list<int>>>::iterator it_pair;
        list<int>::iterator it_key;
    ;

	int size;
	int cap;
	list<pair<int, list<int>>> liFreq; // freq keys
    unordered_map<int, Info> mp; //key->Info

public:
    LFUCache(int capacity) 
		size = 0;
		cap = capacity;
    
    
	int get(int key) 
		if(mp.count(key) == 0)
			return -1;

		Info oldInfo = mp[key];
		
		list<pair<int, list<int>>>::iterator old_it_pair = oldInfo.it_pair; 
		list<int>::iterator old_it_key = oldInfo.it_key;;

		old_it_pair->second.erase(old_it_key); // 删除旧的
		int newFreqNum = old_it_pair->first + 1; // 新的的频率

		list<pair<int, list<int>>>::iterator new_it_pair; // key需要到的新的liFreq位置
		if((int)old_it_pair->second.size() == 0)
		
			new_it_pair = liFreq.erase(old_it_pair); // 删除该频数
		else
			new_it_pair = std::next(old_it_pair);
		

		// 是否需要构建新的频数,然后key总是插入到最后面
		if(new_it_pair == liFreq.end() || new_it_pair->first != newFreqNum)
		
			// liFreq 没有该新的频数,就构造出来,插入到对应的位置
            list<int> li;
            li.push_back(key);
            pair<int,list<int>> pr(newFreqNum, li);

            new_it_pair = liFreq.insert(new_it_pair, pr); // 位置之前插入,并返回插入后,元素所处的位置
		else
			new_it_pair->second.push_back(key);
		

		mp[key].it_pair = new_it_pair;
		mp[key].it_key = std::prev(new_it_pair->second.end());

		return mp[key].val;
	

    void put(int key, int value) 
		if(cap <= 0)
			return;

		int storeVal = get(key);

		if(storeVal !=-1)
		
			mp[key].val = value;
			return;
		

		if(size == cap)
		
			auto it = liFreq.front().second.begin();
			int outKey = *it;
			mp.erase(outKey);

			liFreq.front().second.erase(it); // 删除key

			if((int)liFreq.front().second.size() == 0) // 若删除key后 该频数为空了,那么该频数也可以删除掉了
				liFreq.erase(liFreq.begin());

			size --;
		

		size ++;

		// insert
        if (liFreq.empty() || liFreq.front().first != 1)
        
            list<int> li;
            li.push_back(key);
            pair<int,list<int>> pr(1, li);

            liFreq.push_front(pr);
        
        else
        
            // 最小的频数list中插入一个key, 插入到最后面表示是最近访问到的
            liFreq.front().second.push_back(key);
        

        Info in;
        in.val = value;
        in.it_pair = liFreq.begin();
        in.it_key = std::prev(liFreq.front().second.end());

        mp[key] = in;
	
;

以上是关于页面置换算法--LFU算法实现-O时间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

页面置换算法之LRU算法

(计算机组成原理)第三章存储系统-第六节3:页面置换算法(FIFO,近期最少使用算法-LRU,LFU)

缓存算法(页面置换算法)-FIFOLFULRU

146 LRU Cache 最近最少使用页面置换算法

Redis5.0之后的内存策略--最新八种算法

左神算法进阶班6_1LFU缓存实现