Java 徹底手撕LRU設計(使用JDK容器與不使用JDK容器)

Java 徹底手撕LRU設計(使用JDK容器與不使用JDK容器)

原博鏈接

JDK 1.8

本文講述了兩種利用JDK容器實現LRU的方法,以及最後不使用JDK容器,自己定義HashMap和鏈表來純手工打造時間複雜度爲O(1)的LRU算法。


§ 使用JDK容器

1、使用LinkedHashMap

這種方式比較簡單,註釋都在代碼裏

public class LRUCache<K,V>{ // 使用泛型
    private LinkedHashMap<K, V> cache;
    private int capacity; // 容量

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

    public V get(K key) {
        if (cache.containsKey(key)) {
            V val = cache.get(key);
            cache.remove(key); // 刪除key
            cache.put(key, val); // 重新put,實現了生命週期的更新
            return val;
        } else {
            return null;
        }
    }

    public void put(K key, V val) {
        if (cache.containsKey(key)) {
            cache.remove(key);
        } else if (cache.size() >= capacity) {
            Iterator itr = cache.keySet().iterator();
            cache.remove(itr.next()); // 刪除頭
        }
        cache.put(key, val);
    }

}

2、使用HashMap + LinkedList

  • HashMap存儲K-V元素
  • LinkedList按序存儲key
import java.util.HashMap;
import java.util.LinkedList;

/**
 * @description:
 * @Author: JachinDo
 * @Date: 2020/04/24 22:04
 */

public class LRUCache2<K, V> {

    private HashMap<K, V> map;
    private LinkedList<K> list;
    private int capacity;

    public LRUCache2(int capacity) {
        map = new HashMap<>();
        list = new LinkedList<K>();
        this.capacity = capacity;
    }

    public V get(K key) {
        if (map.containsKey(key)) {
            V val = map.get(key);
            list.remove(key); // 更新
            list.addLast(key);
            return val;
        } else {
            return null;
        }
    }

    public void put(K key, V value) {
        if (map.containsKey(key)) {
            map.remove(key);
            list.remove(key);
        } else if (map.size() >= capacity) {
            K oldKey = list.removeFirst(); // 刪除頭
            map.remove(oldKey);
        }
        list.addLast(key);
        map.put(key, value);
    }
}

§ 不使用JDK容器實現O(1)的LRU – 純手工打造LRU

  • 自定義HashMapJachinHashMap<K, Node>
  • 核心:HashMap的value值封裝爲Node節點(包含k,v,以及前後指針,形成鏈表),這樣即以O(1)的時間複雜度維持順序,避免了LinkedList的O(n)。

1、自定義HashMap

代碼結構圖:


註釋很詳細,結合代碼結構圖更清晰

/**
 * @description: 實現簡易HashMap
 * @Author: JachinDo
 * @Date: 2020/04/25 00:17
 */

public class JachinHashMap<K, V>{

    private Node<K, V>[] table; // 內部元素封裝爲Node類,存儲在一個數組中
    private int length; // 初始化時指定數組長度
    private int size; // HashMap實際元素個數

    public JachinHashMap(int length) {
        this.length = length;
        table = new Node[length];
    }

    /**
     * 返回當前map中元素個數
     */
    public int size() {
        return size;
    }

    /**
     * 添加元素
     */
    public V put(K key, V value) {

        // 根據key的hash值獲取索引下標
        int index = key.hashCode() % (length - 1);
        // 拿到槽點元素
        Node<K, V> node = table[index];

        if (node == null) {
            // 槽點爲空,則直接設置元素
            table[index] = new Node(key, value);
        } else {
            // 如已有元素,則插入鏈表(包含覆蓋舊值或新增節點)
            while (node != null) {
                if (node.getKey().equals(key)) {
                    node.setVallue(value); // 覆蓋舊值
                    break;
                } else {
                    node = node.next;
                }
            }

            if (node == null) {
                // 需要新增節點
                Node newNode = new Node(key, value);
                newNode.next = table[index];
                table[index].pre = newNode;
                table[index] = newNode;
            }
        }
        size++;
        return table[index].getValue();
    }

    /**
     * 獲取元素
     */
    public V get(K key) {

        int index = key.hashCode() % (length - 1);
        // 拿到槽點元素
        Node<K,V> node = table[index];

        if (node == null) {
            // 若爲空,說明不存在,返回null
            return null;
        } else if (node.getKey().equals(key)) {
            return node.getValue(); // 槽點元素即滿足則直接返回
        } else {
            while (node != null) { // 遍歷鏈表
                if (node.getKey().equals(key)) {
                    return node.getValue(); // 找到滿足key的元素,返回。
                } else {
                    node = node.next;
                }
            }
            return null; // 遍歷完還是沒找到,返回null
        }
    }

