链表合并与节点交换——LeetCode 第 23&24 题

这篇博客讨论了LeetCode的第23题(合并K个排序链表)和第24题(两两交换链表中的节点)。对于第23题,博主首先尝试了直接合并方法,但由于超时,后来改用存储排序节点值的方式解决。第24题中,博主通过递归实现了节点交换,虽然开始时表现不佳,但最终成功完成。博客强调了递归在处理链表问题中的应用及其复杂性分析。
摘要由CSDN通过智能技术生成

今天的两道题目全都围绕链表,第一个是困难级别的、要合并多个排序的链表;第二题是中等难度,需要两两交换链表中的节点,昨天没能用递归法写出代码,今天就尝试用递归实现了下,测试效果不咋地,但递归法跑通了!

题目一

第 23 题:合并K个排序链表:

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

输入:
[
  1->4->5,
  1->3->4,
  2->6
]
输出: 1->1->2->3->4->4->5->6

思路

刚看到这题面,联想昨天刚写了合并两个排序链表的题目,最先想到的就是基于能够实现合并两个的方法 mergeTwoLists(l1,l2),先取列表中前两条链表合并,再将结果分别与第三、四等一直到最后一条链表合并,最终结果即所求。但按这个思路写完代码提交后,测评结果是“超出时间限制”。

如果想简化处理,我们从结果来看,其实就是把所有可能的数以链表的形式输出。那么思路就来了:我们把列表中所有链表中出现的节点数字存起来,排序后用这些数字重新生成一条链表,就达到题目要求了。

代码

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

class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        # 用来存所有数字的列表
        record = []
        # 遍历链表所在列表
        for item in lists:
            # while 循环对每条链表取值
            while item!=None:
                # 将链表中每个节点的当前值存入列表中
                record.append(item.val)
                # 移动链表中的节点
                item = item.next
        # 将取到的结果进行排序
        record.sort()
        # 建个初识节点
        start = ListNode(0)
        # 复制下用于最后返回
        start_copy = start
        # 对记录的数字进行遍历
        for i in record:
            # 新生成的链表每个节点值即排序的数字
            start.next = ListNode(i)
            # 移动生成的链表位
            start = start.next
        # 通过复制的初识节点的下一位来返回完整链表
        return start_copy.next    

感觉怎么从链表角度来考虑问题,但又确实用到了链表的 val 和 next 属性,也凑活吧。

提交

效果还不错,可能是取巧了:

执行用时 : 84 ms, 在所有 Python3 提交中击败了 86.09% 的用户
内存消耗 : 17.4 MB, 在所有 Python3 提交中击败了 7.14% 的用户

学习

翻看其它题解时,多数都是称我们刚这思路为暴力法。同时,我最初基于合并两个链表来实现的思路,被称为分治法。

字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

我把代码贴出来分析下:

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

class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        if lists==[]:
            return None
        elif len(lists)==1:
            return lists[0]

        def mergeTwoLists(l1, l2):
            start = ListNode(0)
            start_copy = start

            while l1!=None or l2!=None:
                if l1==None:
                    start.next = l2
                    l2 = l2.next
                elif l2==None:
                    start.next = l1
                    l1 = l1.next
                elif l1.val<l2.val:
                    start.next = l1
                    l1 = l1.next
                else:
                    start.next = l2
                    l2 = l2.next
                start = start.next
            return start_copy.next

        # 先把列表中第一条链表抽出来
        l1 = lists[0]
        # 不断抽出列表中剩余的链表来进行两两合并    
        for i in range(1,len(lists)):
            l1 = mergeTwoLists(self,l1,lists[i])
        return l1

与题解中别的代码做了番对比,我最后这个调用了 n 次自定义函数,但如果做一番调整可以只调用 log2(n) 次:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None
class Solution:
    def merge(self, node_a, node_b):
        dummy = ListNode(None)
        cursor_a, cursor_b, cursor_res = node_a, node_b, dummy
        while cursor_a and cursor_b:  # 对两个节点的 val 进行判断,直到一方的 next 为空
            if cursor_a.val <= cursor_b.val:
                cursor_res.next = ListNode(cursor_a.val)
                cursor_a = cursor_a.next
            else:
                cursor_res.next = ListNode(cursor_b.val)
                cursor_b = cursor_b.next
            cursor_res = cursor_res.next
        # 有一方的next的为空,就没有比较的必要了,直接把不空的一边加入到结果的 next 上
        if cursor_a:
            cursor_res.next = cursor_a
        if cursor_b:
            cursor_res.next = cursor_b
        return dummy.next

    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        length = len(lists)

        # 边界情况
        if length == 0:
            return None
        if length == 1:
            return lists[0]

        # 分治
        mid = length // 2
        return self.merge(self.mergeKLists(lists[:mid]), self.mergeKLists(lists[mid:length]))

