【面试题】一文打尽LeetCode反转链表系列

反转链表Ⅰ

先来看简单的版本
在这里插入图片描述

解法一:迭代法

申请两个指针,第一个指针 pre指向当前节点的前驱节点,最初是指向 null 的。第二个指针 cur指向当前节点,最初是指向 head。然后遍历链表,不断更新 cur,每次遍历到一个新的节点,都将 cur.next 指向 pre,然后 pre 和 cur 都前进一位,直到 cur 变成 null,pre 指向最后一个节点,则表示迭代完了。

Python

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def reverseList(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        cur = head  # 初始化 cur指向 head
        prev = None # 初始化 prev指向 none
        while cur: # 迭代直到 cur为空
            tmp = cur.next # 先记录下当前节点的下一个节点
            cur.next = prev # 将当前节点指向 prev,即它前面的节点
            # pre和cur节点都前进一位
            prev = cur 
            cur = tmp
        return prev # 返回反转后链表新的头结点,即最后一个节点

# 利用python的多变量赋值,可以把while循环里的代码换成一句
# cur.next, prev, cur = prev, cur, cur.next

Java

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) // 链表为空或链表只有一个节点时
            return head;
        ListNode cur = head; // 当前遍历到的结点
        ListNode pre = null; // 当前结点的前驱结点
        ListNode res = null; // 反转后链表的头结点
        while (cur != null){
            ListNode pnext = cur.next; // 当前结点的下一个结点
            if (pnext == null) res = cur; // 如果 pnext即cur.next为空,说明此时 cur到达尾结点处,它正是反转后新链表的头结点
            cur.next = pre; // 调整next指针,反转链表结点顺序
            // pre 和 cur都前进一位
            pre = cur;
            cur = pnext;
        }
        return res;
    }
}

时间复杂度:O(n),n 是列表的长度
空间复杂度:O(1)

解法二:递归法

链表是经典的递归定义的数据结构,链表相关的题目常常会涉及考察递归法,翻转链表是其中的经典题目。

对于递归算法,重要的是明确递归函数的定义。具体来说,这里的 reverseList 函数定义是这样的:
输入一个节点 head,将以 head 为起点的链表反转,并返回反转之后的新链表的头结点。

递归终止条件:当前节点或者下一个节点为空
递归函数内部操作:改变当前结点的指向,将当前结点的下一结点的next指针指向它,即head.next.next = head

Python

class Solution(object):
    def reverseList(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        # 递归终止条件是当前节点为空,或者下一个节点为空,意思是如果链表只有一个节点或为空的时候,反转也是它本身,直接返回即可。
        if (head==None or head.next==None): 
            return head
        res = self.reverseList(head.next) # 递归调用,反转以head.next为起点的链表,返回反转后的新头结点
        head.next.next = head # 当链表递归反转之后,新的头结点是 cur,而递归调用之前的 head 变成了最后一个节点,因此要反转 head.next和 head
        head.next = None # 最后一个节点 head 要指向 NULL
        return res # 返回反转后的新链表的头结点

Java

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) // 递归终止条件
            return head;
        ListNode cur = reverseList(head.next);
        head.next.next = head; // 改变当前结点的指向
        head.next = null;
        return cur;
    }
}

递归解法乍一看不好理解,这里最好配合在纸上画出完整的调用过程来梳理思路,方便理解。比如,反转 1->2->3->4->5,完整的递归调用过程如下:

reverseList: head=1
    reverseList: head=2
	    reverseList: head=3
		    reverseList: head=4
			    reverseList: head=5 
					head.next==None,满足递归终止条件,返回 head
				res = 5
			4.next.next->4,即5->4
			4.next -> null
			res =5
		3.next.next->3,即4->3
		3.next -> null
		res = 5
	2.next.next->2,即3->2
	2.next -> null
	res = 5
