刷题总结:反转链表

140 阅读5分钟

在数据结构与算法的学习和刷题的过程中,链表作为一种重要的线性数据结构,常常会出现在各类面试与竞赛题目里,而反转链表这一经典题目更是频繁亮相,极具代表性。

这篇文章是我刷完灵神b站课程【反转链表】www.bilibili.com/video/BV1sd… 所作的一些总结

链表的基本概念

将火车类比链表,链表的第一个节点是头节点,每一个节点都包含节点值val(value),指向下一个节点的指针next,最后一个节点指向空

image.png

class ListNode:
    def __init__(self, val=0, next=None): 
        self.val = val 
        self.next = next

反转链表

反转后的链表是这样的 头节点指向空,其他的节点指向它的上一个节点

image.png

实现反转链表,我们就可以遍历到每一个节点,将它的next指针指向上一个节点,就需要用变量cur(current)表示当前遍历到的节点,用pre(previous)表示上一个节点,对于链表的第一个节点来说它的上一个节点是空,所以previous为空

image.png 模拟下 pre初始为空,cur初始为头节点,while cur,将cur.next=pre pre=cur 进入下一个循环,进入不了,因为cur.next已经被改变了,无法将cur定位到下一个节点,这个时候就要用一个临时变量nxt来记录cur.next,使循环能正常运行,如图

image.png

代码实现:

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        pre=None
        cur=head
        while cur:
            nxt=cur.next
            cur.next=pre
            pre=cur
            cur=nxt
        return pre

用递归来写这个代码

终止条件:如果传入节点为空(空链表),或者传入节点下一个节点为空(只有一个节点的链表不反转以后等于本身)

操作,将当前节点(cur)的下一个节点(cur.next)的next指针(cur.next.next)指向当前节点,将当前节点(cur)与当前节点下一个节点的指针断开(cur.next=None)避免形成环。

调用,将当前节点(cur)的下一个节点(cur.next)传入函数,返回新的cur,调用函数要在操作之前,因为在操作的时候,cur.next会变,跟上面nxt变量操作是一样的

代码实现

class Solution:
    def reverseList(self, cur: Optional[ListNode]) -> Optional[ListNode]:
        if cur is None or cur.next is None:
            return cur
        new_cur = self.reverseList(cur.next)
        cur.next.next = cur
        cur.next = None
        return new_cur

如果要反转的是中间一部分的链表

假设有四个节点的链表,要反转中间两个,将中间两个单拎出来,1指向中间链表的头节点,中间链表的头节点指向4

反转之后也是一样的,1指向中间链表的头节点,中间链表的头节点指向4

image.png

1的下一个节点(1.next 也就是2)指向4,1就指向3

我们把中间链表的上一个节点叫做p0,反转链表后,pre就是反转链表的末节点,cur是反转链表的下一个节点 把p0指向pre 将p0.next指向cur

为什么1.next为2,反转链表后不是应该会改变吗?仔细看,反转链表的操作是将2的next指针指向None,并不会改变1与2的连接,所以1.next还是可以定位到2的

但是,还有一个特殊情况要考虑,就是如果从头节点开始反转怎么办?p0定位不了,其实也很简单,在头节点前面放一个哨兵节点(dummy),指向头节点就行了

操作一下 定义一个哨兵节点:dummy=ListNode(next=head)

p0先指向哨兵节点:p0=dummy

将p0定位到要反转链表的上一个节点 for _ in range(left-1): p0=p0.next

套公式反转链表:pre=None,cur=p0.next这里并不是while cur 而是循环要反转链表的长度 for _ in range(right-left+1):然后就是反转链表的操作

p0.next=pre p0.next.next=cur

返回新链表的头节点 如果要反转链表在中间,则返回head就行,如果在开头呢?就返回要反转链表的末节点,这边用到哨兵节点的好处就是,哨兵节点指向链表的头节点,无论是在要反转链表在开头还是不在开头都指向链表的头节点。所以直接返回dummy.next就行了

代码实现

class Solution:
    def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
        dummy=ListNode(next=head)
        p0=dummy
        for _ in range(left-1):
            p0=p0.next
        pre=None
        cur=p0.next
        for _ in range(right-left+1):
            nxt=cur.next
            cur.next=pre
            pre=cur
            cur=nxt
        p0.next.next=cur
        p0.next=pre
        return dummy.next

一个小方法:如果头节点会变,用哨兵节点能快速找到头节点

k个一组翻转链表

每逢k个反转一次 考虑的第一个问题,就是如果链表长度不够了怎么办,这道题要知道链表的长度(n) 知道了长度,就是循环,每次n-=k 当n<k时跳出循环,然后每一次循环就是重复上一题的操作

class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        cur=head
        n=0
        while cur:
            n+=1
            cur=cur.next
        
        dummy=ListNode(next=head)
        p0=dummy
        while n>=k:
            n-=k
            pre=None
            cur=p0.next
            for _ in range(k):
                nxt=cur.next
                cur.next=pre
                pre=cur
                cur=nxt
            nxt=p0.next
            p0.next.next=cur
            p0.next=pre
            p0=nxt
        return dummy.next

这里要注意一点的就是,要更新p0的位置,用nxt将p0.next记录下来。

以上是我的一些总结和理解,如果你觉得有用点个赞吧,如果有什么不足请多指教(抱拳)