[剑指offer] 24. 反转链表

题目

定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。

思考题:请同时实现迭代版本和递归版本。

样例
输入:1->2->3->4->5->NULL
输出:5->4->3->2->1->NULL

思路

如何把一个单链表进行反转?

方法1:将单链表储存为数组,然后按照数组的索引逆序进行反转。需要借助外部空间,且两次遍历。
方法2:迭代,就地反转链表。使用3个变量遍历单链表,逐个链接点进行反转。
方法3:递归,终止条件是子链表为空或者只有一个节点。
方法4:用头插法新建链表。我们知道创建链表的两种方式:头插法和尾插法,头结点插入法形成的链表是和输入顺序相反的,尾节点插入法形成的链表是和输入顺序相同的,所以其中一种方法是,遍历原链表,然后用原链表的输出元素顺序用头结点插入法新建链表,这样新建的链表就是反转后的链表。这个方法和第一个借助数组一样,需要额外一倍的空间来存储生成的新链表。

算法1:迭代,O(n)

翻转即把所有节点的next指针指向前驱节点。
由于是单链表,我们在迭代时不能直接找到前驱节点,所以我们需要一个额外的变量pre保存前驱节点。同时在改变当前节点的next指针前(反转前),也要先记录下它的后继节点next,这样才不会在cur.next=pre时断链。循环终止说明cur为空,则应返回pre(cur == None的前一个节点)

空间复杂度分析:遍历时只有3个额外变量,所以额外的空间复杂度是 O(1)
时间复杂度分析:只遍历一次链表,时间复杂度是 O(n)

遍历在while循环中完成。 需要把当前节点cur的next指针指向前驱节点pre(即,翻转),然后继续往后遍历,那么,刚才的cur节点就是它后一个节点的前驱节点了,也就是pre往后移一位到刚才的cur节点。同样,cur节点也应往后移一位,此时发现原来的cur.next指针已经被覆盖为指向pre,所以应该在覆盖操作前保存一个原始cur.next,这里保存为next,后面将cur赋值给pre,将next赋值给cur。
alt

# 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
        """
        # 申请两个节点,pre和cur,pre指向None
		pre = None
		cur = head
		# 遍历链表
		while cur:
			# 记录当前节点的下一个节点
			next = cur.next
			# 然后将当前节点指向pre
			cur.next = pre
			# pre和cur节点都后移一位
			pre = cur
			cur = next
		#上面4行可以合并为1行:
		#   pre, pre.next, cur = cur, pre, cur.next
		return pre	

算法2:递归,O(n)

首先我们要考虑这里定义的 reverseList 函数能做什么,它可以翻转一个链表,并返回新链表的头节点,也就是原链表的尾节点。

递归的思路:
1.大问题变成小问题(解决方法一样,规模变小)
2.递归终止条件(最小的基本问题)
3.小问题的解构成大问题的解

易错重点:
递归的精华在于递归条件的传递性,也就是这里括号中的head.next
有了这个.next 才会从当前递归进入下一个递归,一直自动的递归下去。
直到满足终止条件才可以return某个值,使得函数可以继续执行递归语句后面的其他语句,否则就要卡在递归这一步一直递归下去。
在这里,每一层递归都return同一个值tail,最后也是输出tail。

我们可以先递归处理 reverseList(head.next),这样我们可以将以head.next为头节点的链表翻转,同时得到原链表的尾节点tail。此时head.next是新链表的尾节点,我们令它的next指针也就是head.next.next指向head,并将head.next指向空即可将整个链表翻转,且新链表的头节点是tail。
而reverseList(head.next)又可以先递归处理reverseList(head.head.next),将head.next.next为头节点的链表翻转,得到该链表的尾节点tail,此时head.next.next是新链表的尾节点…… 一直递归,直到处理的那个链表只有一个节点不需要翻转,为止。

空间复杂度分析: 总共递归 n 层,系统栈的空间复杂度是 O(n),所以总共需要额外 O(n)的空间。
时间复杂度分析: 链表中每个节点只被遍历一次,所以时间复杂度是 O(n)。
alt
比如,输入 [1,2,3,4,5],刚开始的head 为 1,不满足终止条件,调用reverseList(1.next),也就是reverseList(2),该函数执行的时候里面的 head == 2,不满足终止条件,继续调用reverseList(2.next=3),然后是reverseList(3.next=4),然后是reverseList(4.next=5),执行reverseList(5)时head==5满足递归终止条件,返回5这个尾节点,作为翻转后的头节点。在reverseList(5)终止了递归,开始向回传递,返回到上一层递归的reverseList(4.next)语句,继续执行reverseList(4)的剩余语句,即,4.next.next=4,也就是5.next指向4,然后4.next指向None,完成了4,5之间的翻转, 并在最后返回tail == 5作为翻转后的头节点,作为上一层reverseList(3.next)语句的输出,然后继续执行reverseList(3)的剩余语句,即,3.next.next=3,也就是4的next指向3,然后3.next=None,也就是3的next指向None,并在最后仍然返回tail=5到上一层递归调用它的reverseList(2.next)语句,然后继续执行剩余语句,即,令3的next指向2,令2的next指向None,返回tail=5到上一层递归调用它的reverseList(1.next)语句,然后继续执行剩余语句,即,令2的next指向1,令1的next指向None,返回tail为5作为翻转后的头节点。递归完成。

# 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
        """
        ##### 求解基本问题(递归的终止条件)
        # 当只有一个节点或者为空的时候,不再需要翻转
        if head == None or head.next==None:
            return head
         
        ##### 大问题如何变为小问题
        # 这里的tail是链表的最有一个节点
        tail = self.reverseList(head.next)
        ##### 小问题的解如何变为大问题的解
        # 如果链表是 1->2->3->4->5,那么此时的tail就是5
		# 而head是4,head的下一个是5,下下一个是空
		# 所以head.next.next 就是5->4
        head.next.next = head
        # 防止链表循环,需要将head.next设置为空
        head.next = None
        # 每层递归函数都返回tail,也就是最后一个节点
        return tail

注意:
【1】递归调用函数这里需要 self.xxxx(params),容易漏掉self
【2】将head.next和head翻转之后,必须head.next设为None

在这里插入图片描述

算法3:头插法

引入dummy作为head节点的前一个节点,那么我们就可以直接操作head节点了(参照 反转链表II,这是一个很有用的思路)
遍历链表,每次把head.next扔到dummy后面,也就是dummy.next=head.next,然后继续移动head

class Solution:  # 迭代法 头插法,面试时需要配合画图和面试官讲解
    def reverseList(self, head: ListNode) -> ListNode:
        dummy = ListNode(0)
        dummy.next = head
        while head and head.next:
            # 先记录下head后面的节点cur
            cur = head.next
            # head越过cur指向cur.next
            head.next = cur.next
            # cur指向dummy现在后接的节点(最开始是head,后面不停更新,因此用dummy.next指代)
            cur.next = dummy.next
            # dummy指向cur,cur成为新的dummy.next
            dummy.next = cur
        # 同理,dummy后面的节点(反转后的头结点)不断更新,而dummy是唯一保持不变的
        return dummy.next

Tips:

写之前先想好一些特殊测例。写完要再次检查是否符合测例,最好先跑一下测例,通过了再提交

本题测例:
1.空链表
2.只有一个节点的链表
3.多个节点的链表

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值