前言
反转链表是链表问题中相当常见的一类,在面试题中经常遇见,因此牢牢掌握很有必要~
本文提供反转链表的三种方式:第一种,迭代;第二种,迭代+虚拟头节点,第三种,递归。
其中,迭代和递归是基本思想,“虚拟头节点”作为扩展。
(部分思想来源于算法村、labuladong算法小抄、以及热心的群u~)
(在链表中某个节点node、某个链表listnode大部分情况下指代的是同一个东西,有节点就会有链表、有链表就会有节点,下文中也有如此写法,希望不会产生混淆。)
目录
迭代法
直接上代码:
pre,cur,nxt = None,head,head
while cur:
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
return pre
我们来简单看一下思路:
首先,为了方便理解,请忽略掉nxt,这是中继变量,暂时不考虑。
为了依次将链表进行反转,我们最先要做的事情就是确立使用while函数遍历链表。随便设定一个cur=head,当cur不是空的时候往后遍历即可。
为了遍历,我们知道一定会有cur=cur.next这么一步。但在本题中,由于某种原因,我们提前用nxt保存了cur.next,while的最后让cur=nxt,完成遍历的操作。
在while里写什么呢?
我们假设有这么一个pre,它能够代表已经经过处理、变成倒序的链表(例如:原本完整链表是1 -> 2 -> 3 -> 4 -> 5,pre代表了3 -> 2 -> 1)。
并且假设cur是pre的下一个节点(例如:cur是4,pre是上述部分倒序链表),那么我们要做的工作显而易见:将cur指向pre,使得4 -> 3 -> 2 -> 1。值得注意的是:完成这一步的时候,pre本身没有变化,我们只是对cur进行了操作。
做好这一步之后,我们发现cur代表了值为4的节点(4 -> 5),pre代表了3的节点(3 -> 2 -> 1),因此应该让pre和cur都往后平移一格。即pre = cur和cur = nxt(我们知道这个nxt实际上就是原本的cur.next)。
好了,现在pre是4(4 -> 3 -> 2 -> 1),cur是5(5 -> None),对其自动执行如上操作即可。直到判断cur为None的时候停止。
看起来一切都很妥当,除了一个问题:即使我们完成了遍历,看起来也只是(5 -> 4 -> 3 -> 2 -> 1)啊,1不应该接着指向None嘛?
好问题~不过其实我们在最开始就解决了:因为cur=head(1),pre=None,所以第一次cur.next = pre的时候我们就自动完成了(1 -> None)。
融合虚拟头节点的迭代
dummy = ListNode(-1)
cur = head
while cur:
nxt = cur.next
cur.next = dummy.next
dummy.next = cur
cur = nxt
return dummy.next
关于什么是虚拟头节点,请参照专栏的前文,那里有一些简短的“科普”。
基本思路与上面一模一样,只不过把所有的pre换成了dummy.next。
递归法
先看代码:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 递归
if not head or not head.next:
last = self.reverseList(head.next)
head.next.next = head
head.next = None
return last
对于一个递归函数,最重要的地方是:我们要明确递归函数的定义。
在本题中,我们让递归函数可以反转输入进去的链表(功能),并且返回反转后链表的头节点(返回值)。
好了,我们假设头节点还是头节点(1),指向的是已经被反转的后续链表(5 -> 4 -> 3 -> 2),这个被反转链表的头节点是5。
显然,节点2的next是空的,我们要让它指向1。该怎么写呢?
我们知道尽管后续链表已经改变,然而头节点1和它的后续关系没有改变,即head.next依然是节点2。所以我们只需要head.next.next=head即可。(看起来很喜感(
现在以last(本题中是5)为头节点的反转链表已经扩展到了(5 -> 4 -> 3 -> 2 -> 1),而我们只需要再让head.next=None,让1指向None,即可完成所有的转变~
你可能想知道实际过程中递归函数的运行过程,但不是很建议这么做:理解递归最好的方式是拆分任务、处理好单个任务,然后剩下的全部交给计算机自己做吧。