主要学习几个写链表代码的技巧
理解指针或引用的含义
有些语言有指针
概念,比如C语言;有些语言没有指针,取而代之的是引用
,比如Java、Python。不管“指针”还是“引用”,都是一个意思,指存储所指对象的内存地址
。
指针含义:将某个变量(对象)赋值给指针(引用),实际上就是就是将这个变量(对象)的地址赋值给指针(引用)
示例:
p—>next = q;
表示p
节点的后继next
指针存储了q
节点的内存地址
p—>next = p—>next—>next;
表示p
节点的后继next
指针存储了p
节点的下下个节点的内存地址。
警惕指针丢失和内存泄漏
示例:
单链表的插入,希望在节点a和相邻节点b之间插入节点x,假设当前指针p指向节点a,则造成指针丢失和内存泄漏的代码:
p->next = x;
x->next = p->next
导致将x
自身赋值给了x->next
,自己指向自己。
对于有些语言来说,比如 C 语言,内存管理是由程序员负责的,如果没有手动释放结点对应的内存空间,就会产生内存泄露。
所以,插入结点时,一定要注意操作的顺序。上面代码的正确写法是:两句代码顺序调换。
同理,删除链表时,也一定要手动释放内存空间,否则,也会出现内存泄漏问题。
Python语言不需手动释放,它的解释器的存储管理系统会自动回收不用的存储。
利用哨兵(头结点)简化实现难度
哨兵
含义:
链表中的哨兵
节点是解决边界问题的,不参与业务逻辑。如果我们引入哨兵
节点,则不管链表是否为空,head指针都会指向这个“哨兵”节点。我们把这种有“哨兵”节点的链表称为带头链表
,相反,没有哨兵
节点的链表就称为不带头链表。
示例:
不带头结点时:
- 对于单链表的插入操作
1 . 如果在p节点后插入一个新节点,只需2行代码即可搞定:
new_node—>next = p—>next;
p—>next = new_node;
2 . 如果向空链表中插入一个新结点,则代码就不同:
if(head == null){
head = new_node;
}
- 对于单链表的删除操作
1 . 如果要删除节点p的后继节点,只需1行代码即可搞定:
p—>next = p—>next—>next;
2 . 如果删除的是链表的最后一个节点(链表中只剩下这个节点),则代码如下:
if(head—>next == null){
head = null;
}
以上示例可以看出,不带头结点时,单链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点作特殊处理。
带头结点时:
哨兵
节点不存储数据,无论链表是否为空,head
指针都会指向它,作为链表的头结点始终存在。
这样,插入第一个节点和插入其他节点,删除最后一个节点和删除其他节点都可以统一为相同的实现逻辑。
留意边界条件处理
常用的检查链表代码是否正确的边界条件:
1 . 如果链表为空时,代码是否能正常工作?
2 . 如果链表只包含一个节点时,代码是否能正常工作?
3 . 如果链表只包含两个节点时,代码是否能正常工作?
4 . 代码逻辑在处理头尾节点时是否能正常工作?
举例画图,辅助思考
对于稍微复杂的链表操作,可以找一个具体例子,将它画在纸上。比如,向单链表中插入一个数据,就画出链表插入前和插入后的情况。
对着图写代码,写完之后,也可举列子,照着代码走一遍,很容易发现代码中的Bug。
多写多练,没有捷径
精选的5个常见链表操作:(以及在LeetCode上对于的题目编号)
1.单链表反转 ---- 206
2.链表中环的检测 ---- 141
3.两个有序链表合并 ---- 21
4.删除链表倒数第n个节点 ---- 19
5.求链表的中间节点 ---- 876