#作者:LotusPanda
#链接:https://leetcode-cn.com/problems/merge-k-sorted-lists/solution/xiong-mao-shua-ti-python3-3chong-jie-fa-bao-li-you/

这里我直接贴人家的代码来做对比,由于合并两个链表的函数结果是一个链表,那么就可以使用上面这种 合并(合并(一半),合并(另一半)) 的巧妙思路,调用次数瞬间降到了 log2(n)。

执行用时 : 184 ms, 在所有 Python3 提交中击败了 24.29% 的用户
内存消耗 : 18.8 MB, 在所有 Python3 提交中击败了 7.14%的用户

但测评效果仍不理想,就是因为合并两个链表的过程其实也是蛮复杂费时的。

题目二

第 24 题:两两交换链表中的节点:

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例:

给定 1->2->3->4, 你应该返回 2->1->4->3.

思路

虽然仍然可以将链表转化为列表再来简化操作,但这次我就想在链表结构的基础上用递归法来完成。

可以定义一个函数 swap(n) 它可以合并 n 节点和其子节点,那么这个函数最终结束的条件就是 n 为单数没有子节点了、或就剩最后 n 和它的子节点了,我们分别对这些情况都做处理。

但此外的交换节点过程基本都是相似的,但要注意的是,n 和 n.next 交换后,n 所在的新节点指向下一单元,而下一单元也会经历类似的交换。所以理想的顺序是倒序生成结果,类似于昨天递归时不断调用自身函数,按照函数完成顺序其实是倒序完成的。所以这里我们也可以在进行 swap(n) 时,在其中继续调用 swap(n.next.next) 对下一单元进行交换,这样最终会先完成对下一单元的交换,再来进行对 n 的交换。

这么空说比较模糊,我们看代码,这次是自己硬肝出来的哦:

代码

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

class Solution:
    def swapPairs(self, head: ListNode) -> ListNode:
        # 定义交换节点的 swap 函数
        def swap(node):
            # 若节点为空
            if node==None:
                return None
            # 若子节点为空,即只剩一个节点了
            elif node.next==None:
                return node
            # 若下一单元为空,即最后只剩两个节点来交换了
            elif node.next.next==None:
                # 交换左右节点
                l,r = node.next,node
                # 重新定义节点关系
                l.next = r
                # 下一单元为空,r 的子节点为空
                r.next = None
                return l
            # 若子节点之后下一单元仍有
            else:
                # 先对下一单元进行 swap 运算
                n = swap(node.next.next)
                # 上面的运算拿到结果 n 即我们这里交换后下一单元
                l,r = node.next,node
                l.next = r
                # 交换后的子节点下一位为 n
                r.next = n
                return l
        # 直接启动 swap 即可
        result = swap(head)
        return result

提交

看到测试通过简直感动得要哭了!尽管表现并不好,但真真是独立对一个中等难度题目的递归尝试啊!

执行用时 : 132 ms, 在所有 Python3 提交中击败了5.75% 的用户
内存消耗 : 13.7 MB, 在所有 Python3 提交中击败了 6.25% 的用户

优化

比较了下自己的递归和推荐题解的递归,确实考虑的情况太繁杂了些,比如 swap(node) 开始的 node==None 和 node.next.next==None 其实是等价的关系,这里我贴一份官方题解中对递归的实现:

# 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: ListNode) -> ListNode:
        """
        :type head: ListNode
        :rtype: ListNode
        """

        # If the list has no node or has only one node left.
        if not head or not head.next:
            return head

        # Nodes to be swapped
        first_node = head
        second_node = head.next

        # Swapping
        first_node.next  = self.swapPairs(second_node.next)
        second_node.next = first_node

        # Now the head is the second node
        return second_node

#作者:LeetCode
#链接:https://leetcode-cn.com/problems/swap-nodes-in-pairs/solution/liang-liang-jiao-huan-lian-biao-zhong-de-jie-di-19/

可以看出,代码中没有多定义一个额外函数、直接用自身调用来实现了功能,考虑的边界情况、和归纳过程更简洁。

估计是避免了定义新函数的调用,时间也瞬间降了下来:

执行用时 : 48 ms, 在所有 Python3 提交中击败了 27.40% 的用户
内存消耗 : 13.7 MB, 在所有 Python3 提交中击败了 6.25% 的用户

其实交换过程还是挺绕的,像这代码写的简单,看看就可能绕住了,写多代码可能会思路更清晰些。

结论

第一道是困难题目,第二道是中等难度,现在对这级别没有概念了,昨天那中等难度的括号题,硬生生卡了好几小时,今天倒是快了些,但一展开对比来分析,时间也不少。

围绕链表展开的题目好多,相关题目做多了确实能积累经验和形成第一感觉,我这才做了二十来道题,就已经可以相互借鉴了,这感觉挺不错的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值