LRU和LFU的區別和使用場景

以下的討論實現都是奔着O(1)時間複雜度

LRU

LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那麼將來被訪問的機率也更高”。

LRU 總體上是這樣的,最近使用的放在前邊(最左邊),最近沒用的放到後邊(最右邊),

來了一個新的數,如果內存滿了,把舊的數淘汰掉(最右邊),

那位了方便移動數據,我們肯定不能考慮用數組,

呼之欲出,就是使用鏈表了,

解決方案:鏈表(處理新老關係)+ 哈希(查詢在不在),

LRU 緩存算法的核心數據結構就是哈希鏈表,雙向鏈表和哈希表的結合體。這個數據結構長這樣:

1、通常會用來做緩存的算法 當緩存被填滿時,它應該刪除最近最少使用的項目。

1.JDK自帶的LinkHashMap實現

public class LRUCache{
    int capacity;
    Map<Integer, Integer> map;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new LinkedHashMap<>();
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        // 先刪除舊的位置,再放入新位置
        Integer value = map.remove(key);
        map.put(key, value);
        return value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            map.remove(key);
            map.put(key, value);
            return;
        }
        map.put(key, value);
        // 超出capacity,刪除最久沒用的,利用迭代器刪除第一個
        if (map.size() > capacity) {
            map.remove(map.entrySet().iterator().next().getKey());
        }
    }
}
View Code

2.Map+雙向聯表實現

package com.mashibing.leetcode.link;

import java.util.HashMap;
import java.util.Map;

public class LRUCache3HeadTail {

    private int capacity;
    private Map<Integer, ListNode> map; //key->node
    private ListNode head;  // dummy head
    private ListNode tail;  // dummy tail

    public LRUCache3HeadTail(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head = new ListNode(-1, -1);
        tail = new ListNode(-1, -1);
        head.next = tail;
        tail.pre = head;
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        ListNode node = map.get(key);
        // 先刪除該節點,再接到 頭部
        node.pre.next = node.next;
        node.next.pre = node.pre;
        moveToHead(node);
        return node.val;
    }

    public void put(int key, int value) {
        // 直接調用這邊的get方法,如果存在,它會在get內部被移動到尾巴,不用再移動一遍,直接修改值即可
        if (get(key) != -1) {
            map.get(key).val = value;
            return;
        }
        // 若不存在,new一個出來,如果超出容量,把尾去掉
        ListNode node = new ListNode(key, value);
        map.put(key, node);
        moveToHead(node);

        if (map.size() > capacity) {
            map.remove(tail.pre.key);
            tail.pre = tail.pre.pre;
            tail.pre.next = tail;
        }
    }

    // 把節點移動到頭部
    private void moveToHead(ListNode node) {
        node.next = head.next;
        head.next = node;
        node.next.pre = node;
        node.pre = head;
    }

    // 定義雙向鏈表節點
    private class ListNode {
        int key;
        int val;
        ListNode pre;
        ListNode next;

        public ListNode(int key, int val) {
            this.key = key;
            this.val = val;
            pre = null;
            next = null;
        }
    }

}
View Code

2、也可以作爲負載均衡的算法 

每次使用了每個節點的時候,就將該節點放置在最後面(做緩存時 放在前面),這樣就保證每次使用的節點都是最近最久沒有使用過的節點。

JDK自帶的LinkHashMap實現

public String doRoute(String serviceKey, TreeSet<String> addressSet) {

        // cache clear
        if (System.currentTimeMillis() > CACHE_VALID_TIME) {
            jobLRUMap.clear();
            CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;//一天
        }

        // init lru
        LinkedHashMap<String, String> lruItem = jobLRUMap.get(serviceKey);
        if (lruItem == null) {
            /**
             * LinkedHashMap
             *      a、accessOrder:ture=訪問順序排序(get/put時排序)/ACCESS-LAST;false=插入順序排期/FIFO;
             *      b、removeEldestEntry:新增元素時將會調用,返回true時會刪除最老元素;可封裝LinkedHashMap並重寫該方法,比如定義最大容量,超出是返回true即可實現固定長度的LRU算法;
             */
            lruItem = new LinkedHashMap<String, String>(16, 0.75f, true){
                @Override
                protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                    if(super.size() > 3){
                        return true;
                    }else{
                        return false;
                    }
                }
            };
            jobLRUMap.putIfAbsent(serviceKey, lruItem);
        }

        // put
        for (String address: addressSet) {
            if (!lruItem.containsKey(address)) {
                lruItem.put(address, address);
            }
        }

        // load
        String eldestKey = lruItem.entrySet().iterator().next().getKey();
        String eldestValue = lruItem.get(eldestKey);//LRU算法關鍵體現在這裏,實現了固定長度的LRU算法
        return eldestValue;
    }
View Code

LFU

LRU算法是預測最近被訪問的數據將來最有可能被訪問到。

LFU(Least Frequently Used)最不經常使用。算法根據數據的歷史訪問頻率來淘汰數據,其核心思想是“如果數據過去被訪問多次,那麼將來被訪問的頻率也更高”。

我們需要定義兩個哈希表,第一個 freq_table 以頻率 freq 爲索引,每個索引存放一個雙向鏈表,這個鏈表裏存放所有使用頻率爲 freq 的緩存,

緩存裏存放三個信息,分別爲鍵 key,值 value,以及使用頻率 freq。

