白话 LRU 缓存及链表的数据结构讲解(三)

双向链表

链表的作用就是按照访问的时间顺序,无论单链表或双链表都如此。我们在单链表的例子看到,维护单链表通常离不开从头部节点开始遍历的操作,尽管有许多巧妙的优化办法,但是只要从链表中查找某个元素(随机访问),必然还是离不开遍历操作。有鉴于此,我们希望可以常数时间内(O(1))随机访问元素,这样就很容易想到 HashMap 了,没错,就是要让 HashMap 加入进来使用。另外,有人问,直接用 HashMap 不行么?HashMap 本身无顺序,而且要定位某个元素,还是要遍历这个 Map。

至于双向链表(Double Linked List),插入、删除更简单,关键双向链表的操作基本都是 O(1) ,更快了,这里我们就毫不客气地使用了。另外,有人问,单纯用双链表不行么?也可以但改进的意义不大,查找元素还是得遍历。
在这里插入图片描述
关于各个选型的时间复杂度比较

  • 单链表:O(n)
  • 单链表 + HashMap:O(n) 意义不大
  • 双向链表:部分 O(1),查找元素还是得遍历 O(n)
  • 双向链表+ HashMap:O(1)
  • HashMap:访问 O(1),但遍历 O(n)
  • 队列:队列只能做到先进先出,但是重复用到中间的数据时无法把中间的数据移动到顶端

下一步的优化方法便是采用双向链表+ HashMap。

public class LRUCache<K, V> {
	static class Node<K, V> {
		K key;
		V value;
		Node<K, V> pre;
		Node<K, V> next;

		public Node(K key, V value) {
			this.key = key;
			this.value = value;
		}
	}

	private HashMap<K, Node<K, V>> map;
	private int capicity, count;
	private Node<K, V> head, tail;

	/**
	 * 
	 * @param capacity 缓存容量
	 */
	public LRUCache(int capacity) {
		this.capicity = capacity;
		map = new HashMap<>();
		head = new Node<>(null, null);
		tail = new Node<>(null, null);

		head.next = tail;
		head.pre = null;
		tail.pre = head;
		tail.next = null;
		count = 0;
	}

	/**
	 * 加到头部
	 * 
	 * @param node
	 */
	public void addToHead(Node<K, V> node) {
		node.next = head.next;
		node.next.pre = node;
		node.pre = head;
		head.next = node;
	}

	/**
	 * 删除节点
	 * 
	 * @param node
	 */
	public void deleteNode(Node<K, V> node) {
		node.pre.next = node.next;
		node.next.pre = node.pre;
	}

	public V get(int key) {
		if (map.get(key) != null) {
			Node<K, V> node = map.get(key);
			V result = node.value;
			deleteNode(node);
			addToHead(node);
			
			return result;
		}

		return null; // 找不到
	}

	public void set(K key, V value) {
		System.out.println("Going to set the (key, value) : (" + key + ", " + value + ")");
		if (map.get(key) != null) { // 已存在
			Node<K, V> node = map.get(key);
			node.value = value;
			deleteNode(node);
			addToHead(node);
		} else {// 插入新的
			Node<K, V> node = new Node<>(key, value);
			map.put(key, node);
			
			if (count < capicity) {
				count++;
				addToHead(node);
			} else { // 满了
				map.remove(tail.pre.key);
				deleteNode(tail.pre);
				addToHead(node);
			}
		}
	}
}

掌握了前面的这些基础,再回头来看看双向链表(Double Linked List),感觉轻松简单多了。

浓缩版

就是 LinkedHashMap 啦,网上一堆介绍,笔者就不重复了。

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Created by liuzhao on 14-5-15.
 */
public class LRUCache2<K, V> extends LinkedHashMap<K, V> {
    private final int MAX_CACHE_SIZE;

    public LRUCache2(int cacheSize) {
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
        MAX_CACHE_SIZE = cacheSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_CACHE_SIZE;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<K, V> entry : entrySet()) {
            sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
        }
        return sb.toString();
    }
}

带锁的线程安全的LRULinkedHashMap简单实现 https://blog.csdn.net/a921122/article/details/51992713

参考文献

https://www.cnblogs.com/dolphin0520/p/3741519.html
https://my.oschina.net/zjllovecode/blog/1634410
https://www.jianshu.com/p/b1ab4a170c3c
http://www.sohu.com/a/298778364_115128
https://www.geeksforgeeks.org/design-a-data-structure-for-lru-cache/
https://blog.csdn.net/qq_34417408/article/details/79308899 分离链表结构和 lru

发布了293 篇原创文章 · 获赞 260 · 访问量 232万+

猜你喜欢

转载自blog.csdn.net/zhangxin09/article/details/90517338