数据结构与算法第三讲 - 链表

2021-04-01  本文已影响0人  郑小鹿

本讲内容

链表定义和分类
链表和数组比较
链表操作
写链表代码的技巧
简单算法题

链表定义和分类

定义:通过指针把零散的内存空间串联起来存储数据
思想:空间换时间

对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化;而消耗过多内存的程序,可以通过消耗更多的时间(时间换空间)来降低内存的消耗。

分类:单链表,循环链表,双向链表,双向循环链表

单链表

循环链表是一种特殊的单链表,单链表尾指针指向null,循环链表尾指针指向头节点

循环链表 双向链表

单链表是单向的,每个节点只有一个指针,而双向链表每个节点有两个指针,所以占用的空间更大,但是它的操作相比于单链表更灵活高效。
接下来我们看下主要高效在什么地方?
删除操作:

第一种情况,不管是单链表还是双向链表,为了查找到值等于给定值的结点,都需要从头结点开始一个一个依次遍历对比,直到找到值等于给定值的结点,然后再通过我前面讲的指针操作将其删除。尽管单纯的删除操作时间复杂度是 O(1),但遍历查找的时间是主要的耗时点,对应的时间复杂度为 O(n)。根据时间复杂度分析中的加法法则,删除值等于给定值的结点对应的链表操作的总时间复杂度为 O(n)。
第二种情况,我们已经找到了要删除的结点,但是删除某个结点 q 需要知道其前驱结点,而单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表,直到 p->next=q,说明 p 是 q 的前驱结点。
对于单链表,需要从头遍历找到该节点的前向节点指针,然后指向删除操作,删除操作时间复杂度O(1),但是遍历节点的时间复杂度为 O(n),所以根据复杂度计算的加法原则,总时间复杂度为 O(n)
对于双向链表,找到的节点包含前向节点的指针,因此不需要再进行遍历,找到前向节点O(1),删除节点O(1),所以总时间复杂度为 O(1)

双向循环链表

链表和数组比较

分类 内存 优点 缺点
数组 连续内存,可以随机读取 结构简单,使用容易,可以利用CPU的缓存 1. 大小固定,为避免越界一般会超额申请,即使有容器类的数组,但是在扩容时,会先拷贝现有的数据,性能比较差
链表 零散内存,需要通过指针进行连接和取值 插入删除操作时间复杂度O(1),频繁的插入删除会导致内存碎片多,需要进行垃圾回收 占用空间多
image.png

链表操作

  1. 插入和删除


    单链表的插入和删除
  2. 查找

写链表代码技巧

技巧一:理解指针或引用的含义

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
示例:
p->next=q。这行代码是说,p 结点中的 next 指针存储了 q 结点的内存地址。
p->next=p->next->next。这行代码表示,p 结点的 next 指针存储了 p 结点的下下一个结点的内存地址。

技巧二:警惕指针丢失和内存泄漏

示例:
单链表的插入


单链表的插入

p->next = x; // 将p的next指针指向x结点;
x->next = p->next; // 将x的结点的next指针指向b结点;

以上代码会导致指针丢失,b节点后的节点无法访问

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

正确写法:

x->next = p->next; // 将x的结点的next指针指向b结点;
p->next = x; // 将p的next指针指向x结点;

删除链表结点时,也一定要记得手动释放内存空间,否则,也会出现内存泄漏的问题。当然,对于像 Java 这种虚拟机自动管理内存的编程语言来说,就不需要考虑这么多了。

注:什么是内存泄漏?
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory(内存溢出)!

技巧三:利用哨兵简化实现难度

x->next = p->next; // 将x的结点的next指针指向b结点;
p->next = x; // 将p的next指针指向x结点;

第一个节点的插入

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

单链表删除
其他节点删除

p->next = p->next->next;

最后一个节点删除

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

如果我们引入哨兵结点,在任何时候,不管链表是不是空,head 指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表

带头链表

哨兵结点是不存储数据的。因为哨兵结点一直存在,所以插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑了。

技巧四:重点留意边界条件处理

要实现没有 Bug 的链表代码,一定要在编写的过程中以及编写完成之后,检查边界条件是否考虑全面,以及代码在边界条件下是否能正确运行。

常需检查的边界情况:

技巧五:举例画图,辅助思考

技巧六:多写多练,没有捷径

简单算法题

如何实现LRU缓存淘汰算法?

上一篇 下一篇

猜你喜欢

热点阅读