1.next.next->1,即2->1
1.next -> null
res = 5
当递归终止后,在回调过程中,res=5 始终指向尾结点,最后返回 res即反转后链表的头结点

反转链表Ⅱ

现在,题目变成了在指定区间内进行反转
在这里插入图片描述

解法一:迭代法

同样地,要实现遍历链表并反转,我们需要两个指针 prev 和 cur,prev 指针初始化为 None,cur 指针初始化为链表的 head。接下来,一步步地向前推进 cur 指针,prev 指针跟随其后,直到 cur 指针到达从链表头起的第 m 个结点。也正是题目所要求的反转链表的起始位置。

注意,为了记录下反转起始位置及其前一个节点,我们需要再引入两个指针 tail 和 con,tail 指针指向从链表头开始的第 m 个结点,此结点是待反转部分链表反转后的尾部,故称为 tail。con 指针指向第 m 个结点的前一个结点,此结点是新链表的头部。tail 和 con 指针在算法开始时被初始化,在算法最后被调用,用于完成链表反转。

cur 抵达第 m 个结点后,先迭代地反转给定链表部分。不断迭代,直到完成第 n 个结点的反转连接。此时,prev 指针会指向第 n 个结点,cur 会指向第 n 个节点的后继节点(第 n+1 个结点)。

我们使用 con 指针来连接 prev 指针,这是因为 prev 指针当前指向的结点(第 n 个结点)会代替第 m 个结点的位置。 同理,我们利用 tail 指针(指向第 m 个结点)来连接 prev 指针之后的结点(第 n+1 个结点),因为反转后第 m 个结点会代替第 n 个节点的位置。

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def reverseBetween(self, head, m, n):
        """
        :type head: ListNode
        :type m: int
        :type n: int
        :rtype: ListNode
        """
        if head == None: # 若链表为空,返回空
            return None

        index = 1  # 当前遍历到第几个位置了
        cur = head 
        pre = None
        tail = None
        con = None
        while index <= n: # 遍历链表直到抵达第 n个节点,后面的节点无需改动
            if index < m: # 到抵达反转链表起始位置 m之前,一步步推进 cur和 pre
                pre, cur = cur, cur.next
            if index == m: # 当到达反转链表起始位置 m时,记录该节点及该节点的前一个节点
                tail = cur # tail指向第 m个节点
                con = pre # con指向第 m个节点的前驱节点
            if index >= m: # 反转链表
                cur.next, pre, cur = pre, cur, cur.next
            index += 1
        # 退出循环时,pre指向的正是第 n个节点,cur指向的是第 n个节点的下一个节点
        if con: # 如果 con即反转起始位置第 m个节点的前一节点不为空,该节点的next指向第 n个节点即 pre
            con.next = pre
        else: # 如果 con即反转起始位置 m的前一节点为空,则说明 m正是链表的第一个位置,反转后第 n个节点代替第 m个节点成为新链表的第一个节点
            head = pre
        tail.next = cur # 第 m个节点代替第 n个节点,其next指向原来第 n个节点的下一个节点
        return head

解法二:递归法

迭代的实现思路虽然看起来简单,但是需要考虑的细节问题很多,有时反而不容易写对。相反,递归实现就很简洁优美。

不妨先来考虑这样一个问题,如何反转链表前 n 个节点?
比如反转前三个节点
在这里插入图片描述
解决思路和前面反转整个链表的递归解法差不多,只要稍加修改即可:

class Solution(object):
    def reverseN(self, head, n): # 反转链表前 n个节点
        # 递归 base case即终止条件变成 n ==1,即当只有一个元素需要反转时,就是它本身
        # 同时注意,现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录其后驱节点 successor(第 n + 1 个节点),反转之后将后面没有反转的部分连接上。
        if (n == 1): 
        	global successor # 声明成 global变量,这样外层的递归函数也可以使用内层递归函数产生的 successor
        	successor = head.next # 记录后驱节点
            return head
        cur = self.reverseN(head.next, n-1) # 递归调用,每次以 head.next 为起点,反转前 n-1 个节点,返回新的头结点
        head.next.next = head # 反转前 n个节点后,head 变成前 n个节点中的结尾
        head.next = successor # 将反转之后的 head 和后面的节点连起来
        return cur # 返回反转后的新链表的头结点