第二個 key_table 以鍵值 key 爲索引,每個索引存放對應緩存在 freq_table 中鏈表裏的內存地址,這樣我們就能利用兩個哈希表來使得兩個操作的時間複雜度均爲 O(1)O(1)。

同時需要記錄一個當前緩存最少使用的頻率 minFreq,這是爲了刪除操作服務的。

這個數據結構長這樣:

 

 

參考leetCode:https://leetcode-cn.com/problems/lfu-cache/solution/lfuhuan-cun-by-leetcode-solution/

1、LFU作爲緩存算法

當緩存達到容量時,則應該在插入新的鍵值對之前,刪除使用頻次(後文用freq表示)最低的鍵值對。

如果freq最低的鍵值對有多個,則刪除其中最舊的那個。

代碼實現:

class LFUCache {
    int minfreq, capacity;
    Map<Integer, Node> key_table;
    Map<Integer, LinkedList<Node>> freq_table;

    public LFUCache(int capacity) {
        this.minfreq = 0;
        this.capacity = capacity;
        key_table = new HashMap<Integer, Node>();;
        freq_table = new HashMap<Integer, LinkedList<Node>>();
    }
    
    public int get(int key) {
        if (capacity == 0) {
            return -1;
        }
        if (!key_table.containsKey(key)) {
            return -1;
        }
        Node node = key_table.get(key);
        int val = node.val, freq = node.freq;
        freq_table.get(freq).remove(node);
        // 如果當前鏈表爲空,我們需要在哈希表中刪除,且更新minFreq
        if (freq_table.get(freq).size() == 0) {
            freq_table.remove(freq);
            if (minfreq == freq) {
                minfreq += 1;
            }
        }
        // 插入到 freq + 1 中
        LinkedList<Node> list = freq_table.getOrDefault(freq + 1, new LinkedList<Node>());
        list.offerFirst(new Node(key, val, freq + 1));
        freq_table.put(freq + 1, list);
        key_table.put(key, freq_table.get(freq + 1).peekFirst());
        return val;
    }
    
    public void put(int key, int value) {
        if (capacity == 0) {
            return;
        }
        if (!key_table.containsKey(key)) {
            // 緩存已滿,需要進行刪除操作
            if (key_table.size() == capacity) {
                // 通過 minFreq 拿到 freq_table[minFreq] 鏈表的末尾節點
                Node node = freq_table.get(minfreq).peekLast();
                key_table.remove(node.key);
                freq_table.get(minfreq).pollLast();
                if (freq_table.get(minfreq).size() == 0) {
                    freq_table.remove(minfreq);
                }
            }
            LinkedList<Node> list = freq_table.getOrDefault(1, new LinkedList<Node>());
            list.offerFirst(new Node(key, value, 1));
            freq_table.put(1, list);
            key_table.put(key, freq_table.get(1).peekFirst());
            minfreq = 1;
        } else {
            // 與 get 操作基本一致,除了需要更新緩存的值
            Node node = key_table.get(key);
            int freq = node.freq;
            freq_table.get(freq).remove(node);
            if (freq_table.get(freq).size() == 0) {
                freq_table.remove(freq);
                if (minfreq == freq) {
                    minfreq += 1;
                }
            }
            LinkedList<Node> list = freq_table.getOrDefault(freq + 1, new LinkedList<Node>());
            list.offerFirst(new Node(key, value, freq + 1));
            freq_table.put(freq + 1, list);
            key_table.put(key, freq_table.get(freq + 1).peekFirst());
        }
    }
}

class Node {
    int key, val, freq;

    Node(int key, int val, int freq) {
        this.key = key;
        this.val = val;
        this.freq = freq;
    }
}
View Code

2、LFU作爲負載均衡算法:保證每次使用都是最不經常使用的節點

代碼實現(此代碼時間複雜度不是O1)

package com.mashibing.leetcode.link;

import java.util.HashMap;
import java.util.Map;

public class LRUCache3HeadTail {

    private int capacity;
    private Map<Integer, ListNode> map; //key->node
    private ListNode head;  // dummy head
    private ListNode tail;  // dummy tail

    public LRUCache3HeadTail(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head = new ListNode(-1, -1);
        tail = new ListNode(-1, -1);
        head.next = tail;
        tail.pre = head;
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        ListNode node = map.get(key);
        // 先刪除該節點,再接到 頭部
        node.pre.next = node.next;
        node.next.pre = node.pre;
        moveToHead(node);
        return node.val;
    }

    public void put(int key, int value) {
        // 直接調用這邊的get方法,如果存在,它會在get內部被移動到尾巴,不用再移動一遍,直接修改值即可
        if (get(key) != -1) {
            map.get(key).val = value;
            return;
        }
        // 若不存在,new一個出來,如果超出容量,把尾去掉
        ListNode node = new ListNode(key, value);
        map.put(key, node);
        moveToHead(node);

        if (map.size() > capacity) {
            map.remove(tail.pre.key);
            tail.pre = tail.pre.pre;
            tail.pre.next = tail;
        }
    }

    // 把節點移動到頭部
    private void moveToHead(ListNode node) {
        node.next = head.next;
        head.next = node;
        node.next.pre = node;
        node.pre = head;
    }

    // 定義雙向鏈表節點
    private class ListNode {
        int key;
        int val;
        ListNode pre;
        ListNode next;

        public ListNode(int key, int val) {
            this.key = key;
            this.val = val;
            pre = null;
            next = null;
        }
    }

}
View Code

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章