白话 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。

java 复制代码
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 啦,网上一堆介绍,笔者就不重复了。

java 复制代码
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 简单实现 blog.csdn.net/a921122/art...

参考文献

www.cnblogs.com/dolphin0520... my.oschina.net/zjllovecode... www.jianshu.com/p/b1ab4a170... www.sohu.com/a/298778364... www.geeksforgeeks.org/design-a-da... blog.csdn.net/qq_34417408... 分离链表结构和 lru

相关推荐
Tony Bai5 小时前
Rust 看了流泪,AI 看了沉默:扒开 Go 泛型最让你抓狂的“残疾”类型推断
开发语言·人工智能·后端·golang·rust
用户3167361303425 小时前
javaLangchain4j从官方文档入手,看他做了什么——具体使用(二)
后端
無名路人5 小时前
Zsh 脚本 + VS Code 任务:NestJS + Vue3 一键部署到 1Panel
运维·后端·自动化运维
ybwycx5 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
程途知微6 小时前
AQS 同步器——Java 并发框架的核心底座全解析
java·后端
iPadiPhone7 小时前
分布式架构的“润滑剂”:RabbitMQ 核心原理与大厂面试避坑指南
分布式·后端·面试·架构·rabbitmq
武子康7 小时前
大数据-255 离线数仓 - Apache Atlas 数据血缘与元数据管理实战指南
大数据·后端·apache hive
javaTodo7 小时前
IntelliJ IDEA 2026.1 上强度了:Spring 运行时 Debug + AI 全面接入,太香了
后端
晴栀ay7 小时前
Generator + RxJS 重构 LLM 流式输出的“丝滑”架构
javascript·后端·llm
下次一定x7 小时前
深度解析 Kratos 客户端服务发现与负载均衡:从 Dial 入口到 gRPC 全链路落地(下篇)
后端·go