python实现链表的删除_干货||链表的技巧和算法总结

5cfaeba7-3414-eb11-8da9-e4434bdf6706.png

5dfaeba7-3414-eb11-8da9-e4434bdf6706.png

链表的操作总结

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  链表反转

这是一个简单的链表操作问题,在leetcode上面有52.7%的通过率,难度是简单。但是还是想在这里基于python做一下总结,顺便总结一下链表的各种操作。

首先先看一下leetcode上面的题目:

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

看完了题目,很直白,就是转过来。我们尝试对这道题进行解决。这道题用python至少会有三种解决方案。

首先是链表的数据结构定义:

5ffaeba7-3414-eb11-8da9-e4434bdf6706.png

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

60faeba7-3414-eb11-8da9-e4434bdf6706.png

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

61faeba7-3414-eb11-8da9-e4434bdf6706.png

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

62faeba7-3414-eb11-8da9-e4434bdf6706.jpeg

从图中可以看出,循环的作用就是将反向指针进行保存。同时令将指针转向的功能。

3.最后一种方案是采用递归的方式进行链表反转。这种方式也需要一定的理解。我们先展示一下代码。

63faeba7-3414-eb11-8da9-e4434bdf6706.png

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

64faeba7-3414-eb11-8da9-e4434bdf6706.png

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

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  快慢指针

何为快慢指针,即对链表进行两个不同步频的指针标记遍历。最经典的是慢指针走一步,快指针走两步。

快慢指针有很多的应用,比如说:

1.判断一个链表是否存在环。

66faeba7-3414-eb11-8da9-e4434bdf6706.png

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

67faeba7-3414-eb11-8da9-e4434bdf6706.png

5dfaeba7-3414-eb11-8da9-e4434bdf6706.png

链表的技巧总结

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  技巧一 :理解指针或者引用的含义


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

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  技巧二:警惕指针丢失和内存泄露


在写链表代码的时候,经常会找不到指针(引用)指到了哪儿,会弄丢了指针。
例如在单向链表中插入节点x,前节点是p,后节点是b。

p.next = x ;
x.next = p.next;

这样就会导致指针丢失和内存泄露。如果把两行代码的顺序颠倒一下,就不会丢失指针,要先将x.next指向b,然后,在将p的next节点指向x,这样才不会丢失指针导致内存泄露。

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  技巧三:利用哨兵简化实现的难度


哨兵,解决的是国家之间的边界问题。同理,这里说的哨兵也是解决"边界问题"的,不直接参与业务逻辑。(还不是很了解,之后再补充)。
技巧四:重点留意边界条件处理
软件开发中,代码在处理一些边界问题或者异常情况中,容易出现bug。链表代码也是容易产生bug,要想实现没有bug的链表代码,一定要在编写的过程中以及在编写完之后,检查边界条件是否考虑的全面,以及代码在边界条件下是否能正常运行。
经常用来检查链表代码是否正确的边界条件有如下几个:

  • 如果链表为空,代码是否能正常运行?

  • 如果链表只包含一个节点时,代码是否正常运行?

  • 如果链表只包含两个节点时,代码是否正常运行?

  • 代码在处理头节点和尾节点时,代码是否正常运行?


我们在写代码的时候也不要只是实现业务逻辑就完事,也要多考虑会遇到哪些边界问题或者异常情况,遇到了应该如何解决,这样写出来的代码才会健壮。

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  技巧四、留意边界的处理

比如链表为空、比如只包含一个结点或两个节点的情况。比如处理头结点和尾节点时,代码是否正确。

再就是多写多练了,这里给出java语言实现的链表代码。

70faeba7-3414-eb11-8da9-e4434bdf6706.png

72faeba7-3414-eb11-8da9-e4434bdf6706.png

74faeba7-3414-eb11-8da9-e4434bdf6706.png

75faeba7-3414-eb11-8da9-e4434bdf6706.png

76faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  技巧五:举例画图,辅助思考


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

7dfaeba7-3414-eb11-8da9-e4434bdf6706.png

画图辅助思考:
我们可以看到这个代码里就体现了边界问题的几个步骤:

1、首先是链表为空时的处理。