在这里插入图片描述
比如上面的例子,反转 1->2->3->4->5->6 的前3个节点,完整的递归调用过程如下:

reverseN: head=1 n=3
    reverseN: head=2 n=2
	    reverseN: head=3 n=1
			successor = head.next = 4,满足递归终止条件,返回 head和 successor
			return head = 3
		cur = 3 返回新的头结点
	2.next.next->2,即3->2
	2.next -> successor,2->4
	cur = 3
1.next.next->1,即2->1
1.next -> successor,1->4
cur = 3
最后返回 cur=3,即反转后链表的头结点 新链表为:3->2->1->4->5->6

现在回到我们要解决的问题,反转链表的一部分。给一个索引区间 [m,n](1 ≤ m ≤ n ≤ 链表长度),仅仅反转给定区间中的链表元素。

  1. 首先,如果 m == 1,那问题不就相当于反转链表前 n 个元素嘛,也就是我们刚才实现的功能:
  2. 如果 m != 1 呢?如果我们把 head 的索引值视为 1,那么我们是想从第 m 个元素开始反转,直到第 n 个元素;那如果我们把 head.next 的索引视为 1 呢?那么相对于 head.next,反转的区间应该是从第 m-1 个元素开始,一直到第 n-1 个元素;那么相对于 head.next.next ,反转的区间就应该是从第 m-2 个元素开始,一直到第 n-2 个元素;以此类推。。。。。。
class Solution(object):
    def reverseBetween(self, head, m, n):
        """
        :type head: ListNode
        :type m: int
        :type n: int
        :rtype: ListNode
        """
        def reverseN(head, n): # 反转链表前n个节点        
            if n == 1:
                global successor
                successor = head.next # 记录后驱节点
                return head
            cur2 = reverseN(head.next, n-1) # 递归调用,每次以 head.next 为起点,反转前 n-1个节点,返回新的头结点
            head.next.next = head # 反转前 n个节点后,head 变成前 n个节点中的结尾
            head.next = successor # 将反转之后的 head 和后面的节点连起来
            return cur2
            
        if m == 1: # 如果 m == 1,那问题就相当于反转链表前 n个元素
            return reverseN(head,n)
        head.next = self.reverseBetween(head.next, m-1, n-1) # 递归调用, 每次都从向前推进索引起点,直到触发反转的起点
        return head

另一种写法:

class Solution(object):
    def __init__(self):
        self.successor = None

    def reverseN(self, head, n):
        if (n == 1): # 递归 base case即终止条件变成 n ==1,即当只有一个元素需要反转时,就是它本身
            self.successor = head.next # 记录后驱节点
            return head
        cur = self.reverseN(head.next, n-1) # 递归调用,每次以 head.next 为起点,反转前 n-1 个节点,返回新的头结点
        self.successor = head.next.next
        head.next.next = head # 反转前 n个节点后,head 变成前 n个节点中的结尾
        head.next = self.successor # 将反转之后的 head 和后面的节点连起来
        return cur # 返回反转后的新链表的头结点
        
    def reverseBetween(self, head, m, n):
        if m ==1:
            return self.reverseN(head, n)
        head.next = self.reverseBetween(head.next, m-1, n-1)
        return head

反转链表Ⅲ

现在,题目变成了要求两两交换链表中的节点
在这里插入图片描述

解法一:迭代法

