雙向鏈表與LRU算法實現

雙向鏈表與LRU算法

各位很久不見啊,因爲疫情緣由筆者一直宅在家中作考研複習。俗語云:聚沙成塔,跬步千里。因而我在此作一個簡單分享,一步步記錄個人學習歷程。java

先從單鏈表談起

道家有言:一輩子二,二生三,三生萬物 ,萬物皆有源頭,在說雙向鏈表以前讓咱們先看看單鏈表吧。node

咱們在學習計算機編程語言時,最早接觸的數據結構線性表,線性表是邏輯結構,其根據存儲方式的不一樣,又分爲 順序表鏈表。而 單鏈表是鏈表中最基礎的結構。算法

以下圖所示,編程

其中,咱們有兩個節點,第一個節點的值爲10,並擁有一個指針指向下一個節點15。緩存

可能的類代碼:數據結構

public class SLList {
   private IntNode first;
   public SLList() {
      first = null;
   }

   public SLList(int x) {
      first = new IntNode(x, null);
   }
 
   public void addFirst(int x) {
      first = new IntNode(x, first);
   }
   public int getFirst() {
  	  return first.item;
   }
}

規範——哨兵節點的誕生

在上面的單鏈表中,咱們實現了從頭結點插入的功能,若是咱們要實現從鏈表的尾部插入的功能呢?編程語言

咱們可能會這樣寫:學習

public void addLast(int x) {
      size += 1;
      IntNode p = first;
      while (p.next != null) {
         p = p.next;
      }
      p.next = new IntNode(x, null);
   }

可是,若是咱們要插入到一個空鏈表時,由於 first自己是 null ,當咱們運行到 while(p.next != null)時,程序會發生錯誤!this

有的同窗就會想到,那咱們加一個 if 處理不就好了。操作系統

if (first == null) {
    first = new IntNode(x, null);
    return;
  }
while(p.next != null){
    p = p.next;
}
p.next = new IntNode(x,null);

可是,這樣處理問題會顯得不美觀。並且當你處理的特殊狀況愈來愈多的時候,你的代碼會愈來愈長,致使難以閱讀和維護,並破壞了簡單設計的原則。

這個時候咱們的大救星,哨兵節點,閃亮登場。

如上圖所示,咱們在初始化空鏈表時,會建立一個哨兵節點,他不存儲值,只是提供了一個守門員的角色,幫助你看看門外有沒有人並幫助你尋找後面的節點。咱們把它叫作 sentinel

這樣咱們就不用擔憂會遇到空節點的狀況,萬歲。事情變得簡單規範化了,沒有特殊例子!

咱們能夠這樣寫代碼了,去掉了 if語句:

IntNode p = sentinel;
  while (p.next != null) {
    p = p.next;
  }
p.next = new IntNode(x,null);

夠不着怎麼辦

咱們解決了從頭部插入和從尾部插入的問題,可是若是咱們要刪除最後一個節點呢?時間複雜度是多少?

顯然,咱們要從頭節點,一直找下去,直到導數第二個節點,時間複雜度爲 O(n)。有沒有辦法縮短期呢?

終極進化

若是咱們想要刪除最末尾的節點,顯然咱們要找到最後的節點和倒數第二個節點,因此咱們能夠添加一個指向上一個節點的指針。並添加指向最末尾的指針,一直指向最後一個節點。

這樣的結構夠好麼?別忘了還有咱們的哨兵朋友們!

最後綜合上述緣由,咱們造出了帶有哨兵節點的雙向鏈表!以下圖所示:

雙向鏈表的實現

上面咱們講了雙向鏈表的由來,這裏咱們正式實現雙向鏈表:

API:

  • addFirst : 頭插入
  • removeFirst: 刪除頭節點
  • addLast: 尾插入
  • removeLast: 刪除尾節點
public class DLList<T> {
    // 使用了泛型實現雙向鏈表
    private TNode sentinel;
    private int size;

    // 新建內部類,節點
    public class TNode{
        TNode prev;
        TNode next;
        T item;
        public TNode(T item,TNode prev,TNode next){
            this.item = item;
            this.prev = prev;
            this.next = next;
        }
    }
	// 新建空鏈表
    public DLList(){
        sentinel = new TNode(null,null,null);
        sentinel.prev = sentinel.next = sentinel;
        size = 0;
    }

