链表

  • 1.五花八门的链表结构
    • 1.1 底层的存储结构
    • 1.2 三种最常见的链表结构
      • 1.2.1 单链表
        • 1.2.1.1 单链表的查找、插入、删除
      • 1.2.2 循环链表
      • 1.2.3 双向链表
        • 1.2.3.1 双向链表删除操作
      • 1.2.4 双向循环链表
  • 2. 链表 VS 数组性能大比拼
  • 3. 如何基于链表实现 LRU 缓存淘汰算法?

1.五花八门的链表结构

相比数组,链表是一种稍微复杂一点的数据结构。这两个非常基础、非常常用的数据结构,我们常常会放到一块儿来比较。所以我们先来看,这两者有什么区别。

1.1 底层的存储结构

我们先从底层的存储结构上来看一看。

为了直观地对比,我画了一张图。从图中我们看到,数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。

而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。

1.2 三种最常见的链表结构

三种最常见的链表结构,它们分别是:单链表、双向链表和循环链表。

1.2.1 单链表

链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫作后继指针 next。


其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。

1.2.1.1 单链表的查找、插入、删除

与数组一样,链表也支持数据的查找、插入和删除操作。
我们知道,在进行数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是 O(n)。而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以,在链表中插入和删除一个数据是非常快速的。

从图中我们可以看出,针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)。

但是,有利就有弊。链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。

数组随机访问的时间复杂度为O(1),数组随机访问的效率特别特别高。 但是链表是不支持随机访问的。当我们想要访问链表中的第5个结点时。数组可以根据寻址公式,一下就定位到第5个元素所在的地址。而链表不行,要想访问链表中的第5个结点,我们只能从头节点开始遍历。顺序访问。一个一个往下找。所以链表"随机访问"的时间复杂度为O(n). 更为准确的来讲,链表的访问只能叫顺序访问,而不能叫随机访问。

你可以把链表想象成一个队伍,队伍中的每个人都只知道自己后面的人是谁,所以当我们希望知道排在第 k 位的人是谁的时候,我们就需要从第一个人开始,一个一个地往下数。所以,链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度。

1.2.2 循环链表

循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。

和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。尽管用单链表也可以实现,但是用循环链表实现的话,代码就会简洁很多。

1.2.3 双向链表

在实际的软件开发中,也更加常用的链表结构:双向链表。

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

图中可以看出来,双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。

从结构上来看,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。

找到链表中某个结点的前驱结点: 单链表的平均时间复杂度为O(n),因为需要从头节点开始找。 而双向链表的时间复杂度为O(1),因为这个结点中的prev前驱结点指针域存放的是前驱结点的地址,通过这个地址,我们一步就找到了该结点的前驱结点,所以时间复杂度O(1)

1.2.3.1 双向链表删除操作

在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:

  • 删除结点中“值等于某个给定值”的结点;
  • 删除给定指针指向的结点。

对于第一种情况,不管是单链表还是双向链表,为了查找到值等于给定值的结点,都需要从头结点开始一个一个依次遍历对比,直到找到值等于给定值的结点,然后再通过我前面讲的指针操作将其删除。

尽管单纯的删除操作时间复杂度是 O(1),但遍历查找的时间是主要的耗时点,对应的时间复杂度为 O(n)。根据时间复杂度分析中的加法法则,删除值等于给定值的结点对应的链表操作的总时间复杂度为 O(n)。

很多书中把链表的插入和删除操作的时间复杂度写为O(1),是没有考虑插入、删除操作之前查找元素的过程(时间复杂度O(n)),只考虑存粹的插入和删除操作时间复杂度自然是O(1)。

对于第二种情况,我们已经找到了要删除的结点,但是删除某个结点 q 需要知道其前驱结点,而单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表,直到 p->next=q,说明 p 是 q 的前驱结点。

但是对于双向链表来说,这种情况就比较有优势了。因为双向链表中的结点已经保存了前驱结点的指针,不需要像单链表那样遍历。所以,针对第二种情况,单链表删除操作需要 O(n) 的时间复杂度,而双向链表只需要在 O(1) 的时间复杂度内就搞定了!

