用Java实现GPS轨迹弯道识别:从Ramer-Douglas-Peucker抽稀到三点夹角计算的完整实战
2026/5/16 19:38:31
| 算法名称 | 核心逻辑 | 优点 | 缺点 |
|---|---|---|---|
| FIFO(先进先出) | 按照数据进入缓存的顺序淘汰,先进入的先淘汰 | 实现简单(用队列即可) | 未考虑数据访问频率,可能淘汰常用数据 |
| LFU(最少使用) | 淘汰访问次数最少的数据 | 考虑了数据访问频率 | 实现复杂(需记录访问次数),可能淘汰近期频繁使用但总次数少的数据 |
| LRU(最近最少使用) | 淘汰最长时间未被访问的数据 | 兼顾访问顺序和效率,实现难度适中 | 对突发的大量新数据访问不友好(缓存污染) |
| LRU-K | 淘汰最近 K 次访问中最久未访问的数据 | 优化了 LRU 的缓存污染问题 | 实现更复杂(需记录访问历史) |
prev指针,可直接获取前驱节点。prev指针直接定位前驱,时间复杂度 O (1)。实现一个支持O (1) 时间复杂度的查找、插入、删除、移动操作的缓存,且遵循 “最近最少使用” 的淘汰策略。
key -> 双向链表节点的映射,实现O (1) 查找节点。java
运行
import java.util.HashMap; import java.util.Map; /** * LRU Cache 实现:哈希表 + 双向链表(哑节点优化) * 核心逻辑:哈希表保证O(1)查找,双向链表保证O(1)插入/删除/移动 */ public class LRUCache { // 1. 双向链表节点类(静态内部类,仅当前类使用) static class DLinkedNode { int key; // 存储key:删除节点时需同步从哈希表移除,必须保存key(重点) int value; DLinkedNode prev; // 前驱节点 DLinkedNode next; // 后继节点 // 无参构造 public DLinkedNode() {} // 带参构造:初始化key和value public DLinkedNode(int key, int value) { this.key = key; this.value = value; } } // 2. LRUCache核心属性 private final int capacity; // 缓存容量(不可变,final修饰) private int size; // 当前缓存数据个数 private final Map<Integer, DLinkedNode> cache; // 哈希表:key -> 节点 private final DLinkedNode head; // 哑头节点(哨兵):避免空指针(难点) private final DLinkedNode tail; // 哑尾节点(哨兵):避免空指针(难点) // 3. 构造方法:初始化缓存容量、哈希表、双向链表 public LRUCache(int capacity) { // 参数校验:避免容量为0或负数(边界条件,补充优化) if (capacity <= 0) { throw new IllegalArgumentException("缓存容量必须大于0!"); } this.capacity = capacity; this.size = 0; this.cache = new HashMap<>(); // 初始化哑节点:head <-> tail(解决tail.prev为空的空指针问题,难点) this.head = new DLinkedNode(); this.tail = new DLinkedNode(); head.next = tail; tail.prev = head; } // 4. 私有辅助方法:双向链表的核心操作(封装细节,提高复用性) /** * 移除双向链表中的指定节点(通用操作,O(1)) * @param node 要移除的节点 */ private void removeNode(DLinkedNode node) { // 步骤:将节点的前驱和后继直接相连,跳过当前节点 node.prev.next = node.next; node.next.prev = node.prev; } /** * 将节点添加到双向链表的尾部(最近使用的位置,O(1),难点:四指针修改) * @param node 要添加的节点 */ private void addToTail(DLinkedNode node) { // 步骤:先关联node与tail的前驱,再关联node与tail node.prev = tail.prev; node.next = tail; tail.prev.next = node; tail.prev = node; } /** * 将指定节点移动到链表尾部(访问/更新节点时调用,O(1)) * @param node 要移动的节点 */ private void moveToTail(DLinkedNode node) { removeNode(node); // 先从原位置移除 addToTail(node); // 再添加到尾部 } /** * 移除双向链表的头部节点(最近最少使用的节点,O(1)) * @return 被移除的节点(用于同步从哈希表删除key) */ private DLinkedNode removeHead() { DLinkedNode delNode = head.next; // 哑头的下一个节点是真正的头节点 removeNode(delNode); // 移除该节点 return delNode; } // 5. 公有方法:外部调用的get和put(核心业务方法,重点) /** * 获取key对应的value(不存在则返回-1,存在则将节点移到尾部) * @param key 要查找的key * @return 对应的value,不存在则返回-1 */ public int get(int key) { DLinkedNode node = cache.get(key); // 情况1:key不存在,返回-1 if (node == null) { return -1; } // 情况2:key存在,将节点移到尾部(标记为最近使用) moveToTail(node); return node.value; } /** * 插入/更新key-value对(核心逻辑:处理key存在/不存在,容量超限) * @param key 要插入/更新的key * @param value 对应的value */ public void put(int key, int value) { DLinkedNode node = cache.get(key); // 情况1:key不存在,新建节点 if (node == null) { DLinkedNode newNode = new DLinkedNode(key, value); // 步骤1:将新节点存入哈希表 cache.put(key, newNode); // 步骤2:将新节点添加到链表尾部 addToTail(newNode); // 步骤3:当前大小+1 size++; // 步骤4:判断是否超出容量,超出则移除最近最少使用的节点(头部) if (size > capacity) { DLinkedNode delNode = removeHead(); // 同步从哈希表删除该节点的key(必须用节点的key,重点) cache.remove(delNode.key); // 当前大小-1 size--; } } else { // 情况2:key存在,更新value并将节点移到尾部 node.value = value; moveToTail(node); } } // 辅助测试方法:打印缓存链表(调试用) public void printCache() { DLinkedNode cur = head.next; while (cur != tail) { System.out.print("(" + cur.key + ":" + cur.value + ") -> "); cur = cur.next; } System.out.println("null"); } // 主方法测试:验证LRU逻辑 public static void main(String[] args) { LRUCache lruCache = new LRUCache(2); // 容量为2 lruCache.put(1, 1); lruCache.printCache(); // 输出:(1:1) -> null lruCache.put(2, 2); lruCache.printCache(); // 输出:(1:1) -> (2:2) -> null System.out.println(lruCache.get(1)); // 输出:1,此时1移到尾部 lruCache.printCache(); // 输出:(2:2) -> (1:1) -> null lruCache.put(3, 3); // 容量满,移除最近最少使用的2 lruCache.printCache(); // 输出:(1:1) -> (3:3) -> null System.out.println(lruCache.get(2)); // 输出:-1(已被移除) lruCache.put(4, 4); // 容量满,移除最近最少使用的1 lruCache.printCache(); // 输出:(3:3) -> (4:4) -> null System.out.println(lruCache.get(1)); // 输出:-1(已被移除) System.out.println(lruCache.get(3)); // 输出:3,移到尾部 lruCache.printCache(); // 输出:(4:4) -> (3:3) -> null System.out.println(lruCache.get(4)); // 输出:4,移到尾部 lruCache.printCache(); // 输出:(3:3) -> (4:4) -> null } }key、value、prev、next,是双向链表的基本单元。capacity:缓存容量(final 修饰,保证不可变)。size:当前缓存数据个数。cache:哈希表,映射 key 到节点。head/tail:哑头 / 哑尾节点,简化链表操作。get(获取数据)、put(插入 / 更新数据),实现 LRU 核心逻辑。printCache(打印链表)、main(验证逻辑)。key的必要性(易忽略重点)value,无法获取 key,哈希表的删除操作无法完成。DLinkedNode类中的int key属性,removeHead()方法返回节点后,通过delNode.key删除哈希表中的键值对。get/put方法的核心逻辑(业务重点)get方法逻辑put方法逻辑size++。size > capacity,删除链表头部节点,同步删除哈希表中的 key,size--。get/put方法的所有操作均为 O (1)(哈希表操作 + 链表操作)。head/tail为null,调用head.next或tail.prev会触发空指针异常(如会议中提到的addToTail方法中tail.prev为空的问题)。head(哑头)和tail(哑尾)两个虚拟节点,初始化时让head.next = tail、tail.prev = head,真实节点均在两者之间。addToTail方法,操作难点)node.prev = tail.prev;(节点的前驱指向 tail 的前驱)。node.next = tail;(节点的后继指向 tail)。tail.prev.next = node;(tail 的前驱的后继指向节点)。tail.prev = node;(tail 的前驱指向节点)。put方法中判断 key 存在时,更新 value 并移动节点(代码中已处理)。removeEldestEntry方法,当方法返回true时,LinkedHashMap 会自动删除最久未使用的节点。java
运行
import java.util.LinkedHashMap; import java.util.Map; public class LRUCacheByLinkedHashMap extends LinkedHashMap<Integer, Integer> { private final int capacity; public LRUCacheByLinkedHashMap(int capacity) { // 初始化:初始容量16,负载因子0.75,按访问顺序排序(第三个参数为true) super(16, 0.75f, true); this.capacity = capacity; } // 重写方法:当节点数超过容量时,返回true,自动删除最久未使用的节点 @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { return size() > capacity; } // 测试 public static void main(String[] args) { LRUCacheByLinkedHashMap lru = new LRUCacheByLinkedHashMap(2); lru.put(1, 1); lru.put(2, 2); System.out.println(lru.get(1)); // 访问1,移到尾部 lru.put(3, 3); // 容量满,删除2 System.out.println(lru.get(2)); // 返回null(或-1,可封装) } }Collections.synchronizedMap包装。size计数错误、链表断裂、哈希表并发修改异常)。get/put方法上加synchronized关键字(缺点:性能较低,同一时间只能有一个线程访问)。ConcurrentHashMap替代HashMap,结合ReentrantLock锁(分段锁,性能更高)。LRU Cache 是算法与开发中的高频考点,核心在于哈希表 + 双向链表的组合使用,重点掌握数据结构的协同逻辑、get/put方法的业务逻辑,难点在于哑节点的设计和双向链表的指针操作。在实战中,需注意线程安全和边界条件处理,也可利用 Java 自带的 LinkedHashMap 快速实现 LRU,或使用 Redis 等中间件实现分布式缓存。