    /**
     * 刪除指定key對應元素
     */
    public V remove(K key) {
        int index = key.hashCode() % (length - 1);
        // 拿到槽點元素
        Node<K,V> node = table[index];

        if (node == null) {
            // 所刪元素不存在。
            return null;
        } else if (node.getKey().equals(key)) {
            V value = node.getValue();
            // 刪除槽點元素(頭節點),設置新頭
            Node next = node.next;
            node.next = null;
            if (next != null) {
                next.pre = null;
            }
            table[index] = next;
            size--;
            return value;
        } else {
            while (node != null) {
                // 遍歷鏈表,找到滿足條件key,刪除。
                if (node.getKey().equals(key)) {
                    V value = node.getValue();
                    // 刪除節點,並重新組建鏈表連接
                    Node pre = node.pre;
                    Node next = node.next;
                    node.pre = null;
                    node.next = null;
                    if (pre != null) {
                        pre.next = next;
                    }
                    if (next != null) {
                        next.pre = pre;
                    }
                    size--;
                    return value;
                } else {
                    node = node.next;
                }
            }
            return null; // 沒有滿足條件的元素
        }
    }

    /**
     * JachinHashMap中的元素封裝類型
     */
    class Node<K, V> {
        K key;
        V value;
        Node<K, V> next;
        Node<K, V> pre;
        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
        public K getKey() {
            return key;
        }
        public V getValue() {
            return value;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public void setVallue(V value) {
            this.value = value;
        }
    }
}

2、LRU的實現

代碼結構圖:


註釋很詳細,結合代碼結構圖更清晰

/**
 * @description:
 * @Author: JachinDo
 * @Date: 2020/04/24 22:57
 */

public class JachinLRUCache<K, V> {

    private JachinHashMap<K, JachinLRUNode> map; // 將value封裝爲Node類,便於在鏈表中進行索引
    private int capacity; // 緩存大小

    // 維護鏈表頭尾,便於刪頭,插尾
    private JachinLRUNode head;
    private JachinLRUNode tail;

    public JachinLRUCache(int capacity) {
        this.capacity = capacity;
        map = new JachinHashMap<>(capacity);
    }

    /**
     * 添加元素
     */
    public void put(K key, V value) {
        JachinLRUNode node = map.get(key);
        if (node != null) {
            // 若存在,更新值,並將節點移至末尾
            node.value = value;
            updateNode(node);

        } else { // 若不存在,則需判斷是否需要執行lru淘汰,並插入新節點

            JachinLRUNode newNode = new JachinLRUNode(key, value); // 創建節點

            if (map.size() == capacity) { // 若容量滿,需要刪除頭節點(最近最少訪問節點)

                JachinLRUNode<K, V> oldHead = removeHead(); // 鏈表中刪除
                map.remove(oldHead.key); // map中刪除
            }
            addTail(newNode); // 將節點插入尾部
            map.put(key, newNode); // 將節點加入map
        }
    }

    /**
     * 獲取元素
     */
    public V get(K key) {
        JachinLRUNode<K, V> node = map.get(key);
        if (node != null) {
            // 若存在,則更新節點位置到尾部
            updateNode(node);
            return node.value;
        }
        return null;
    }

    /**
     * 更新節點位置,將剛訪問過的節點移至鏈表尾部
     */
    public void updateNode(JachinLRUNode node) {
        if (tail == node) { // 如果當前node就是末尾,則不做操作
            return;
        }
        if (head == node) { // 如果當前node是頭節點,則將其下一節點作爲頭節點
            head = node.next;
            head.pre = null;
        } else {
            // 調整雙向鏈表指針,將當前節點刪除
            node.pre.next = node.next;
            node.next.pre = node.pre;
        }
        // 將當前節點接到尾部,成爲新的尾節點
        node.pre = tail;
        node.next = null;
        tail.next = node;
        tail = node;
    }

    /**
     * 刪除鏈表頭節點
     */
    public JachinLRUNode removeHead() {
        if (head == null) {
            return null;
        }
        JachinLRUNode oldHead = head;
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            head = oldHead.next; // 設置新頭
            oldHead.next = null; // 斷開連接
            head.pre = null;
        }
        return oldHead; // 返回舊頭,以便map中刪除
    }

    /**
     * 添加節點到鏈表尾部
     */
    public void addTail(JachinLRUNode newNode) {
        if (newNode == null) {
            return;
        }
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode; // 插入新節點
            newNode.pre = tail;
            tail = newNode; // 更新尾節點爲新節點
        }
    }
}

/**
 * LRUCache中value元素封裝類型
 * 用JachinLRUNode實現雙向鏈表,實現O(1)的刪除節點(刪除最近最少使用節點)
 */
class JachinLRUNode<K, V> {

    K key;
    V value;
    JachinLRUNode pre;
    JachinLRUNode next;

    public JachinLRUNode(K key, V value) {
        this.key = key;
        this.value = value;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章