删除给定指针指向的结点: 这种情况是已经找到了要删除的元素,我们只需要执行删除操作即可. 针对单链表而言: 单链表如果要删除一个结点q.必须要知道这个结点的前驱结点是谁,修改前驱结点的指针指向即可.单链表找某个结点的前驱结点,只能从头开始遍历. 临界值 p->next == q;说明p就是q的前驱结点.所以在单链表中,找这个前驱结点的平均时间复杂度为O(n),然后执行删除操作的时间复杂度为O(1).
根据时间复杂度分析的加法法则: 删除给定指针指向的结点 --> 单链表的总的时间复杂度为O(n).

针对双链表而言: 双链表要删除一个结点q.也必须得知道这个结点的前驱结点和后继结点. 修改前驱结点的后继指针next和后继结点的前驱指针prev即可.而针对双链表而言,找q的前驱结点和q的后继结点的时间复杂度都为O(1).而执行删除操作(修改指针指向)的时间复杂度也为O(1).
根据时间复杂度分析的加法法则: 删除给定指针指向的结点 --> 双链表的总的时间复杂度为O(1).

除了插入、删除操作有优势之外,对于一个有序链表,双向链表的按值查询的效率也要比单链表高一些。因为,我们可以记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据。

实际上,这里有一个更加重要的知识点需要掌握,那就是用空间换时间的设计思想。当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。

1.2.4 双向循环链表

了解了循环链表和双向链表,如果把这两种链表整合在一起就是一个新的版本:双向循环链表。

2. 链表 VS 数组性能大比拼

数组和链表是两种截然不同的内存组织方式。正是因为内存存储的区别,它们插入、删除、随机访问操作的时间复杂度正好相反。

不过,数组和链表的对比,并不能局限于时间复杂度。而且,在实际的软件开发中,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储数据。

3. 如何基于链表实现 LRU 缓存淘汰算法?

学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是 LRU 缓存淘汰算法。

缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?这就需要缓存淘汰策略来决定。常见的策略有三种:先进先出策略 FIFO(First In,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。

这些策略你不用死记,我打个比方你很容易就明白了。假如说,你买了很多本技术书,但有一天你发现,这些书太多了,太占书房空间了,你要做个大扫除,扔掉一些书籍。那这个时候,你会选择扔掉哪些书呢?对应一下,你的选择标准是不是和上面的三种策略神似呢?

我的思路是这样的:我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。

1). 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
2) 如果此数据没有在缓存链表中,又可以分为两种情况:

  • 如果此时缓存未满,则将此结点直接插入到链表的头部;
  • 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。

这样我们就用链表实现了一个 LRU 缓存。

现在我们来看下缓存访问的时间复杂度是多少。因为不管缓存有没有满,我们都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。

实际上,我们可以继续优化这个实现思路,比如引入散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到 O(1)。

