数据结构:链表

1.什么是链表?

    • 和数组一样,链表也是一种线性表.

    • 从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。

    • 链表中的每一个内存块被称为节点 Node.节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针 next.

 

2.为什么使用链表?

    • 插入,删除数据效率高0(1)级別(只需更改指针指向即可),随机访问效率低0(0)级别(需要从链头至链尾进行遍历).

    • 和数组相比,内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针.

 

3.常用链表

    单链表、循环链表和双向链表

3.1 单链表

    • 每个节点只包含一个指针,即后继指针.

    • 单链表有两个特殊的节点,即首节点和尾节点.为什么特殊?用首节点地址表示整条链表,尾节点的后继指针指向空地址null.

    • 性能特点:插入和删除节点的时间复杂度为0(1),查找的时间复杂度为0(1).

3.2 循环链表

    • 除了尾节点的后继指针指向首节点的地址外均与单链表一致.

    • 适用于存储有循环特点的数据,比如约瑟夫问

3.3 双向链表

    • 节点除了存储数据外,还有两个指针分别指向前ー个节点地址(前驱指针 prev)和下一个节点地址(后继指针 next).

    • 首节点的前驱指针 prev和尾节点的后继指针均指向空地址.

    • 性能特点:

    和单链表相比,存储相同的数据,需要消耗更多的存储空间.

    插入、删除操作比单链表效率更高0(1)级別.以删除操作为例,删除操作分为2种情况:给定数据值删除对应节点和给定节点地址删除节点.对于前一种情况,单链表和双向链表都需要从头到尾进行遍历从而找到对应节点进行删除,时间复杂度为0(n).对于第二种情况,要进行删除操作必须找到前驱节点,单链表需要从头到尾进行遍历直刀p->next=q,时间复杂度为0(),而双向链表可以直接找到前驱节点,时间复杂度为0(1).对于ー个有序链表,双向链表的按值查询效率要比单链表高一些,因为我们可以记录上次查找的位置p,每一次查询时,根据要查找的值与p的大小关系,決定是往前还是往后査找,所以平均只需要查找一半的数据.

    • 双向循环链表:首节点的前驱指针指向尾节点,尾节点的后继指针指向首节点.

 

4.选择数组还是链表?

4.1 插入,删除和随机访问的时间复杂度

    数组:插入,删除的时间复杂度是o(n),随机访问的时间复杂度是0(1).

    链表:插入,删除的时间复杂度是0(n),随机访问的时间复杂端是0(1).

4.2 数组缺点

    • 若申请内存空问很大,比如100M,但若内存空间没有100M的连续空间时,则会申请失败,尽管内存可用空间超过100M.

    • 大小固定,若内存空间不足,需进行扩客,一旦扩容就要进行数据复制,而这时非常费时的.

4.3 链表缺点

    • 内存空间消耗更大,因为需要额外的空间存储指针信息.

    • 对链表进行频繁的插入和删除操作,会导致频繁的内存申请和释放,容易造成内存碎片,如果是Java 语言, 还可能会造成频繁的GC(自动垃圾回收器)操作.

4.4 如何迭择?

    数组简单易用,在实现上使用连续的内存空间,可以借助CPU的缓冲机制预读数组中的数据,所以访问效卒更高,而链表在内存中并不是连续存储,所以对 CPU缓存不友好,没办法预读.

    如果代码对内存的使用非常苛刻,那数组就更适合.

 

5.如何分别用链表和数组实现 LRU缓冲淘汰策略?

5.1 什么是缓存?

    缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非广泛的应用,比如常见的CPU缓存、数据库缓存、浏览器缓存等等.

5.2 为什么使用缓存?

    缓存的大小是有限的,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?就需要用到缓存淘汰策略.

5.3 什么是缓存淘汰策略?

    指的是当缓存被用满时清理数据的优先顺序.

5.4 有哪些缓存淘汰策略?

    常见的3种包括先进先出策略 FIFO(First In,First Out)、最少使用策略LFU(Least Frenquently Used)、 最近最少使用策略LRU(Least Recently Used).

5.5 链表实现LRU缓存淘汰策略

    当访问的数据没有存储在缓存的链表中时,直接将数据插入链表表头,时间复杂度为0(1);当访问的数据存在于存储的链表中时,将该数据对应的节点,插入到链表表头,时间复杂度为0(n).如果缓存被占满,则从链表尾部的数据开始清理,时间复杂度为0(1).

5.6 数组实现LRU缓存淘汰策

