数据结构|LeetCode(力扣)经典题:链表

本文详细解析了LeetCode中关于链表的几道经典题目,包括两数相加、反转链表以及K个一组翻转链表的多种解法,如递归、迭代、尾插法等。通过实例探讨了不同解法的时间复杂度和空间复杂度,并提供了代码实现。

链表

更多请查看我的专栏:LeetCode(力扣)刷题指南

可直接在LeetCode中搜索题目名称

1. 两数相加:链表

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

1.1 解决方案

算法思路:时间复杂度O(max(len(l1),len(l2)))即O(n)

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


class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        dummyNode=ListNode(0) #值不重要
        l3=dummyNode
        carry=0  #进阶初始化,carry=0或1
        while(l1 or l2):
            x=l1.val if l1 else 0  #如果l1的值为真,返回给x;否则x=0({},[],null,False,()这些数据都为假)
            y=l2.val if l2 else 0  #为什么不是if l1.val或l2.val:如果是[1,8],[0]就会出错,l2的第二个结点根本不存在,无法判断
            sum_l1l2=x+y+carry
            carry=sum_l1l2//10
            l3.next=ListNode(sum_l1l2%10)  #这两行代码可以使用divmod()
            l3=l3.next
            if(l1 != None):l1=l1.next  #不能判断l1.next!=None,否则l1.next永远不等于0,进入死循环
            if(l2 != None):l2=l2.next
        if(carry==1):
            l3.next=ListNode(1)
        return dummyNode.next  

1.2 思考和总结

  1. 如何理解逆序?

    用笔算,也是从个位数开始

  2. 如何理解dummyNode?

    这是个哑结点,用来返回列表(链表)l3

  3. 如何理解carry?

    9+9=18,而每个结点只能存储一个数,所以下一个结点要+carry(=1)

  4. 为什么是 if l1 else 0而不是if l1.val else 0?

    其实l1是类的一个实例,特性有val,next,即使l1.val=Nonel1.next=Nonel1仍然也为真。假设输入是[1,8], [0]。进入第二轮循环时,l2=None,此时l2没有特性.val,会出现AttributeError: 'NoneType' object has no attribute 'val’的错误。

2. 反转链表:递归

2.1 解决方案

1.利用外部空间

  1. 申请一个动态扩容的数组或者容器,比如ArrayList这样的。然后不断遍历链表,将链表中的元素添加到这个容器中。
  2. 再利用容器自身的 API,反转整个容器,这样就达到反转的效果了。
    最后同时遍历容器和链表,将链表中的值改为容器中的值。因为此时容器的值是:5 4 3 2 1,链表按这个顺序重新被设置一边,就达到要求啦。
    当然你可以可以再新创建 N 个节点,然后再返回,这样也可以达到目的。
    这种方式很简单,但你在面试中这么做的话,面试官100%会追问是否有更优的方式,比如不用外部空间。

2.双指针迭代:(时间O(n):112ms,空间O(n):30MB)

  1. 申请两个指针,第一个指针pre初始指向None;第二个指针cur初始指向head;
  2. 将cur的next指向pre,然后 pre 和 cur 前进一位。
  3. 迭代完(cur 变成 None),pre 就是最后一个节点了。

(迭代中,需要临时变量now来完成cur和pre的指向变化)

迭代.gif

代码实现

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

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        #迭代
        pre,cur=None,head
        while(cur):
            now=cur.next
            cur.next=pre
            pre=cur
            cur=now
        return pre

3.迭代:(时间O(n):112ms,空间O(1):33.7MB)

递归的两个条件:

  1. 终止条件是当前节点或者下一个节点==None
  2. 在函数内部,改变节点的指向,也就是 head 的下一个节点指向 head 递归函数:head.next.next = head

​ 其实就是 head 的下一个节点指向head。
​ 递归函数中每次返回的 cur 其实只最后一个节点,在递归函数内部,改变的是当前节点的指向。
动画演示如下:

递归.gif

代码实现

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

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        #递归
        if head==None or head.next==None:
            return head

        cur=self.reverseList(head.next)
        head.next.next=head
        head.next=None
        return cur  #对自身的调用

2.2 总结和思考

1.递归

函数实现的具体步骤:

#递归步骤
[1,2,3,4,5]
Step5.reverseList(1)>>>2.next=1,1.next=None,return 1
 ^
 ^
 ^
Step4.reverseList(2)>>>3.next=2,2.next=None,return 2
 ^
 ^
 ^
Step3.reverseList(3)>>>4.next=3,3.next=None,return 3
 ^
 ^
 ^
Step2.reverseList(4)>>>5.next=4,4.next=None,return 4
 ^
 ^
 ^
