链表

2019-12-01  本文已影响0人  蹩脚的小三

一、什么是链表?

  1. 和数组一样,链表也是一种线性表。
  2. 从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。
  3. 链表中的每一个内存块被称为节点Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针next。

二、为什么使用链表?即链表的特点

  1. 插入、删除数据效率高O(1)级别(只需更改指针指向即可),随机访问效率低O(n)级别(需要从链头至链尾进行遍历)。
  2. 和数组相比,内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针。

三、常用链表:单链表、循环链表和双向链表

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


    image.png

四、选择数组还是链表?

  1. 插入、删除和随机访问的时间复杂度
    数组:插入、删除的时间复杂度是O(n),随机访问的时间复杂度是O(1)。
    链表:插入、删除的时间复杂度是O(1),随机访问的时间复杂端是O(n)。
  2. 数组缺点
  1. 链表缺点
  1. 如何选择?
    数组简单易用,在实现上使用连续的内存空间,可以借助CPU的缓冲机制预读数组中的数据,所以访问效率更高,而链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法预读。
    如果代码对内存的使用非常苛刻,那数组就更适合。

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

五、应用

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

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

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

  1. 如何通过单链表实现“判断某个字符串是否为水仙花字符串”?(回文字符串:比如 上海自来水来自海上)

六、设计思想

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

如何优雅的写出链表代码

一、理解指针或引用的含义

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

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

  1. 插入节点
    在节点a和节点b之间插入节点x,b是a的下一节点,,p指针指向节点a


    image.png

    造成指针丢失和内存泄漏的代码:

p—>next = x;
 x—>next = p—>next;

显然这会导致x节点的后继指针指向自身。
正确的写法是2句代码交换顺序,即:

x—>next = p—>next; 
p—>next = x;

插入结点时,一定要注意操作的顺序

  1. 删除节点
    在节点a和节点b之间删除节点b,b是a的下一节点,p指针指向节点a:p—>next = p—>next—>next;
    删除链表结点时,也一定要记得手动释放内存空间

三、利用“哨兵”简化实现难度

  1. 什么是“哨兵”?
    链表中的“哨兵”节点是解决边界问题的,不参与业务逻辑。如果我们引入“哨兵”节点,则不管链表是否为空,head指针都会指向这个“哨兵”节点。我们把这种有“哨兵”节点的链表称为带头链表,相反,没有“哨兵”节点的链表就称为不带头链表。
  2. 未引入“哨兵”的情况
    如果在p节点后插入一个新节点,只需2行代码即可搞定:
new_node—>next = p—>next;
p—>next = new_node;

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

if(head == null){
    head = new_node;
}

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

p—>next = p—>next—>next;

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

if(head—>next == null){
head = null;
}

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

  1. 引入“哨兵”的情况
    “哨兵”节点不存储数据,无论链表是否为空,head指针都会指向它,作为链表的头结点始终存在。这样,插入第一个节点和插入其他节点,删除最后一个节点和删除其他节点都可以统一为相同的代码实现逻辑了。


    image.png
  2. “哨兵”还有哪些应用场景?
    比如插入排序、归并排序、动态规划等。

四、重点留意边界条件处理

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

  1. 如果链表为空时,代码是否能正常工作?
  2. 如果链表只包含一个节点时,代码是否能正常工作?
  3. 如果链表只包含两个节点时,代码是否能正常工作?
  4. 代码逻辑在处理头尾节点时是否能正常工作?

五、举例画图,辅助思考

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

链表练习题:

  1. 链表的种类以及他们的特点、优势、应用场景
  2. 链表和数组的比较
  3. 链表的插入、删除等操作;以及加入哨兵节点之后的插入、删除等操作
  4. 回文字符串
    https://github.com/andavid/leetcode-java/blob/master/note/234/README.md
  5. LRU缓存实现
  6. 单链表反转(leetCode:206)
  7. 链表中环的检测(leetCode:141)
  8. 两个有序链表合并(leetCode:21)
  9. 删除链表倒数第n个节点(leetCode:19)
  10. 求链表的中间节点(leetCode:876)
  11. 约瑟夫环问题
  12. 单链表的归并排序
上一篇 下一篇

猜你喜欢

热点阅读