以下的討論實現都是奔着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()); } } }
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; } } }
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; }
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; } }
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; } } }