方式ー:首位置保存最新访问数据,末尾位置优先清理

    当访问的数据未存在于缓存的数组中时,直接将数据插入数组第一个元素位置,此时数组所有元素需要向后移动1个位置,时间复杂度为00);当访问的数据存在于缓存的数组中时,查找到数据并将其插入数组的第一个位置,此时亦需移动数组元素,时间复杂度为O(n).缓存用满时,则清理掉未尾的数据,时间复杂度为0(1).

方式二:首位置优先清理,未尾位置保存最新访问数据

    当访问的数据未存在于缓存的数组中时,直接将数据添加进数组作为当前最有一个元素时间复杂度为0(1);当访问的数据存在于缓存的数组中时,查找到数据并将其插入当前数组最后一个元素的位置,此时亦需移动数组元素, 时间复杂度为0(n)。缓存用满时,则清理掉数组首位置的元素,且剩余数组元素需整体前移一位,时间复杂度为0(n).(优化:清理的时候可以考虑一次性清理一定数量,从而降低清理次数,提高性能。)

 

6.设计思想

    时空替换思想:"用空间换时间”与"用时间换空间"

    当内存空间充足的时侯,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高,时间复杂度小相对较低的算法和数据结构,缓存就是空间换时间的例子,如果内存比较紧缺,比如代码跑在手机或者单片机上,这时,就要反过来用时间换空间的思路.

 

7.如何优雅的写出链表代码?

7.1 理解指针或引用的含义

    含义:将某个变量(对象)赋值给指针(引用),实际上就是就是将这个变量(对象)的地址赈值给指計(引用).

7.2 警惕指针丢失和内存泄漏(单链表)

7.2.1 插入节点

    在节点a和节点b之间插入节点x,b是a的下一节点,,p指针指向节点a,则造成指针丢失和内存泄漏的代码:p->next=xX->next=p->next,显然这会导致x节点的后继指针指向自身。

        正确的写法是2句代码交换顺序,即:x->next=D->next,p->next=x

7.2.2 删除节点

        在节点a和节点b之间删除节点b,b是a的下一节点,P指针指向节点a:D->next=p->next->next,

7.3 利用"哨兵'简化实现难度

7.3.1 什么是哨兵?

    链表中的"哨兵”节点是解决边界问题的,不参与业务逻辑.如果我们引人"哨兵'节点,则不管链表是否为空,head指针都会指向这个"哨兵节点.我们把这种有"哨兵节点的链表称为带头链表,相反,没有哨兵”节点的链表就称为不带头链表.

7.3.2 未引入哨兵的情况

    如果在p节点后插入一个节点,只需2行代码即可編定:

            new_node->next=p->next,

            D->next=new_node;

    但,若向空链表中插入一个节点,则代码如下:

        if(head== nul)

        head=new_node;

    如果要删除节点p的后继节点,只需1行代码即可搞定:

        p->next=p->next->next,

    但,若是删除链表的最有一个节点(链表中只剩下这个节点),则代码如下:

        if(head->next==null)

        head=null;

    从上面的情况可以看出,针对链表的插入、删除操作,需要对插入第一个节点和删除最后一个节点的情況进行特殊处理.这祥代码就会显得很繁琐,所以引入“哨兵”节点来解决这个间题。

7.3.3 引人哨兵的情况

    "哨兵"节点不存储数据,无论链表是西为空,head指针都会指向它,作为链表的头结点始终存在.这样,插入第一个节点和插入其化村点,湖除長后ー个节点和删除其他节点都可以统一为相同的代码实现逻辑了·

7.3.4 哨兵还有那些应用场景?

    这个知识有限,暂时想不出来!但总结起来,哨兵最大的作用就是简化边界条件的处理.

7.4 重点留意边界条件处理

    经常用来检查链表是否正确的边界4个边界条件

    • 如果链表为空时,代码是否能正常工作?

    • 如果链表只包含一个节点时,代码是否能正常工作

    • 如果链表只包合两个节点时,代码是西能正常工作?

    • 代码逻辑在处理头尾节点时是否能正常工作?

7.5 举刷画图,辅助思考

    核心思想:释放脑容量,留更多的给逻辑思考,这样就会感觉思路清晰很多。

7.6 多写多练,没有捷径

    5个常见的链表操作:

    • 单链表反转

    • 链表中环的检测

    • 两个有序链表合井

    • 删除链表倒数第n个节点

    • 求链表的中间节点

 

6.代码示例:https://github.com/luomingkui/dataalgo

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员学习圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值