Step1.reverseList(5)>>>return 5

3. K个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

示例 :

给定这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5


说明 :

  • 你的算法只能使用常数的额外空间。
  • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

3.1 解决方案

1.栈

用栈,我们把 k 个数压入栈中,然后弹出来的顺序就是翻转的!

这里要注意几个问题:

第一,剩下的链表个数够不够 k 个(因为不够 k 个不用翻转);

第二,已经翻转的部分要与剩下链表连接起来。

注意:这里的空间复杂度为O(k),并不符合题目要求

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

class Solution:
    def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
        dummy = ListNode(0)  #设置一个哑结点,初始化为链表的头结点
        p = dummy  
        while True:
            count = k 
            stack = []  #初始化栈
            tmp = head  #初始化:本个翻转区的第一个结点存放在临时结点中
            while count and tmp:  #将可翻转区的结点压栈
                stack.append(tmp)
                tmp = tmp.next
                count -= 1
            # 注意,目前tmp所在k+1位置,即下一个可翻转区的第一个结点
            # 说明剩下的链表不够k个,跳出循环
            if count : 
                p.next = head  #本翻转区不可翻转,按顺序连链接
                break
            # 翻转操作
            while stack:
                p.next = stack.pop()
                p = p.next
            #与剩下链表连接起来 
            p.next = tmp  #此时临时结点为该下一个翻转区的第一个结点
            head = tmp  #初始化初始结点
        
        return dummy.next

2.尾插法

直接举个例子:k = 3

pre
tail    head
dummy    1     2     3     4     5
# 我们用tail 移到要翻转的部分最后一个元素
pre     head       tail
dummy    1     2     3     4     5
	     cur
# 我们尾插法的意思就是,依次把cur移到tail后面
pre          tail  head
dummy    2     3    1     4     5
	     cur
# 依次类推
pre     tail      head
dummy    3     2    1     4     5
....
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
        dummy = ListNode(0)
        dummy.next = head
        pre = dummy
        tail = dummy
        while True:
            count = k
            while count and tail:
                count -= 1
                tail = tail.next
            if not tail: break  #所剩元素不够翻转,跳出循环
            head = pre.next  #将初始结点设为翻转区翻转后的最后一个结点
            while pre.next != tail:
                cur = pre.next # 获取下一个元素
                pre.next = cur.next # pre与cur.next连接起来,此时cur(孤单)掉了出来
                cur.next = tail.next # 和剩余的链表连接起来
                tail.next = cur #插在tail后面
            # 改变 pre tail 的值
            pre = head 
            tail = head
        return dummy.next

3.递归算法

算法思路:

  1. 设置当前结点curreverseKGroup(head,k)head

  2. 循环(当前结点cur为真和count!=k):当前结点cur=cur.next,计数器count+=1

  3. 如果count==k

    • 当前结点cur=reverseKGroup(cur,k)(其中reverseKGroup(cur,k)中的cur为该层递归中可翻转区的第一个结点,即该层可翻转区的初始结点head)(目的:返回下层递归可翻转区的初始结点结点到当前结点cur中)

    • 循环(当count为真):

      1. 将翻转结点的下一个存放到临时结点中tmp=head.next
      2. 将翻转结点指向当前结点head.next=cur,实现翻转;
      3. 将当前结点设为下一个被指向结点(即翻转结点)cur=head
      4. 将临时结点中结点赋给翻转结点head=tmp
      5. 计数器计数count-=1
    • 将该层递归的初始结点设置当前结点(即上层递归中的第一个翻转结点)head=cur

  4. 返回这个初始结点return head(如果有上层递归,则该初始结点为上层递归中的第一个翻转结点;没有这是最上层递归,则是返回翻转后的链表)。

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

class Solution:
    def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
        cur = head
        count = 0
        while cur and count!= k:
            cur = cur.next
            count += 1
        if count == k:
            cur = self.reverseKGroup(cur, k)
            while count:
                tmp = head.next
                head.next = cur
                cur = head
                head = tmp
                count -= 1
            head = cur   
        return head

3.2 思考与总结

1.另外一种思路:

在翻转链表的时候,可以借助三个指针:prev、curr、next,分别代表前一个节点、当前节点和下一个节点,实现过程如下所示。

img

  1. 将 curr 指向的下一节点保存到 next 指针;

  2. curr 指向 prev,一起前进一步;

  3. 重复之前步骤,直到 k 个元素翻转完毕;

  4. 当完成了局部的翻转后,prev 就是最终的新的链表头,curr 指向了下一个要被处理的局部,而原来的头指针 head 成为了链表的尾巴。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值