写出一段代码将链表中的两个节点位置互换位置_干货||链表的技巧和算法总结...

041255656110a2ba111a2dfda1595d4f.png

1e090519ec57bea49a6a899e86ba2209.png

链表的操作总结

7bb02082fb3c68e4f4a88df4ff245e01.png  链表反转

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

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

反转一个单链表。

示例:

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

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

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

719260c8904aa550ca25a6a468aa6dd7.png

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

d563230bf5a2b42a9b42a192c6a7b530.png

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

05e976820ec081ff0312b12ce1ed023c.png

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

5b82abc8d258c31aabc7e84d9d9a8471.png

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

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

2b1da70d1e0d0625adcbb48a8b5b4df1.png

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

27cbb50330e0b7392495863056b0a0b3.png

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

7bb02082fb3c68e4f4a88df4ff245e01.png  快慢指针

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

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

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

8f2200c03fe03d25b433704e6cb2a5cb.png

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

0edcf653b74a876975e397e265513580.png

1e090519ec57bea49a6a899e86ba2209.png

链表的技巧总结

7bb02082fb3c68e4f4a88df4ff245e01.png  技巧一 :理解指针或者引用的含义


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

7bb02082fb3c68e4f4a88df4ff245e01.png  技巧二:警惕指针丢失和内存泄露


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

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

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

7bb02082fb3c68e4f4a88df4ff245e01.png  技巧三:利用哨兵简化实现的难度


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

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

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

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

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


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

7bb02082fb3c68e4f4a88df4ff245e01.png  技巧四、留意边界的处理

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

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

d952567e9fc34ce5d25a0a2b347904f1.png

829fb922e051a69ca09e0e91c3467c36.png

c9ed042d0215259fd0e00a5749289af8.png

2b05ea22303d1feeab6775c2a5683795.png

e12ac8d68b18fec302af84af500c503e.png

7bb02082fb3c68e4f4a88df4ff245e01.png  技巧五:举例画图,辅助思考


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

5c9db259f8bfa137e3805b95c857a647.png

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

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

2、链表头的处理。

3、链表多节点的处理。

4,链表尾的处理。

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

7bb02082fb3c68e4f4a88df4ff245e01.png  技巧六:多谢多练,没有捷径


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

  • 单链表反转

  • 检测链表中的环

  • 两个有序链表的合并

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

  • 球链表的中间节点

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

1e090519ec57bea49a6a899e86ba2209.png

链表的经典技巧及算法

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

31568783490fa77201b15feb6117f12c.png

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

b5bb8d4b5c79cd6901ab12dcee281a0c.png

7bb02082fb3c68e4f4a88df4ff245e01.png  2、检测链表是否有环:经典的做法也是用快慢指针,如果没有环,快指针一定先到达链表的末端(NULL),如果有环,快、慢指针一定会相遇在环中,代码如下:

958620f633acd70e8905e9d6adf3ae07.png

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

66d31070ef6792964b6ff409b0157e8b.png

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

3db3d11ad200d61deaa8809c17743c89.png

7bb02082fb3c68e4f4a88df4ff245e01.png  4、链表翻转:假设原链表为1->2->3,翻转以后的链表应该是1

09067c102ccb4f5f2ac38a837ca4d58a.png

7bb02082fb3c68e4f4a88df4ff245e01.png  5、删除链表中的节点,注意这里只给出要删除的那个节点,不给出链表头(假设被删除节点不是尾节点),代码如下:

18bce1baddd07b585146a7f5f6f26583.png

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

e82ed565ba38bf8d4320fe2e3f3a09aa.png

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

2311c6c43524653f42dc45ed4cc1f089.png

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

7bb02082fb3c68e4f4a88df4ff245e01.png  7、合并有序链表:基本思路跟合并有序数组一样,但是不需要O(N)的空间复杂度了,只需要O(1)的空间复杂度,代码如下:

920017ca3214ffb49caae43a1502806f.png

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

f41216cb87dd41e47029cdc640756bfe.png

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

a67fa334bbf48716697b5986097ceda1.png

7bb02082fb3c68e4f4a88df4ff245e01.png  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->的后面,代码如下:

0ff408ef4c116fa5f266d3513e566673.png

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

58be221eef37f3606c29c320afdac9eb.png

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

a7ec803d7f79876a86b146932f9a7401.png

1452788033c2e8cb71b4bc35381c4c87.png

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

31dbdbe1df1f5165a4e2eddbb5f5b617.png

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

47e4f3d51ed2543103599f9aeb1d7944.png

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

5d018863bd9b7d159ade2281e47fd069.png

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

4c953e48bab6be5a50b7739c40bbe477.png

7bb02082fb3c68e4f4a88df4ff245e01.png  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),代码如下(合并两个有序链表的代码已经在前面给出):

d80c92dc0073ea91f11ec3fef1ae244d.png

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

192471052d96e9eb6b0f0223cf01142c.png

7bb02082fb3c68e4f4a88df4ff245e01.png  17、链表的插入排序,这个就很直白了,代码如下:

d7fa8a347bc9316638cdc5bc8cc0df8f.png

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

431bb02cf4fca0c22590df71f12dec6c.png

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

f14b27ee5ae91d20d47ffd4be68138dc.png

7180b58d34bc69688ab4a5ea9ceb0e6e.png

信息整理自网络

0766be9f049a9d7a533efc17b42bd635.png

实践出真知▼

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值