题 24:反转链表
题干
定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。——《剑指 Offer》P142
测试样例
链表的数据结构定义如下(Python):
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
若传入的头结点 head 指向的链表结构如图所示:
则返回的头结点 head 指向的链表结构为:
迭代法
方法思路
想要翻转链表,其实就是修改每个结点的指针域,使其指向该结点的前驱。修改时,对于每个结点而言有 2 个点需要格外注意,其一是保存每个结点的前驱,第二是记录结点的后继,防止丢失信息而无法访问后续的结点。对于整张链表而言,需要注意链表翻转之后,原本的首元结点的后继应该为 NULL,而原本的尾结点将成为新的首元结点。
方法模拟
例如有如下的链表,使用一个指针 ptr 指向首元结点,pre 指向该结点的前驱(首元结点没有前驱,因此指向 NULL),nex 指向 ptr->next。使用 3 个指针记录是为了在修改结点的前驱、后继关系时,能够做到不丢信息。
在记录 3 个结点的信息之后,可以把 ptr 的后继修改为其前驱 pre。
修改该结点的后继关系后,需要把 ptr 和 pre 往后移动,这样才能继续处理剩余的结点。因为 ptr 指针的信息还在,可以直接把 pre 移动到 ptr 的位置上。
接着把 ptr 移动到 nex 的结点上:
令 nex = ptr->next,就可以更新 nex 指针。经过对原来的首元结点进行调整,现在的链表状态如下,重复上述操作即可实现翻转链表。
解题代码
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
ptr = head #ptr 指向首元结点
pre = None #pre 初始化为 NULL
while ptr:
nex = ptr.next #修改 ptr 的后继
ptr.next = pre #将 ptr 和 pre 向后移动
pre = ptr
ptr = nex
return pre
时空复杂度
设链表的结点数为 n,算法的流程需要遍历整张链表,因此时间复杂度为 O(n)。
由于只需要 3 个指针存储信息,而不需要其他辅助空间,空间复杂度为 O(1)。
递归法
从题解——【反转链表】:双指针,递归,妖魔化的双指针中学习到的。
方法思路
递归法是一种巧妙的方法,充分利用了递归回溯的功能。由于链表不支持随机访问,因此想要访问第 i 个结点,就需要先访问第 i-1 个结点。如果将思路逆转过来,翻转链表可以视为将原来的尾结点当做头结点,做尾插法。此时的关键点有 2 个,其一是如何将原来的尾结点当做首元结点返回,其二是如何反向取到每一个结点。
方法模拟
这时可以充分利用递归,首先递归遍历整个链表,递归函数每次传入结点 ptr 的后继 ptr-> next。
当遍历到尾结点时开始回溯,此时递归函数的返回值是尾结点,因为这样就类似于接力棒,可以把尾结点作为翻转后的链表的头结点传回来。至于如何实现“尾插法”,注意此时递归的层次已经回到了尾结点的前驱,因此可以通过 ptr->next 取到当前递归层次所在的结点的后继,这样就可以进行修改。
再次递归回溯,就可以继续取到前面的结点了,继续 ptr->next 取到当前结点的后继进行操作。不断重复上述操作,即可完成链表逆序。
题解代码
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
#遍历完所有结点作为递归出口
if(head == None or head.next == None):
return head #返回尾结点
tail = self.reverseList(head.next) #接收返回的尾结点
head.next.next = head #修改 head->next->next 的指向
head.next = None
return tail #仍然返回尾结点,向前传递
时空复杂度
设链表的结点数为 n,算法的流程需要遍历整张链表,因此时间复杂度为 O(n)。
由于递归的深度和结点数 n 相同,保留递归的状态需要额外的空间,空间复杂度为 O(1)。
参考资料
《剑指 Offer(第2版)》,何海涛 著,电子工业出版社
【反转链表】:双指针,递归,妖魔化的双指针