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

问题的讨论

前文讲述了数组、单链表来实现缓存的数据结构,一步一步地反映出 LRU 是如何被改进的。至今我们没有放下改进的脚步。在上一例单链表中遇到的一个问题是,提供泛型的 T 为单一记录值,无法处理 Key/Value 结构。通过百度,我们找到一仁兄的资源,比较不错,解决了该问题,并且在单链表的思路与上例更不一样,却更为简洁。

java 复制代码
public class LRUCache_2<K, V> {
	int cap;
	int size = 0;
	private Node<K, V> head;
	private Node<K, V> tail;

	/**
	 * 设定容量
	 * @param cap 设定容量
	 * @param db
	 */
	public LRUCache_2(int cap) {
		this.cap = cap;
	}

	public V get(K key) {
		Node<K, V> previous = null; // ahead 是之前的那个节点
		Node<K, V> temp = head;
		while (temp != null) {
			if (temp.key.equals(key)) {
				if (temp.next != null) // 找到了匹配的缓存
					moveToLast(previous, temp);
				return temp.value;
			}
			previous = temp;
			temp = temp.next;
		}
		return null;
	}

	private void moveToLast(Node<K, V> previous, Node<K, V> target) {
		if (target == tail)
			return;
		if (head == target) {
			head = target.next;// 头部不当头了,后面的上
		} else {
			previous.next = target.next; // target 被抽出来了,后面的接上
		}

		tail.next = target;// 续上新的, 放在最后插入
		tail = target;
		target.next = null;
	}

	public V put(K key, V value) {
		if (head == null) { // 空,直接插入
			tail = head = new Node<>(key, value);
			size++;
			return value;
		}

		Node<K, V> temp = head;
		while (temp != null) {
			if (temp.key.equals(key)) { // 已经存在
				if (temp.value.equals(value)) { // 没有不同
					return value;
				} else {
					V oldValue = temp.value; // 进行了值的替换
					temp.value = value;
					return oldValue;
				}
			}
			temp = temp.next;
		}
		// 找不到,是新成员
		temp = new Node<>(key, value);
		tail.next = temp;// 加入到尾部
		tail = temp;
		size++;

		checkCap();
		return value;
	}

	private void checkCap() {
		if (size > cap) {
			// 进行淘汰,摘除开头
			head = head.next;
			size--;
		}
	}

	public Node<K, V> peek() {
		return head;
	}

	public static class Node<K, V> {
		K key;
		V value;
		Node<K, V> next;
		Node(K key, V value) {
			this.key = key;
			this.value = value;
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		while (head != null) {
			sb.append(head.key + " " + head.value);
			head = head.next;
		}
		return sb.toString();
	}
}

笔者修改了几处地方,不影响主干意思。通过代码可见,该例并没有分离单链表类和 LRU 类,而是合二为一。另外类声明中除了 head 头部元素外,还给出了 tail 尾部元素,使得在执行 moveToLast() 时候无须遍历链表,改进了性能。

值得一提的是,该链表都是尾部加入新元素,在头部删除老元素------这一点与前面的例子刚好相反。删除的方法也直观简单:

java 复制代码
if (size > cap) {
	head = head.next;// 进行淘汰,摘除开头
	size--;
}

问题的讨论(二)

1、既然谈到优化问题,我们不妨再继续讨论。我们考察 moveToLast(),原本需要前驱节点 previous 参数,现在可否只需要一个 target 参数无须 previous?

java 复制代码
private void moveToLast(Node<K, V> previous, Node<K, V> target) {
	if (target == tail)
		return;

	if (head == target) {
		head = target.next;// 头部不当头了,后面的上
	} else {
		previous.next = target.next; // target 被抽出来了,后面的接上
	}

	tail.next = target;// 续上新的, 放在最后插入
	tail = target;
	target.next = null;
}

传统思维,如上例,是要获取到被删除结点前面的那一个结点的指针 next, 修过它为 target.next 才可以的,比如一个链表 A->B->C->D->E->F->G。如果我们要删除结点 E,那么我们只需要让结点 D 的指针指向结点 F 即可。但是因为限于单链表,是没有前驱节点的,所以要靠遍历链表获取前一个节点,显然这种方法是不能在 O(1) 时间内调整节点的。

我们能否换个思路?这里有个巧妙的方法,被删除结点E的后一个结点指针是很容易得到的,也就是 target.next,下一级也容易得到,即 target.next.next。于是当前节点对象不变,但 data 数据拷贝自下一个节点,并且 next 指向 target.next.next,那样等于"架空"了 target.next,却又保留 target.next 的数据,完成删除动作。moveToLast() 也是同理,如法炮制。

2、考察 get 方法,要获取匹配的 value 仍需要经过 while 循环,时间复杂度为 O(n)

java 复制代码
public V get(K key) {
	Node<K, V> previous = null; // ahead 是之前的那个节点

	Node<K, V> temp = head;
	while (temp != null) {
		if (temp.key.equals(key)) {
			if (temp.next != null) // 找到了匹配的缓存
				moveToLast(previous, temp);

			return temp.value;
		}

		previous = temp;
		temp = temp.next;
	}

	return null;
}

有没有办法可以让复杂度也在 O(1)? 实际上 moveToLast() 的操作是 O(1) 时间复杂度,因为提供了 tail 尾部指针,这已经有点 双向链表的意味了。------没错,使用双向链表,所有操作都是 O(1) 时间复杂度(除了遍历)。下一节将深入讨论。

文章贡献:www.cnblogs.com/dolphin0520...

相关推荐
小跌—15 分钟前
Linux:进程信号理解
linux·c++·算法
l12345sy1 小时前
Day22_【机器学习—集成学习(2)—Bagging—随机森林算法】
算法·机器学习·集成学习·bagging·随机森林算法
snowfoootball1 小时前
近期算法学习记录
学习·算法
今天也好累2 小时前
C++ 小游戏:拍桌子
c++·笔记·学习·算法
KyollBM2 小时前
【CF】Day139——杂题 (绝对值变换 | 异或 + 二分 | 随机数据 + 图论)
算法
纪元A梦2 小时前
贪心算法应用:交易费优化问题详解
算法·贪心算法
Miraitowa_cheems3 小时前
LeetCode算法日记 - Day 34: 二进制求和、字符串相乘
java·算法·leetcode·链表·职场和发展
wan5555cn3 小时前
AI生成内容的版权问题解析与实操指南
人工智能·笔记·深度学习·算法·音视频
DDAshley1264 小时前
【PaddleOCR】从零开始训练自己的模型--详细教程
算法·计算机视觉