可以把整个链表想成两部分:已经两两交换过的前链,还没有交换的后链,每次迭代对后链中每两个相邻的节点进行交换。
使用三个辅助指针:pre_node 指向前链的最后一个节点,也即后链中每组待交换节点的前驱节点;first_node,second_node分别指向当前交换的前节点和后节点。

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def swapPairs(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        # 由于第一组节点交换后会改变整个链表的第一个节点,因此需要头节点的前驱节点
        # 头结点指针在遍历链表的过程,位置会发生跳动,因此需要记录原来头结点的位置
        new_head = ListNode(-1) # 创建一个空头结点记录下原来头结点head的前驱节点
        new_head.next = head # 空头节点的next记录下原来链表的head位置
        pre_node = new_head # first_node的前驱节点,初始是在链表之外的

        while head and head.next: # 如果链表只有一个节点或为空, 退出循环
            # 定义前后两个指针
            first_node = head # 指向待反转节点中的前面的节点      
            second_node = head.next # 指向待反转节点中的后面的节点
            
            # 交换前后两节点
            pre_node.next = second_node # first_node的前驱节点指向要发生变换
            successor = second_node.next # 记录second_node的后继节点
            second_node.next = first_node
            first_node.next = successor
                        
            pre_node = first_node # 交换后,记录下一次first_node的前驱节点      
            head = successor # head直接跳到下一组待交换节点的第一个节点处,即successor的位置
        return new_head.next # new_head不在循环中被操作,它的位置没有移动,new_head.next就是链表原来头结点的位置

代码中添加一个新的空头结点只是为了统一化操作:

  1. 在遍历链表的过程,原来头结点 head 位置会发生跳动,加了空结点,方便记录原来头部的位置。
  2. 第一组节点交换是需要有一个前驱的,这个前驱节点原来并不存在,因此创建一个空头结点记录下原来头结点 head 的前驱节点

图解这个非递归的迭代过程可以参考这篇文章

另一种写法:

class Solution(object):
    def swapPairs(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        pre, pre.next = self, head
        while pre.next and pre.next.next:
            first_node = pre.next
            second_node = first_node.next
            pre.next, second_node.next, first_node.next = second_node, first_node, second_node.next
            pre = first_node
        return self.next

稍微解释一下,首先要注意的是,头指针 pHead 与头结点 head 是不同的,head 是第一个结点,而 pHead.next 才是指向第一个节点,即 pHead.next == head)。那么,使用头指针有什么好处呢?因为链表不像数组可以直接按下标进行索引,链表的遍历要从头节点 head 开始,不断沿着 head.next 的指向去访问下一个元素,可是如果过程中链表发生了反转或其它交换操作,链表第一个节点被改变了,不再是原来 head 所指向的那个位置了,而 head 又不能直接改动,因为我们需要它继续访问后面的元素,这时候我们要如何记录下这个新的第一个节点呢?我们可以创建一个头指针 pHead,让头指针的 next(pHead.next)永远指向第一个节点的位置,这样就可以避免最后返回的时候找不到新链表的第一个节点了。

这里正是把 self 当做链表的头指针使用的,self 是这个类的一个对象,所以在定义类的时候,可以在任何地方给 self 增加新的属性。相信大家都知道在 init(self, attr) 里面可以定义通过 self.myattr = attr 来定义一个 myattr 属性。其实这个语句写在任意一个类的方法里也可以,所以在 swapPairs() 里面当然也可以定义新的属性。所以这行代码pre, pre.next = self, head应该理解为,pre 初始化指向 self,同时为 pre 增加一个 next 属性(此时 pre 也就是 self,它们是一样的现在,所以也相当于为 self 也增加一个 next 属性,虽然 self 不是一个 ListNode 类型的对象,但它作为一个类的对象只要有一个 next 属性就可以了),这个 self.next 属性在后面循环中没被修改过,始终指向第一个结点的位置。最后返回它的时候,也就是新链表的第一个节点。

解法二:递归法

写递归代码,最应该关心的是递归的终止条件(递归出口)和返回值。因为递归本质就是不断重复相同的事情,不要去思考完整的调用栈,一级又一级,很容易把自己绕晕。对于这个问题,

  1. 递归的终止条件是当传入递归函数的子链表只含有一个节点或没有节点的时候
  2. 返回值则是每一组交换节点的后一个节点,因为它是完成反转后的子链表的新头部

至于递归函数里头做了什么,其实很简单,每次递归都只负责交换一对节点,为此,我们同样需要定义两个指针 firstNode 和 secondNode 表示待交换的两个节点。至于下一对需要交换的节点,则交给下一次递归函数去实现。因此,只要链表中还有节点,就继续递归。

class Solution(object):
    def swapPairs(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        # 如果链表只有一个节点或没有节点,无需反转,直接返回自身
        if not head or not head.next:
            return head

        # 定义前后两个指针指向待交换的两个节点
        first_node = head
        second_node = head.next

        # 交换节点
        first_node.next  = self.swapPairs(second_node.next) # 下一对节点由下一个递归完成反转
        second_node.next = first_node # 当前这一组节点进行反转

        # 返回每一组交换节点的后一个节点
        return second_node

反转链表Ⅳ

在这里插入图片描述
前面的题目的两两一组反转链表,现在题目难道又升级了,变成了 k 个元素一组反转链表。

解法一:迭代法

class Solution(object):
    def reverseKGroup(self, head, k):
        """
        :type head: ListNode
        :type k: int
        :rtype: ListNode
        """
        dummy = ListNode(0) # 增加一个哨兵节点
        dummy.next = head
        pre = dummy
        tail = dummy
        while True:
            count = k
            while count and tail: # tail 移到待翻转区间的最后一个元素(第 k个元素)
                count -= 1
                tail = tail.next
            if not tail: break # 说明剩下的链表元素不够 k个, 跳出循环

            head = pre.next
            while pre.next != tail:
                cur = pre.next # 获取下一个元素
                # pre与cur.next连接起来,此时cur(孤单)掉了出来
                pre.next = cur.next 
                cur.next = tail.next # 和剩余的链表连接起来
                tail.next = cur # 依次把cur 移到 tail后面
            # 更新 pre和 tail
            pre = head 
            tail = head
            
        return dummy.next

解法二:递归法

每次递归都只负责反转 k个节点,下一组则交给下一次递归去处理。每次对于待反转区间 [a,b]中的 k个元素,我们可以看成以 a为索引起点,反转前 k个节点。然后,更新索引起点a,待反转区间变成下一组的 k个元素,递归调用,直到剩余待反转区间内的元素不足 k 个。

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def reverseKGroup(self, head, k):
        """
        :type head: ListNode
        :type k: int
        :rtype: ListNode
        """
        # 定义辅助函数,实现反转 start_node到 end_node之间的节点
        def reverseN(start_node, end_node): 
            cur = start_node
            pre = None
            while cur != end_node:
                cur.next, pre, cur = pre, cur, cur.next
            return pre

        if not head: # 空链表直接返回空
            return head
        
        #区间 [start_node, end_node) 包含 k 个待反转元素
        start_node = end_node = head # start_node, end_node初始化为头节点

        # end_node更新为当前待反转区间中的第 k 个元素
        for i in range(k): 
            if end_node == None: # 所待反转区间不足 k 个元素,保持原有顺序,即递归终止条件
                return head 
            end_node = end_node.next

        new_head = reverseN(start_node, end_node) # 反转 start_node到 end_node之间的节点   
        start_node.next = self.reverseKGroup(end_node, k) # 递归反转后续链表,并将前后连接起来

        return new_head

小结

值得一提的是,实际上递归法操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要利用堆栈,空间复杂度是 O(N)。所以考虑效率的话还是使用迭代算法更好。

参考

步步拆解:如何递归地反转链表的一部分
两两交换链表中的节点
图解两两交换链表节点的递归过程
如何k个一组反转链表
【反转链表】:双指针,递归,妖魔化的双指针

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值