2、链表头的处理。

3、链表多节点的处理。

4,链表尾的处理。

而图中的举例也正是对代码实现的分解的很好的解释。(画图辅助真的很棒) 

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  技巧六:多谢多练,没有捷径


就是把常见的链表操作都自己多写几遍。最开始我都是遇到了各种各样的不理解,甚至对于链表的操作都有些迷糊,但是多出问题多调试,慢慢的我们也能孰能生巧。唯手熟尔!
常见的链表操作,只要能熟练的写出来,不熟就多写几遍,保证之后就不会再害怕写链表代码。

  • 单链表反转

  • 检测链表中的环

  • 两个有序链表的合并

  • 删除链表中倒数第K个节点

  • 球链表的中间节点

写出正确链表代码的六个技巧。分别是理解指针或引用的含义、警惕指针的丢失和内存泄露,利用哨兵简化实现难度,重点留意边界条件处理,以及举例画图,辅助思考,还有就是多写多练,唯手熟尔。
勤能补拙,生活就是养成游戏,勤练内功,即使当前不能花里胡哨,未来也会强壮到无人能敌!

5dfaeba7-3414-eb11-8da9-e4434bdf6706.png

链表的经典技巧及算法

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  1、寻找链表的中间节点:最简单的方法是,先遍历一遍链表,计算出链表的长度,然后计算出中间节点的位置,然后再遍历一遍,边遍历边计算,直到找到中间节点,这个方法略显啰嗦,最坏的情况需要遍历2次链表,代码如下:    

83faeba7-3414-eb11-8da9-e4434bdf6706.png

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

85faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  2、检测链表是否有环:经典的做法也是用快慢指针,如果没有环,快指针一定先到达链表的末端(NULL),如果有环,快、慢指针一定会相遇在环中,代码如下:

88faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  3、检测环的入口:经典的做法是先检测是否有环,如果有环,则计算出环的长度,然后使用前后指针(不是快慢指针),所谓的前后指针就是一个指针先出发,走了若干步以后,第二个指针也出发,然后两个指针一起走,当前后指针相遇时,它们正好指向了环的入口,代码如下:

8bfaeba7-3414-eb11-8da9-e4434bdf6706.png

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

8cfaeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  4、链表翻转:假设原链表为1->2->3,翻转以后的链表应该是1

8efaeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  5、删除链表中的节点,注意这里只给出要删除的那个节点,不给出链表头(假设被删除节点不是尾节点),代码如下:

90faeba7-3414-eb11-8da9-e4434bdf6706.png

如果被删除节点是尾节点,上面的代码就无法将target上一个节点的next置为NULL,所以只有给了头节点后,才能遍历到target的上一个节点,并把其next置为NULL。5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  6、回文链表的检测:所谓回文链表,即链表元素关于中间对称,,如1->2->3->2->1,比较简单的方法是用一个栈,先顺序遍历链表,并把每个节点放入栈中,遍历完成后,栈的出栈顺序正好是原链表的逆,代码如下:

92faeba7-3414-eb11-8da9-e4434bdf6706.png

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

94faeba7-3414-eb11-8da9-e4434bdf6706.png

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

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  7、合并有序链表:基本思路跟合并有序数组一样,但是不需要O(N)的空间复杂度了,只需要O(1)的空间复杂度,代码如下:

99faeba7-3414-eb11-8da9-e4434bdf6706.png

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

9bfaeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  8、链表排序:如果没有空间复杂度、时间复杂度的要求,那可选的方法太多了,像插入排序、选择排序、冒泡排序,但是如果要求时间复杂度为O(NlogN),而且空间复杂度为O(1)呢?归并排序!!!正好可以用上刚刚写的合并有序链表的代码,代码如下:

a0faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  9、链表的循环右移:举例如下1->2->3->4->5->NULL,循环右移2位后,变成了4->5->1->2->3->NULL,可以这么考虑,如果链表的长度为N,循环右移K位,那么等效于循环右移 K%N位,K%N是一个小于N的数,然后我们只需要找到循环右移后的头节点即可,上面的例子就是4,然后直接把1->2->3链接到4->5->的后面,代码如下:

