链表

主要学习几个写链表代码的技巧

理解指针或引用的含义

有些语言有指针概念,比如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
-------------完-------------