本文部分文字图片引用了极客时间的《数据结构与算法之美》链表篇 https://time.geekbang.org/column/article/41013
讲解的很不错的课程,如果有需要可以去订阅。
链表介绍
链表通过指针将一组零散的内存块串联在一起。
内存块称为链表的“结点”。
为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。
如图所示,我们把这个记录下个结点地址的指针叫作后继指针 next。
习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。
尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。
数组与链表
数组需要一块连续的内存空间来存储。
而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。
分类
单链表、双向链表和循环链表
插入和删除操作
链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)。
链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度。
关于删除额外要说的
尽管单纯的删除操作时间复杂度是 O(1),但遍历查找的时间是主要的耗时点,对应的时间复杂度为 O(n)。
单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表。
删除一个数据无外乎这两种情况:
- 删除结点中“值等于某个给定值”的结点;
- 删除给定指针指向的结点。
第一种情况,不管是单链表还是双向链表,都需要从头结点开始一个一个依次遍历对比,时间复杂度为 O(n)。
第二种情况,单链表还是要遍历来获取前驱节点,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点。
单链表删除操作需要 O(n) 的时间复杂度,而双向链表只需要在 O(1) 的时间复杂度内就搞定了。
使用技巧
理解指针或引用的含义
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
p->next=q。这行代码是说,p 结点中的 next 指针存储了 q 结点的内存地址。
警惕指针丢失和内存泄漏
插入结点时,一定要注意操作的顺序。
删除链表结点时,也一定要记得手动释放内存空间。
利用哨兵简化实现难度
针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。
// 插入
new_node->next = p->next;
p->next = new_node;
// 插入(头部)
if (head == null) {
head = new_node;
}
// 删除
p->next = p->next->next;
// 删除(头部)
if (head->next == null) {
head = null;
}
重点留意边界条件处理
- 如果链表为空时,代码是否能正常工作?
- 如果链表只包含一个结点时,代码是否能正常工作?
- 如果链表只包含两个结点时,代码是否能正常工作?
- 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
举例画图,辅助思考
练习
如果想做链表的相关练习,以便更熟悉链表结构,可以尝试下面的代码实现操作。
- 单链表反转
- 链表中环的检测
- 两个有序的链表合并
- 删除链表倒数第 n 个结点
- 求链表的中间结点
这些问题的代码可以参考:https://github.com/wangzheng0822/algo/blob/master/python/07_linkedlist/linked_list_algo.py
另外我还对部分问题进行了代码的解读,请参考:
链表中环的检测,求单链表的中间结点
删除链表倒数第n个结点
Python代码实现链表,请看下篇:【链表】链表的介绍与python实现 下篇