    public void addFirst(T item){
        TNode newNode = new TNode(item,sentinel,sentinel.next);
        sentinel.next.prev = newNode;
        sentinel.next = newNode;
        size+=1;

    }
    public boolean validateIndex(int index){
        if(index<0||index>=size){
            return false;
        }
        return true;
    }
    /*
     * helper method to get the node we need
     * */
    private TNode getNode(int index){
        TNode res;
        if(index<size/2){
            res = sentinel.next;
            for (int i=0;i<index;i++){
                res = res.next;
            }
            return res;
        }
        res = sentinel.prev;
        int newIndex = size - index -1;
        for (int i = 0 ;i<newIndex;i++){
            res = res.prev;
        }
        return res;
    }

    public T get(int index){
        if(!validateIndex(index)) return null;
        return getNode(index).item;
    }


    public int size(){
        return size;
    }
    public boolean isEmpty(){
        return size==0;
    }
    public void addLast(T item){
        TNode newNode = new TNode(item,sentinel.prev,sentinel);
        sentinel.prev.next = newNode;
        sentinel.prev = newNode;
        size+=1;
    }
    /*
     * helper method to delete the node we want
     * */
    private T delete(int index){
        if(!validateIndex(index)) throw new IndexOutOfBoundsException();
        TNode cur = getNode(index);
        T res = cur.item;
        cur.prev.next = cur.next;
        cur.next.prev = cur.prev;
        cur = null;
        size--;
        return res;
    }
    public T removeLast(){
        return delete(size-1);
    }

    public T removeFirst(){
        return  delete(0);
    }
}

LRU算法

學習過計算機操做系統的小夥伴,必定知道咱們管理內存時須要頁面置換算法。其中一種經典的算法就是LRU算法(最近最久未使用算法)。

利用雙向鏈表,咱們能夠軟件模擬這種操做。每次使用數據,或者插入新數據的時候,咱們把它移動到頭部。

這樣越靠近頭部的就是咱們常用的數據。而當數據滿了的時候,咱們只要刪除尾部的節點就行了,由於他是最久未使用的數據。

衆所周知,鏈表的遍歷是線性的,當咱們要查詢數據的時候,速度並不理想。因而咱們引入哈希表加速查找。


具體實現

LRU 緩存機制能夠經過哈希表輔以雙向鏈表實現,咱們用一個哈希表和一個雙向鏈表維護全部在緩存中的鍵值對。

雙向鏈表按照被使用的順序存儲了這些鍵值對,靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的。

哈希表即爲普通的哈希映射(HashMap),經過緩存數據的鍵映射到其在雙向鏈表中的位置。

這樣一來,咱們首先使用哈希表進行定位,找出緩存項在雙向鏈表中的位置,隨後將其移動到雙向鏈表的頭部,便可在 O(1), O(1) 的時間內完成 get 或者 put 操做。具體的方法以下:

對於 get 操做,首先判斷 key 是否存在:

若是 key 不存在,則返回 -1−1;

若是 key 存在,則 key 對應的節點是最近被使用的節點。經過哈希表定位到該節點在雙向鏈表中的位置,並將其移動到雙向鏈表的頭部,最後返回該節點的值。

對於 put 操做,首先判斷 key 是否存在:

若是 key 不存在,使用 key 和 value 建立一個新的節點,在雙向鏈表的頭部添加該節點,並將 key 和該節點添加進哈希表中。而後判斷雙向鏈表的節點數是否超出容量,若是超出容量,則刪除雙向鏈表的尾部節點,並刪除哈希表中對應的項;

若是 key 存在,則與 get 操做相似,先經過哈希表定位,再將對應的節點的值更新爲 value,並將該節點移到雙向鏈表的頭部。

上述各項操做中,訪問哈希表的時間複雜度爲 O(1)O(1),在雙向鏈表的頭部添加節點、在雙向鏈表的尾部刪除節點的複雜度也爲 O(1)O(1)。而將一個節點移到雙向鏈表的頭部,能夠分紅「刪除該節點」和「在雙向鏈表的頭部添加節點」兩步操做,均可以在 O(1)O(1) 時間內完成。

代碼以下:

public class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用僞頭部和僞尾部節點
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 若是 key 存在,先經過哈希表定位,再移到頭部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 若是 key 不存在,建立一個新的節點
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加進哈希表
            cache.put(key, newNode);
            // 添加至雙向鏈表的頭部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 若是超出容量,刪除雙向鏈表的尾部節點
                DLinkedNode tail = removeTail();
                // 刪除哈希表中對應的項
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 若是 key 存在,先經過哈希表定位,再修改 value,並移到頭部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

更多

引用:

^1鏈表定義

^2緩存文件置換機制

^3leetcode

相關文章
相關標籤/搜索