技巧一:理解指针或是引用的含义
不管是“指针”还是“引用”,实际上都是一个意思,表示的是存储所指对象的内存地址。将某个变量赋值给指针,实际就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了变量的内存地址,指向了这个变量,通过指针就可以找到这个变量。
技巧二:警惕指针丢失和内存泄漏
如图,我们希望在a结点和相邻的结点b之间插入结点x,假设当前指针p指向结点a。如果我们的代码是下面这样,就会造成指针丢失和内存泄漏。
p->next = x;//将p的next指针指向x
x->next = p->next; //将x的结点的next指针指向b结点
p的next指针在完成第一步操作以后,就不再指向结点b了,而是指向结点x。第2步相当于是将x赋值给x->next,自己指向自己。因此整个链表就相当于段成了两半,从结点b以后的结点就再也无法访问到了。
纠正方法:将两行代码调换顺序。
要先将结点x的next指针指向结点b,再把结点a的next指向x,这样才不会丢失指针,导致内存泄漏。
技巧三:利用哨兵简化实现难度
插入操作:
在结点p后面再插入一个新的结点,只需要下面两行代码就可以搞定:
new_node->next = p->next;
p->next = new_node;
但是如果是向一个空链表中插入第一个结点,刚刚的逻辑就不能用。我们需要进行下面这样的特殊处理,其中head表示链表的头结点。
if (head == null){
head = new_node
}
删除操作:
如果我们要删除结点p的后继结点,只需要一行代码搞定。
p->next = p->next->next;
但如果要删除链表中的最后一个结点,前面的删除代码就不work了。跟插入类似,对于这种情况,我们也需要做特殊的处理。
if (head->next == null){
head = null
}
从上面的一步步分析,我们可以发现,针对链表的插入、删除操作,需要对插入的第一个结点和删除的最后一个结点的情况进行特殊的处理。这样代码实现起来不简洁、容易因考虑不周而出错。所以为了解决这个问题,哨兵就登场了。哨兵是解决“边界问题”的,不直接参与业务逻辑。head = null 表示链表中没有结点了。其中head表示头结点指针,指向链表中的第一个结点。如果引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫做带头链表。相反,没有哨兵结点的链表就叫作不带头链表。
哨兵结点不存储数据。因为哨兵结点一直存在,所以插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑了。
技巧四:重点留意边界条件处理
在软件开发中,代码在一些边界或是异常情况下,最容易出现bug。链表也不例外。为了实现没有bug的链表代码,一定要在编程的过程中以及编写完成之后,检查边界条件是否考虑全面,以及代码在边界条件下是否能够正确运行。
经常用来检查链表代码是否正确的边界条件:
——如果链表为空,代码是否正常工作?
——如果链表只包含一个结点时,代码是否可以正常工作?
——如果链表只包含两个结点时,代码是否能正常工作?
—— 代码逻辑在处理头结点和尾结点的时候,能否正常工作?
技巧五:举例画图,辅助思考
当觉得链表代码太复杂,脑容量不够用的时候,可以采用举例法和画图法,用以辅助思考。
技巧六:多写多练,没有捷径
精选的5个常见的链表操作:
—— 单链表反转
——链表中环的检测
——两个有序链表的合并
——删除链表倒数第n个结点
——求链表的中间结点
Leetcode对应编号:206,141,21,19,876