题目
定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
思考题:请同时实现迭代版本和递归版本。
样例
输入: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。
# 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)。
比如,输入 [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.多个节点的链表