a4faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  10、以组为单位翻转链表:组的长度用K表示,比如原链表为1->2->3->4->5,当K=2时,翻转的结果为2->1->4->3->5,当K=3时,翻转的结果为3->2->1->4->5,即先翻转K个,再翻转K个,当剩下的节点数小于K时,就不用翻转了。用递归的方法很容易实现,代码如下:

a7faeba7-3414-eb11-8da9-e4434bdf6706.png

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

aafaeba7-3414-eb11-8da9-e4434bdf6706.png

acfaeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg 11、翻转链表的相邻节点,比如原链表为1->2->3->4,翻转后为2->1->4->3,这个其实就是上一道题的特例,即K=2,也要求空间复杂度为O(1),不过还是递归简洁啊,这里只给出递归的代码:

aefaeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  12、删除链表的倒数第N个节点,要求只遍历一次,还记得检测环的入口吗?是的,用前后指针,前后指针需要相隔(N+1)步,这样当前指针为NULL的时候,后指针正好指向倒数第(N+1)个节点,然后直接删除倒数第N个节点即可,代码如下:

b0faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  13、删除有序链表中的重复元素,如原链表为1->2->3->3->4->4->5,删除后的链表为1->2->5,这道题的关键是如果某节点有重复,务必将其全部删掉,所以要对有重复的节点做个标记,代码如下:

b2faeba7-3414-eb11-8da9-e4434bdf6706.png

技巧:哑变量的引入使得头结点不再具有特殊性,从而简化处理流程。5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  14、像快排那样将链表分成前后两个部分,比如原链表为1->4->3->2->5->2,给出数字3,那么链表中比3小的放在前面,比3大(或等于3)的放在链表的后面,处理后的链表应该是这样的1->2->2->4->3->5,注意,4和5都大于3,那么处理后的链表中4仍然应该在5的前面,代码如下:

b4faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  15、合并K个有序链表,这个咋一听很简单,先合并第1个、第2个,然后将合并后的结果与第3个合并,然后将合并的结果与第4个合并……,假设每个链表的长度为N,那么时间复杂度为O(NK²),N为总的节点数,因为要合并K次。其实有更优的时间复杂度,我们可以先两两合并,即第1个与第2个合并,第3个与第4个合并,即执行K/2次合并,这作为第一轮合并,时间复杂度为O(KN),接下来就只需要合并K/2个有序链表了,即进行第二轮合并,这样总共需要进行logK轮合并,每一轮的时间复杂度为O(KN),所以总的时间复杂度为O(NKlogK),代码如下(合并两个有序链表的代码已经在前面给出):

b6faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  16、寻找两个链表的第一个公共节点,即两个链表从某个节点开始合并了,我们要找出这个节点,经典的方法是先计算两个链表的长度:L1,L2,假设L1>L2,那么公共节点一定不在链表1的前(L1-L2)个节点中,这样我们就可以让链表1的头节点指向第(L1-L2+1)个节点,然后同时推进两个链表的头节点,边推进边比较,直到遇到同一个节点,代码如下:

b9faeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  17、链表的插入排序,这个就很直白了,代码如下:

bbfaeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  18、把有序链表转换成一个尽量平衡的二叉树,其实所谓的尽量平衡,就是把链表的中间节点作为根节点,根节点左边的链表是树的左子树,根节点右边的链表是树的右子树,然后问题就转换成了原问题的子问题,即用前半段链表建立一个尽量平衡的左子树,用后半段链表建立一个尽量平衡的右子树,代码如下:

bdfaeba7-3414-eb11-8da9-e4434bdf6706.png

5efaeba7-3414-eb11-8da9-e4434bdf6706.jpeg  19、在链表上模拟加法运算,比如(2 -> 4 -> 3) + (5 -> 6 -> 5)=7 -> 0 -> 9,链表的头节点为个位,然后是十位……其实模拟的关键就是处理进位,代码如下(略长,但比较直白):

bffaeba7-3414-eb11-8da9-e4434bdf6706.png

c3faeba7-3414-eb11-8da9-e4434bdf6706.png

信息整理自网络

c4faeba7-3414-eb11-8da9-e4434bdf6706.png

实践出真知▼

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值