科普文:算法和数据结构系列【数据结构:链表扩展之有序链表(Sorted Linked List)和LRU缓存链表(LRU Cache Linked List)原理、应用即java实现】
概叙
科普文:算法和数据结构系列【算法和数据结构概叙】-CSDN博客
科普文:算法和数据结构系列【数据结构:数组和链表】-CSDN博客
科普文:算法和数据结构系列【数据结构:链表扩展之跳表Skip List原理、应用即java实现跳表】-CSDN博客
双向链表 (Doubly Linked List)
双向链表是一种链表数据结构,其中每个节点包含数据、指向前一个节点的指针(prev)和指向下一个节点的指针(next)。这使得节点可以从两个方向遍历,让双向链表可以从任意方向遍历链表。
原理
双向链表允许从任意节点开始向前或向后遍历链表。插入和删除操作需要修改相邻节点的指针,但通常可以在常数时间内完成。
在双向链表中,插入新元素时,需要根据元素的值找到合适的位置,以确保链表的有序性。这通常涉及到遍历链表,找到合适的位置来插入新节点。
优缺点
优点:插入和删除操作相对较快,因为不需要移动大量元素;可以快速访问最小(或最大)值。
- 插入和删除操作高效,特别是在已知节点位置的情况下。
- 双向遍历能力提供了灵活性,适用于需要双向访问的场景。
缺点:访问效率低,需要从头节点开始遍历。
- 相对于单向链表,每个节点需要额外的指针存储空间。
- 链表本身不是随机访问的,访问特定元素可能需要O(n)的时间复杂度。
应用场景
优先队列、索引结构、缓存(如LRU缓存)、排序算法(如归并排序)、统计数据(如计算中位数、百分位数)。
- 需要在链表两端频繁进行插入和删除操作的数据结构。
- 双向遍历需求,如撤销/重做功能、双向队列等。
注意事项
插入和删除操作的时间复杂度为O(n),在链表中间插入或删除时。需要额外的空间来维护元素的顺序。
- 插入和删除操作需要维护前后指针的正确性。
- 遍历链表时,要注意边界条件,如空链表或只有一个节点的链表。
java实现双向链表
// 创建一个双向链表的类
class DoubleLinkedList {
// 先初始化一个头节点, 头节点不要动, 不存放具体的数据
private HeroNode2 head = new HeroNode2(0, "", "");
// 返回头节点
public HeroNode2 getHead() {
return head;
}
// 遍历双向链表的方法
// 显示链表[遍历]
public void list() {
// 判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
// 因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode2 temp = head.next;
while (true) {
// 判断是否到链表最后
if (temp == null) {
break;
}
// 输出节点的信息
System.out.println(temp);
// 将temp后移, 一定小心
temp = temp.next;
}
}
// 添加一个节点到双向链表的最后.
public void add(HeroNode2 heroNode) {
// 因为head节点不能动,因此我们需要一个辅助遍历 temp
HeroNode2 temp = head;
// 遍历链表,找到最后
while (true) {
// 找到链表的最后
if (temp.next == null) {//
break;
}
// 如果没有找到最后, 将将temp后移
temp = temp.next;
}
// 当退出while循环时,temp就指向了链表的最后
// 形成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
// 修改一个节点的内容, 可以看到双向链表的节点内容修改和单向链表一样
// 只是 节点类型改成 HeroNode2
public void update(HeroNode2 newHeroNode) {
// 判断是否空
if (head.next == null) {
System.out.println("链表为空~");
return;
}
// 找到需要修改的节点, 根据no编号
// 定义一个辅助变量
HeroNode2 temp = head.next;
boolean flag = false; // 表示是否找到该节点
while (true) {
if (temp == null) {
break; // 已经遍历完链表
}
if (temp.no == newHeroNode.no) {
// 找到
flag = true;
break;
}
temp = temp.next;
}
// 根据flag 判断是否找到要修改的节点
if (flag) {
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
} else { // 没有找到
System.out.printf("没有找到 编号 %d 的节点,不能修改\n", newHeroNode.no);
}
}
// 从双向链表中删除一个节点,
// 说明
// 1 对于双向链表,我们可以直接找到要删除的这个节点
// 2 找到后,自我删除即可
public void del(int no) {
// 判断当前链表是否为空
if (head.next == null) {// 空链表
System.out.println("链表为空,无法删除");
return;
}
HeroNode2 temp = head.next; // 辅助变量(指针)
boolean flag = false; // 标志是否找到待删除节点的
while (true) {
if (temp == null) { // 已经到链表的最后
break;
}
if (temp.no == no) {
// 找到的待删除节点的前一个节点temp
flag = true;
break;
}
temp = temp.next; // temp后移,遍历
}
// 判断flag
if (flag) { // 找到
// 可以删除
// temp.next = temp.next.next;[单向链表]
temp.pre.next = temp.next;
// 这里我们的代码有问题?
// 如果是最后一个节点,就不需要执行下面这句话,否则出现空指针
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.printf("要删除的 %d 节点不存在\n", no);
}
}
}
// 定义HeroNode2 , 每个HeroNode 对象就是一个节点
class HeroNode2 {
public int no;
public String name;
public String nickname;
public HeroNode2 next; // 指向下一个节点, 默认为null
public HeroNode2 pre; // 指向前一个节点, 默认为null
// 构造器
public HeroNode2(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
// 为了显示方法,我们重新toString
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
}
}
运行结果:
有序链表 (Sorted Linked List)
有序链表是一种链表数据结构,其中节点按照某种顺序(如升序或降序)排列。
有序链表是一种链表,其中元素按照特定的顺序(通常是升序或降序)排列。这种特性使得有序链表在某些应用中非常有用,例如在需要频繁插入与搜索的场景。
原理
插入新节点时,需要遍历链表以找到正确的插入位置,以保持链表的有序性。查找操作也可以利用链表的有序性来优化。
在有序链表中,插入新元素时,需要根据元素的值找到合适的位置,以确保链表的有序性。这通常涉及到遍历链表,找到合适的位置来插入新节点。
优缺点
优点:插入和删除操作相对较快,因为不需要移动大量元素;可以快速访问最小(或最大)值。
- 插入和删除操作虽然需要遍历链表,但链表本身可以动态增长,无需预分配空间。
- 查找操作(如果实现得当)可以利用有序性进行二分查找或类似优化,提高性能。
缺点:访问效率低,需要从头节点开始遍历。
- 插入和删除操作通常需要O(n)的时间复杂度。
- 访问特定元素仍然需要O(n)的时间复杂度,除非实现额外的索引结构。
应用场景
- 需要保持元素有序且频繁进行插入和删除操作的数据结构。
- 优先级队列等需要有序访问的场景。
- 优先队列
- 索引结构
- 缓存(如LRU缓存)
- 排序算法(如归并排序)
- 统计数据(如计算中位数、百分位数)
注意事项
- 插入和删除操作需要确保链表的有序性。
- 查找操作可以利用有序性进行优化,但实现复杂度可能增加。
- 插入和删除操作的时间复杂度为O(n),在链表中间插入或删除时。
- 需要额外的空间来维护元素的顺序。
java实现有序链表
package com.zxx.study.algorithm.datastruct.DataStructures.linkedlist;
/**
* @author zhouxx
* @create 2025-01-07 5:30
*/
// 有序链表类
public class SortedLinkedList<T extends Comparable<T>> {
private ListNode<T> head;
public SortedLinkedList() {
this.head = null;
}
// 插入数据,保持链表有序
public void insert(T data) {
ListNode<T> newNode = new ListNode<>(data);
// 插入前先比较大小:小的往前放、大的往后放。
if (head == null || head.data.compareTo(data) >= 0) {
newNode.next = head;
head = newNode;
} else {
ListNode<T> current = head;
while (current.next != null && current.next.data.compareTo(data) < 0) {
current = current.next;
}
newNode.next = current.next;
current.next = newNode;
}
}
// 删除数据
public boolean delete(T data) {
if (head == null) {
return false;
}
if (head.data.equals(data)) {
head = head.next;
return true;
}
ListNode<T> current = head;
while (current.next != null && !current.next.data.equals(data)) {
current = current.next;
}
if (current.next == null) {
return false; // 数据不存在于链表中
}
current.next = current.next.next;
return true;
}
// 查找数据
public boolean search(T data) {
ListNode<T> current = head;
while (current != null) {
if (current.data.equals(data)) {
return true;
}
current = current.next;
}
return false;
}
// 打印链表
public void printList() {
ListNode<T> current = head;
while (current != null) {
System.out.print(current.data + " -> ");
current = current.next;
}
System.out.println("null");
}
// 链表节点类
class ListNode<T extends Comparable<T>> {
T data;
ListNode<T> next;
ListNode(T data) {
this.data = data;
this.next = null;
}
}
// 主方法,用于测试有序链表的功能
public static void main(String[] args) {
SortedLinkedList<Integer> sortedList = new SortedLinkedList<>();
// 插入数据
sortedList.insert(5);
sortedList.insert(3);
sortedList.insert(8);
sortedList.insert(1);
sortedList.insert(7);
// 打印链表
System.out.println("链表内容:");
sortedList.printList();
// 查找数据
System.out.println("查找 3: " + sortedList.search(3));
System.out.println("查找 10: " + sortedList.search(10));
// 删除数据
sortedList.delete(3);
System.out.println("删除 3 后的链表内容:");
sortedList.printList();
sortedList.delete(1);
System.out.println("删除 1 后的链表内容:");
sortedList.printList();
sortedList.delete(8);
System.out.println("删除 8 后的链表内容:");
sortedList.printList();
}
}
运行结果:
核心方法:insert,在插入数据时,先排序,再插入。默认和头节点比,小的往前放、大的往后放。

LRU链表 (Least Recently Used Linked List)
LRU链表是实现LRU缓存替换策略的一种数据结构,其中最近使用的元素被移动到链表头部,最久未使用的元素位于链表尾部。
LRU(Least Recently Used)链表是一种常用的缓存淘汰策略,它根据数据的使用顺序来管理缓存。最近使用的数据被放置在链表的头部,而最久未使用的数据则被放置在链表的尾部。
原理
LRU链表通常基于双向链表实现,结合哈希表来快速查找缓存中的数据。当缓存达到其容量上限时,最近最少使用的数据会被优先淘汰。
LRU链表通常与哈希表结合使用。哈希表用于快速查找元素,而双向链表用于维护元素的使用顺序。每次访问元素时,都将其从链表中移除并重新插入到头部。当缓存达到容量限制时,尾部元素被移除。
优缺点
优点:高效,能够快速访问最近使用的项,减少缓存未命中率。
- 高效管理缓存,提高缓存命中率。
- 双向链表和哈希表的结合使得插入、删除和查找操作都能够在常数时间内完成。
缺点:缓存大小受限,无法缓存所有数据;频繁访问的项可能被淘汰。
- 相对于其他缓存策略(如FIFO、LFU),LRU可能不是所有情况下的最优选择。
- 需要额外的空间来存储指针和哈希表条目。
应用场景
- 缓存系统,如页面缓存、数据库查询缓存等。
- 需要频繁访问最近使用数据项的应用场景。
- 操作系统中的页面置换
- 数据库查询优化
- Web缓存等
科普文:Java基础之算法系列【哈希算法和位图:手搓LRU(Least Recently Used)过滤器】-CSDN博客
注意事项
实现LRU算法需要同时维护数据的访问顺序和快速查找能力,这可能会增加实现的复杂性。
- 需要确保链表和哈希表之间的同步,以避免数据不一致。
- 当缓存达到容量限制时,要及时移除尾部元素并更新哈希表。
- LRU链表可能不适合所有访问模式,选择缓存策略时应考虑具体应用场景。
java实现LRU链表
package com.zxx.study.algorithm.datastruct.DataStructures.linkedlist;
import java.util.HashMap;
/**
* @author zhouxx
* @create 2025-01-07 5:46
*/
// LRU缓存类
public class LRUCache<K, V> {
// 双向链表节点类
class DLinkedNode<K, V> {
K key;
V value;
DLinkedNode<K, V> prev;
DLinkedNode<K, V> next;
DLinkedNode(K key, V value) {
this.key = key;
this.value = value;
}
}
private final int capacity;
private final HashMap<K, DLinkedNode<K, V>> map;
private final DLinkedNode<K, V> head;
private final DLinkedNode<K, V> tail;
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>(capacity);
this.head = new DLinkedNode<>(null, null); // 哨兵头节点
this.tail = new DLinkedNode<>(null, null); // 哨兵尾节点
head.next = tail;
tail.prev = head;
}
// 获取缓存中的值
public V get(K key) {
DLinkedNode<K, V> node = map.get(key);
if (node == null) {
return null; // 缓存中不存在该键
}
System.out.print("key moveToHead :"+key+",value=");
moveToHead(node); // 将访问的节点移动到链表头部
return node.value;
}
// 将键值对放入缓存中
public void put(K key, V value) {
DLinkedNode<K, V> node = map.get(key);
if (node == null) {
// 插入新节点到链表头部,并更新哈希表
DLinkedNode<K, V> newNode = new DLinkedNode<>(key, value);
map.put(key, newNode);
addToHead(newNode);
if (map.size() > capacity) {
// 如果缓存超出容量,移除链表尾部的节点
DLinkedNode<K, V> tail = removeTail();
map.remove(tail.key);
}
} else {
// 更新已有节点的值,并将其移动到链表头部
node.value = value;
moveToHead(node);
}
}
// 将节点移动到链表头部
private void moveToHead(DLinkedNode<K, V> node) {
removeNode(node);
addToHead(node);
}
// 添加节点到链表头部
private void addToHead(DLinkedNode<K, V> node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
// 从链表中移除节点
private void removeNode(DLinkedNode<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 移除并返回链表尾部的节点
private DLinkedNode<K, V> removeTail() {
DLinkedNode<K, V> res = tail.prev;
removeNode(res);
return res;
}
// 打印链表
public void printList() {
DLinkedNode<K, V> current = head;
while (current != null) {
System.out.print("{key="+current.key+",value="+current.value + "} <--> ");
current = current.next;
}
System.out.println("null");
}
// 主方法,用于测试LRU缓存的功能
public static void main(String[] args) {
LRUCache<Integer, String> lruCache = new LRUCache<>(3);
lruCache.put(1, "One");
lruCache.printList();
lruCache.put(2, "Two");
lruCache.printList();
lruCache.put(3, "Three");
lruCache.printList();
System.out.println(lruCache.get(1)); // 输出: One;此时1已经移到头部了,2是尾部。
lruCache.printList();
lruCache.put(4, "Four"); // 2被移除,因为缓存容量是3,并且1
System.out.println(lruCache.get(2)); // 输出: null
lruCache.printList();
lruCache.put(5, "Five"); // 3被移除,因为缓存容量是3
lruCache.printList();
System.out.println(lruCache.get(3)); // 输出: null
lruCache.printList();
System.out.println(lruCache.get(4)); // 输出: Four
System.out.println(lruCache.get(5)); // 输出: Five
lruCache.printList();
lruCache.put(6, "Six"); // 1被移除,因为最近没有使用
lruCache.printList();
System.out.println(lruCache.get(1)); // 输出: null
lruCache.printList();
}
}

关键方法:get和put;以及moveToHead
get:命中key则往头部移动

put方法:默认往头部插入新数据(如果key,value已存在,则将其移到头部),超过LRU长度则移除尾节点。

moveToHead:往头部移动热点数据

更多推荐


所有评论(0)