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

相关推荐
会员源码网2 小时前
使用`mysql_*`废弃函数(PHP7+完全移除,导致代码无法运行)
后端·算法
木心月转码ing2 小时前
Hot100-Day10-T438T438找到字符串中所有字母异位词
算法
HelloReader3 小时前
Wi-Fi CSI 感知技术用无线信号“看见“室内的人
算法
颜酱6 小时前
二叉树分解问题思路解题模式
javascript·后端·算法
qianpeng8977 小时前
水声匹配场定位原理及实验
算法
董董灿是个攻城狮19 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
AI软著研究员1 天前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish1 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱1 天前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者2 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