02-链表结构(Linked list)相关推荐

  1. python中的列表是采用链式结构实现的_Python数据结构与算法之列表(链表,linked list)简单实现...

    Python数据结构与算法之列表(链表,linked list)简单实现 Python 中的 list 并不是我们传统(计算机科学)意义上的列表,这也是其 append 操作会比 insert 操作效 ...

  2. LinkedList底层链表结构

    LinkedList底层链表结构 前言概述 核心代码 效果展示 第一章 前言概述 第01节 简述说明 LinkedList 底层 LinkedList 底层采用的是 双向链表完成,特点是 增删快,查询 ...

  3. Python数据结构之链表(linked list)

    Python数据结构之链表 一.链表的基本知识 最近在leetcode刷题时遇到了几道关于链表的题,于是恶补了一下关于链表的知识.什么是链表?熟悉python语法的同学肯定都知道list,但是这并不是 ...

  4. java有链表吗_Java数据结构之链表(Linked List)

    1.链表(Linked List)介绍 链表是有序的列表,但是它在内存存储结构如下: 2.特点: 链表是以节点的方式来存储,是链式存储 每个节点包含 data 域, next 域:指向下一个节点. 链 ...

  5. 数据结构学习笔记(一):链表(linked list)

    目录 1 链表的概念和分类 2 链表对于数据的增删查操作(以单向链表为例) 2.1 增删查操作的描述 2.2 增删查操作的代码实现(JAVA) 1 链表的概念和分类 链表,又称线性表或线性链表,是若干 ...

  6. linux内核杂记(17)-内核链表结构(1)

    1.链表有单向链表和双向链表.单向链表只可从首结点访问到尾结点,每个节点只有后结点的链接:双向链表,每个节点有前后结点的链接,可从首到尾,也可从尾到首访问. 2.链表有环形链表和非环形链表.非环形链表 ...

  7. C语言合并链表merge linked list(附完整源码)

    实现合并链表merge linked list node结构体 合并链表merge linked list完整源码(定义,实现,main函数测试) node结构体 struct node {int d ...

  8. 牛客网 链表结构 算法相关内容

    链表结构 单链表的节点结构  由以下结构的节点依次连接起来所形成的链叫单链表结构 Clas Node<V>{ V value; Node next; } 双链表的节点结构 由以下结构的节点 ...

  9. 数据结构与算法详细解析之双链表结构解析

    一.双向链表结构分析及其特点: 双链表结构:双向链表中每一个Node节点含Data域及pre指针(指向上一个节点).Next指针(指向下一个节点): 特点: 1)双向链表可以双向查找(向前或者向后查找 ...

  10. 数据结构——链表(Linked List)

    数据结构--链表(Linked List) 目录 数据结构--链表(Linked List) 一.链表的概念 二.单向链表 2.1单链表节点的尾部添加 2.2单链表节点的自动排序添加 2.3单链表节点 ...

最新文章

  1. Ubuntu 16.04 install NVDIA display driver
  2. Cygwin,Nutch安装配置,检验是否正确(对网友守望者博客的修改---在此感谢守望者)1
  3. 推荐一个高质量的git命名查询和学习的github仓库git-recipes
  4. 解决若干WTL与VS2010的兼容问题(如error MSB6006: “cmd.exe”)
  5. Apple Watch问与答
  6. ios 性能优化之图形性能测试
  7. 通过反射认识泛型的本质
  8. 单片机应用案例大全-900套(保持更新)
  9. 基于sisotool极点配置PI参数及基于Plecs的三相电压源逆变器仿真
  10. 人人视频android app,人人视频安卓版
  11. sublime php code sniffer,Sublime插件CodeSniffer配置
  12. 基于JAVA优乐帮育儿系统计算机毕业设计源码+系统+数据库+lw文档+部署
  13. 几行烂代码,用错 Transactional,我赔了16万
  14. 数字后端——信号完整性分析
  15. Linux 安装rabbitMQ guest账号登录总是提示失败
  16. 消防车登高平台液压缸设计(设计说明书+CAD装配图、零件图、液压原理图)
  17. 手动在电脑桌面制作关机/重启图标并添加到任务栏
  18. D15-Acwing-3.2-4.1代码实现
  19. carsim自带4ws模型_carsim软件介绍
  20. 发动机冷却系统的控制论文综述

热门文章

  1. 史上最通俗易懂! 一文明白Git是干嘛的
  2. android蓝牙开发简单获取android设备已经连接过(已适配)的蓝牙设备信息
  3. 在本地部署自己的漏洞文章武器库(详细步骤说明)
  4. C语言实现int数组类型冒泡法升序排列
  5. oracle旬统计,oracle获取上一旬的开始时间和结束时间的函数
  6. C语言 人事管理系统练习
  7. 重来之大学版|学习篇-大学逃课问题,逃学威龙低配版
  8. java计算机毕业设计健身俱乐部管理系统源码+系统+mysql数据库+lw文档+部署
  9. 「实在RPA·制造业数字员工」助力制造业加「数」发展
  10. 保存图片到MySQL从MySQL读取图片