链表的操作总结

链表反转
这是一个简单的链表操作问题,在leetcode上面有52.7%的通过率,难度是简单。但是还是想在这里基于python做一下总结,顺便总结一下链表的各种操作。
首先先看一下leetcode上面的题目:
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
看完了题目,很直白,就是转过来。我们尝试对这道题进行解决。这道题用python至少会有三种解决方案。
首先是链表的数据结构定义:

1.将链表遍历进list中,然后利用切片反转list,再将list填充到链表中即可。这是最简单的一种思考逻辑,但是也比较消耗性能。时间和空间复杂度都为O(n)。

2.另一种迭代算法,是一种纯粹交换的迭代,笔者这里截取了leetcode速度最快的一种。

这一波交换操作,我们可以画个示意图就知道他的交换是一种怎么样的交换。

从图中可以看出,循环的作用就是将反向指针进行保存。同时令将指针转向的功能。
3.最后一种方案是采用递归的方式进行链表反转。这种方式也需要一定的理解。我们先展示一下代码。

这种解法其实理解起来只有两部分内容,传递反向指针和进行指针反向拼接。我们先来理解一下指针反向拼接这个操作。

如此循环即可将链表反转过来。但是还有个关键就是将最后一个指针传递出来。我们可以看到之前的代码中,end传出来后是一直没有做任何操作的。不停的return出最后一个指针。所以就将最后一个指针传递了出来。
以上就是链表反转的3中方法。除此之外还想写一些链表的简单操作。

快慢指针
何为快慢指针,即对链表进行两个不同步频的指针标记遍历。最经典的是慢指针走一步,快指针走两步。
快慢指针有很多的应用,比如说:
1.判断一个链表是否存在环。

两个指针并排走,如果有环的话快指针最终会指向慢指针的位置。没有环的话,快指针会指向None后退出。
当然这道题的解法不止这一样,还可以利用set进行判断。
2.输出链表中的倒数第K个节点
这道题利用快慢指针的思路是这样的。定义两个指针,第一个指针向前走k-1步;第2个指针保持不动;到第k步时第2个指针也开始移动。由于两个指针始终保持着k-1的距离,所以当快指针到达末尾时,慢指针刚好指向倒数第k个。

链表的技巧总结

技巧一 :理解指针或者引用的含义
看懂链表结构不是很难,但是一旦把它和指针混在一起,就很容易让人摸不着头脑,(我再写代码的时候就出现了这种情况),所以要想写对链表代码,首先要理解好指针。
实际上,对于指针的理解,你只需要记住下面这句话就可以了:将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。

在写链表代码的时候,经常会找不到指针(引用)指到了哪儿,会弄丢了指针。
例如在单向链表中插入节点x,前节点是p,后节点是b。
p.next = x ;
x.next = p.next;
这样就会导致指针丢失和内存泄露。如果把两行代码的顺序颠倒一下,就不会丢失指针,要先将x.next指向b,然后,在将p的next节点指向x,这样才不会丢失指针导致内存泄露。

哨兵,解决的是国家之间的边界问题。同理,这里说的哨兵也是解决"边界问题"的,不直接参与业务逻辑。(还不是很了解,之后再补充)。
技巧四:重点留意边界条件处理
软件开发中,代码在处理一些边界问题或者异常情况中,容易出现bug。链表代码也是容易产生bug,要想实现没有bug的链表代码,一定要在编写的过程中以及在编写完之后,检查边界条件是否考虑的全面,以及代码在边界条件下是否能正常运行。
经常用来检查链表代码是否正确的边界条件有如下几个:
- 如果链表为空,代码是否能正常运行?
- 如果链表只包含一个节点时,代码是否正常运行?
- 如果链表只包含两个节点时,代码是否正常运行?
- 代码在处理头节点和尾节点时,代码是否正常运行?
我们在写代码的时候也不要只是实现业务逻辑就完事,也要多考虑会遇到哪些边界问题或者异常情况,遇到了应该如何解决,这样写出来的代码才会健壮。

技巧四、留意边界的处理
比如链表为空、比如只包含一个结点或两个节点的情况。比如处理头结点和尾节点时,代码是否正确。
再就是多写多练了,这里给出java语言实现的链表代码。






举例画图,这个太有用了,我再学习算法的过程中,有时候看不懂实现代码的时候,就想着如何通过画图来分解代码的实现步骤,感谢在学习中,帮助过我的朋友,让我能够理解实现的过程。
在这里继续用上述的有序链表的合并的代码:

画图辅助思考:
我们可以看到这个代码里就体现了边界问题的几个步骤:
1、首先是链表为空时的处理。
2、链表头的处理。
3、链表多节点的处理。
4,链表尾的处理。
而图中的举例也正是对代码实现的分解的很好的解释。(画图辅助真的很棒)

就是把常见的链表操作都自己多写几遍。最开始我都是遇到了各种各样的不理解,甚至对于链表的操作都有些迷糊,但是多出问题多调试,慢慢的我们也能孰能生巧。唯手熟尔!
常见的链表操作,只要能熟练的写出来,不熟就多写几遍,保证之后就不会再害怕写链表代码。
- 单链表反转
- 检测链表中的环
- 两个有序链表的合并
- 删除链表中倒数第K个节点
- 球链表的中间节点
写出正确链表代码的六个技巧。分别是理解指针或引用的含义、警惕指针的丢失和内存泄露,利用哨兵简化实现难度,重点留意边界条件处理,以及举例画图,辅助思考,还有就是多写多练,唯手熟尔。
勤能补拙,生活就是养成游戏,勤练内功,即使当前不能花里胡哨,未来也会强壮到无人能敌!
链表的经典技巧及算法


另一个更灵巧的方法是,用两个指针,慢指针每次走一步,快指针每次走两步,当快指针走到链表的末端(NULL)时,慢指针正好指向了中间节点,代码如下:





如果允许使用额外的内存,可以有更简单的做法,即一边遍历,一边将节点放在map中,当某个节点第二次出现在map中时,它就是入口节点,代码如下:





如果被删除节点是尾节点,上面的代码就无法将target上一个节点的next置为NULL,所以只有给了头节点后,才能遍历到target的上一个节点,并把其next置为NULL。 

上面代码的空间复杂度为O(N),其实还有空间复杂度为O(1)的算法,也很灵巧,运用了之前提到的一些技巧,代码如下:

这个方法的缺点是修改了原链表,但是综合运用了链表的很多技巧,值得收藏。


其实如果不要求空间复杂度为O(1),可以用递归的思想,代码更简略,如下:







这个算法的空间复杂度为O(N/K),即正比于递归深度,如果要求空间复杂度为O(1)呢?其实也比较简单,只要循环处理每一段长度为K的链表,处理的时候注意保存上一段链表的尾节点,代码如下:








技巧:哑变量的引入使得头结点不再具有特殊性,从而简化处理流程。 




























