暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

链表:内存里的“灵活小火车”,搞定LRU就靠它!

今天聊聊链表(Linked List)。学这玩意儿有啥用?别急,一个经典场景马上揭晓:LRU缓存淘汰算法。它可是链表大展拳脚的舞台!

想象一下你的书架(缓存)满了,新书来了,必须扔掉一些。扔旧的(FIFO)?扔看得少的(LFU)?还是扔最近没碰过的(LRU)?这就是缓存淘汰策略!如何用链表实现高效的LRU? 带着这个悬念,我们发车!

一、链表 vs 数组:内存里的“住法”大不同

  • 数组:住“集体宿舍” - 要求一大片连续的内存空间。申请100MB数组?必须找到一整块100MB的空地!即使总内存够,碎片太多也可能失败。

  • 链表:住“分散公寓” - 通过指针(地址)把零散的内存块(结点)串起来。申请100MB链表?只要零散小空间总和够100MB就行!灵活性完胜。

二、五花八门的“小火车”:链表家族秀

链表家族成员不少,重点介绍三位主角:

  1. 单链表:单向寻宝图

    • 插入/删除:在已知位置操作,只需修改相邻结点的指针(改“藏宝图”),O(1) 搞定!超高效。(想想在火车中间加挂或摘掉一节车厢,只需改前后连接钩

    • 查找(访问第k个):只能从车头开始,一节一节往后数,“藏宝图”一张一张看,O(n) 较慢。(不知道第k节在哪,只能从头数

    • 车头(头结点):记录起点地址,没有宝藏(不存数据),只指向第一节载物车厢。有了它,整列火车都能找到。

    • 车尾(尾结点):它的“藏宝图”指向NULL
      (空地),表示终点。

    • 结构:每个“车厢”(结点)装载数据
       + 一张指向下一个车厢的“藏宝图”(后继指针 next
      )。

    • 特殊车厢

    • 操作特点

  2. 循环链表:首尾相接的贪吃蛇

    • 结构:本质是单链表,但车尾的“藏宝图”不指空地,而是指向车头! 形成一个闭环。

    • 优势:从车尾回找车头很方便。处理环形数据(如约瑟夫问题)代码更简洁优雅。(在环上,哪都是起点也是终点

  3. 双向链表:自带前后视镜的豪华列车 ⭐️(更常用!)

    • 查找前驱(前一个结点):直接通过prev
      找到!O(1)

    • 特定场景下删除/插入效率飙升

    • 有序链表按值查询:可以记录上次位置,决定向前(prev
      )还是向后(next
      )查找,平均只需查一半

    • 单链表:删q
      需要知道它前一个(p
      )。只能从车头遍历找p
      (直到p->next == q
      ),O(n)

    • 双向链表:通过q->prev
      直接拿到前驱p
      !删q
      只需改p->next
      q->next->prev
      (如果有),O(1)

    • 情景1:删除“值等于X”的结点?单/双向都得O(n)遍历找X。

    • 情景2删除已知指针q
      指向的结点?

    • 在已知结点前插入:同理,双向链表O(1),单链表O(n)

    • next
      :指向下一个车厢。

    • prev
      :指向上一个车厢。

    • 结构:每个“车厢”升级!装载数据
       + 两张“藏宝图”

    • 代价:多存一个指针,占用内存稍大。

    • 巨大优势双向遍历能力!

✨ 设计思想点睛:空间换时间
双向链表多用内存(prev
指针),换来了关键操作的极致速度(O(1))。缓存(Cache)本身也是“空间换时间”的典范:用宝贵的内存空间缓存数据,换取远超硬盘/网络的访问速度!开发中,内存充足时优先考虑速度;内存紧张(如单片机)则需“时间换空间”。

三、链表 vs 数组:性能擂台赛

操作
数组
链表
胜出方
随机访问
O(1) (寻址公式直达)
O(n) (需遍历)
数组
插入删除
O(n) (需搬移保连续性)
O(1) (已知位置改指针)
链表
内存
连续大块,易触发OOM
零散利用,天然支持动态扩容
链表
CPU缓存
友好 (连续内存易预读)
不友好 (非连续难预读)
数组
内存开销
小 (仅数据)
较大 (数据+指针,可能翻倍)
数组
内存碎片
频繁增删易产生碎片
数组

📍 选型指南:

  • 数组: 数据量已知或变化小、频繁随机访问、追求极致性能/内存效率、CPU缓存友好、多维数据直观。

  • 链表: 数据量未知或频繁变化、频繁在已知位置插入删除、需要动态扩容。Java的LinkedList
    (双向链表), LinkedHashMap
    (双向链表+哈希表)就是链表实力的体现!

四、揭秘开篇:链表实现LRU缓存淘汰

核心思路:维护一个按访问时间排序的有序链表 (越近访问的越靠头)

  1. 访问数据X

    • 缓存未满:直接将X新结点插入链表头部

    • 缓存已满

    • X在链表中 (缓存命中)

    • X不在链表中 (缓存未命中)

    1. 删除链表尾结点 (最久未访问)。

    2. 将X新结点插入链表头部

    1. 找到X对应的结点。

    2. 将它从当前位置删除

    3. 将X结点插入链表头部。(标记为最新访问)

  2. 优点:逻辑清晰,插入/删除头部尾部都是O(1)(双向链表优势尽显)。

  3. 缺点:判断X是否在缓存(查找)需要O(n)遍历链表。

  4. 优化方向 (预告):结合哈希表(Hash Table),记录数据位置,可将判断存在性及定位的操作降到O(1)!这就是更完美的LRU实现(后续讲哈希表细说)。

五、思考加油站

  1. 回文链表挑战:如何判断一个存储在单链表中的字符串是否是回文(正读反读一样,如"level")?

    • 你的思路? 能否在 O(n) 时间和 O(1) 额外空间 内搞定?(提示:快慢指针找中点、反转后半部分链表再比较)

    • 时空复杂度 你的方案是多少?

结语

链表,这列由指针串联起的“内存小火车”,以其非连续的存储、动态扩容的天赋,尤其在插入删除上的高效(O(1)),成为程序员手中对抗数据流动性的利器。理解了单链表的朴素、循环链表的巧妙、特别是双向链表“空间换时间”的智慧,你就能在数组与链表的抉择中游刃有余。下次面对缓存淘汰、频繁增删的场景,别忘了这列灵活的“小火车”!快用链表,让你的代码飞驰吧!🚄


关注微信公众号【让天下没有难学的编程】
一、求职、编程、职场任何问题都欢迎+V 
二、1对1答疑、制定学习计划、优化简历、offer建议、职业规划、模拟面试 
三、架构选型、技术方案 ,只要我有经验的事情都可以聊 
 希望通过我的经验能帮你有效提高收入,找到更好的工作。

文章转载自让天下没有难学的编程,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论