前言
《剑指offer》的16&19-22连续出现了多个链表类的题目,比之前的难度要稍微高一些。这时候一定要画图做分析,达到事半功倍的效果。
关于链表的结构请参考:
《剑指offer》预备知识-链表与二叉树
我会直接调用里面定义的这个链表的类
之前还出现了两道典型题目:05的链表反向输出和21的链表反转,可以参考这个笔记:
《剑指offer》05&21、链表反向输出与反转
关于链表其实网上有海量的文章,但是到了这份上相信对链表本身大家已经相当熟悉,只是一些思路不能很快的理清,也没有很clean的代码。我在文后摘录了一些其它代码供大家一起学习提高。我自己也对这些代码有一定的参考。
删除链表节点
分析
offer16的要求是,给出一个节点,要求从一个链表中删除该节点。
先考虑简单的情形:
1、整个链表只有一个节点,直接删掉
2、整个链表有多个节点,要删除尾节点:先顺序遍历到尾结点前一个节点,然后直接指向None
3、整个链表有多个节点,要删除中间的某个:
对于这种情况,我们将要删除节点的下一个节点的值和指针赋给该节点,然后再把下一个节点删掉;此时原本要删除节点就“摇身一变”,变成了它的下一节点。需要注意的是还要另外找一个节点做中间过渡。我们以下图删除中间的节点3为例:
代码
有了以上的分析,代码就好写了。
# offer16-solution
class Solution:
def delete_node(self, head_node, del_node):
if not (head_node and del_node):
return False
# 要删除的节点不是尾节点
if del_node.next_node:
# 先找到要删除的节点的下一个节点
del_next_node = del_node.next_node
# 再将下一个节点的值和指针都赋给该节点
del_node.value = del_next_node.value
del_node.next_node = del_next_node.next_node
# 最后删除下一节点,此时下一节点已被当前节点所取代
del_next_node.value = None
del_next_node.next_node = None
# 链表只要一个节点,删除头节点(也是尾节点)
elif del_node == head_node:
head_node = None
# 不写这行可能不AC
del_node = None
# 链表中有多个节点,删除尾节点
else:
node = head_node
# 遍历到尾结点的前一节点,然后直接指向None
while node.next_node != del_node:
node = node.next_node
node.next_node = None
# 不写这行可能不AC
del_node = None
return head_node
链表中的倒数第k个节点
offer19要求链表中的倒数第
k
k
k个节点,最无脑的方法自然是先遍历一次链表得到
n
=
l
e
n
g
t
h
(
l
s
t
)
n=length(lst)
n=length(lst),然后再一次遍历找到第
n
−
k
+
1
n-k+1
n−k+1个节点。
有没有一次遍历就搜到倒数第
k
k
k个节点的方法呢?答案是双指针。多个指针和链表搭配的题目最近越来越常见,但是有些题目比较新颖,第一眼不一定能找到思路,不过只要多积累多看,其实也就那么回事。
就本题而言,我们设置两个指针,让第一个指针先走
k
k
k步,然后两个指针同时前进直到第一个指针到达链表尾部,那么第二个指针就指向链表中的倒数第
k
k
k个节点了。
代码自然也不难
# offer19-solution
class Solution:
def FindKthToTail(self, head, k):
if not head or k <= 0:
return None
p1head = p2head = head
for i in range(k-1):
if p1head.next:
p1head = p1head.next
else:
return None
while p1head.next:
p1head = p1head.next
p2head = p2head.next
return p2head
环的入口
分析
offer20要求寻找链表中是否存在环,如果存在,还要输出环的入口。
我们刚才已经初步尝试过双指针了,如链接2所言,双指针类问题大体分为左右指针和快慢指针两种。offer19是简化版的左右指针,寻找环的问题就要考虑快慢指针了。
我们先思考一下小时候经常遇到的追及问题。有两个小孩在跑步,他们的速度自然是不一样的。如果跑道是纯粹的直线,那么两人距离只会越来越远。但如果跑道是环形跑道,那快的小孩总能追上慢的小孩。即便是在环形跑道前加一段直线跑道也是一样的道理。
那么对于存在环的链表,我们也给两个指针,快指针每次向前走两步,慢指针每次向前走一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。
为了不失一般性,我画了一个直线跑道比环形跑道更长的链表(这样一来快指针在环里就会走不止一圈),假设这个链表的前5个节点是线型的,6是环的入口,6-9节点形成一个环,如下图我们可以看到:
第一阶段:慢指针S走到环的入口,此时走了6步,快指针F走了12步,则快指针先走完了直线跑道6+一个环4+新的步数2;此时F会停在节点8的位置(这个大家在心中模拟一下)
第二阶段:追及。S从6到8的时候,F就会从8→6→8。此时两指针相遇,这就说明链表中有环。
在解决第一个问题以后,我们还要找到环的入口,这其实是一个数学问题:我们设线型链表的长度为
L
L
L,环形链表的长度为
R
R
R,慢指针走的步数为
k
k
k,快指针就是
2
k
2k
2k了。再设快慢指针相交的节点距离环的入口长度为
x
x
x,那我们可以列两条等式:
{
k
=
L
+
x
2
k
=
L
+
n
R
+
x
(
n
∈
N
∗
)
\left\{\begin{array}{c} k=L+x \\ 2 k=L+n R+x\left(n \in N^{*}\right) \end{array}\right.
{k=L+x2k=L+nR+x(n∈N∗)
消掉
k
k
k,有
L
=
n
R
−
x
(
n
∈
N
∗
)
L=n R-x\left(n \in N^{*}\right)
L=nR−x(n∈N∗),左边的意义非常明显,右边的
n
R
−
x
nR-x
nR−x代表“还差
x
x
x步就走完
n
n
n圈”,考虑慢指针现在的状况——距离环的入口有
x
x
x步,也就是说,如果这时候有个新指针从起点出发,逐步遍历,当它走完
L
L
L步到达环的入口的时候,慢指针也将同时回到环的入口。
代码
通过如上的分析我们就可以写代码了:
第一阶段:通过构造快慢指针,判断是快指针先到达None,还是两指针先相遇。前者意味着没有环,后者意味着有环。
第二阶段:如果有环,从两指针相遇的时间节点开始,让一个新的慢指针从头开始遍历,当新旧慢指针第一次相遇时,那就是命运的十字路口——环的入口。
# offer20-solution
class Solution:
def EntryNodeOfLoop(self, pHead):
pFast = pHead
pSlow = pHead
while pFast!=None and pFast.next!=None:
pFast = pFast.next.next
pSlow = pSlow.next
#两个指针相遇且非空,则说明有环
if pFast == pSlow:
break
if pFast == None or pFast.next == None:
return None
pFast=pHead
while (pFast != pSlow):
pFast = pFast.next
pSlow = pSlow.next
#返回环的入口节点
return pFast
合并有序链表
思路其实不少,包括遍历的思路。不过我觉得挺容易想到的是利用链表的有序性:比较list1 list2的头节点大小,取比较小的链表作为新链表的起点。依次取小的节点往该链表中插入元素。这个过程最好写成递归。
# offer22-solution
class Solution:
def ListMerge(self, pHead1, pHead2):
if not pHead1:
return pHead2
elif not pHead2:
return pHead1
pMHead = None
if (pHead1.val < pHead2.val):
pMHead = pHead1
pMHead.next = self.ListMerge(pHead1.next, pHead2)
else:
pMHead = pHead2
pMHead.next = self.ListMerge(pHead1, pHead2.next)
return pMHead
能想到递归的话,这题的难度比前三个都要低一些。