阅读,与值得关注的内容Readance

面了美团AI全栈开发,面试官让我写个合并两个排序链表,我笑了:“我刷题500道,苦练举一反三的能力,你就考我原题?”

正在更新力扣hot100系列,希望早日更完:https://programmercarl.com/hot100/

美团、字节、小米汽车,今年都有考察 合并两个排序好的链表,字节的面试官还特别强调了一下,是否可以用递归。

美团
美团
字节
字节

这道题目虽然简单,也小细节也很多。

21、 合并两个有序链表,力扣链接:https://leetcode.cn/problems/merge-two-sorted-lists/

题目描述

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:

输入:list1 = [1,2,4], list2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

输入:list1 = [], list2 = []
输出:[]

示例 3:

输入:list1 = [], list2 = [0]
输出:[0]

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • list1 和 list2 均按非递减顺序排列

思路

两个链表本身已经有序,怎么利用这个条件呢?

只需要比较两个链表当前节点的值,谁更小,就把谁接到结果链表后面,然后让对应链表的指针向后移动一位。

例如 list1 当前是 2,list2 当前是 3,那么 2 一定是剩余节点中的最小值,可以放心地把节点 2 接入结果链表。

这里有两个问题需要想清楚。

第一个问题:结果链表的头结点怎么处理?

如果直接操作真正的头结点,每接入一个节点,都要判断结果链表是不是空。为了统一操作,我们定义一个**虚拟头结点 dummy**,再用 cur 指向结果链表的尾部:

  • dummy:固定不动,方便最后找到结果链表的头结点
  • cur:始终指向已合并部分的最后一个节点
  • list1list2:分别指向两个链表中还没有处理的第一个节点

每轮循环只做三件事:

  1. 比较 list1->val 和 list2->val
  2. 把较小的节点接到 cur->next
  3. 移动被选中的链表指针,再移动 cur

第二个问题:如果一个链表先遍历完了怎么办?

另一个链表剩余部分本来就是有序的,并且其中所有节点都不小于已经合并的节点,所以不需要继续逐个比较,直接把剩余链表整体接到 cur->next 即可

最后返回 dummy.next,因为 dummy 只是为了方便操作,并不属于真正的结果链表。

模拟过程

以 list1 = [1,2,4]list2 = [1,3,4] 为例。

先创建虚拟头结点 dummy,令 cur = dummy。此时结果链表为空,list1 和 list2 分别指向两个链表的第一个节点。

比较两个链表的当前节点。相等时我们接入 list1 的节点 1,随后 list1 指向节点 2,cur 指向刚接入的节点 1。

下一轮比较 2 和 1,接入 list2 的节点 1。后续仍然按照相同规则,依次接入节点 2、3、4。

当 list1 已经指向空,而 list2 还剩一个节点 4 时,循环结束。直接执行 cur->next = list2,结果链表就是 [1,1,2,3,4,4]

很多录友写这道题时,容易忘记最后接上剩余链表。想清楚循环条件是“两个链表都不为空”,自然就知道循环结束后至少有一个链表为空,还要处理另一个链表。

解题代码

迭代法

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy(0)// 虚拟头结点,统一头结点的处理
        ListNode* cur = &dummy;

        while (list1 != nullptr && list2 != nullptr) {
            if (list1->val <= list2->val) {
                cur->next = list1;
                list1 = list1->next;
            } else {
                cur->next = list2;
                list2 = list2->next;
            }
            cur = cur->next;
        }

        // 一个链表为空后,直接接上另一个链表的剩余部分
        cur->next = list1 != nullptr ? list1 : list2;
        return dummy.next;
    }
};

递归法

递归写法要先明确:当前应该返回哪个节点作为合并后链表的头结点?

如果 list1->val <= list2->val,那么当前头结点一定是 list1。接下来只需要把 list1->next 指向“list1->next 与 list2 合并后的结果”。另一种情况同理。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if (list1 == nullptrreturn list2;
        if (list2 == nullptrreturn list1;

        if (list1->val <= list2->val) {
            list1->next = mergeTwoLists(list1->next, list2);
            return list1;
        }

        list2->next = mergeTwoLists(list1, list2->next);
        return list2;
    }
};

复杂度分析

迭代法:时间复杂度 O(m + n),空间复杂度 O(1)。

递归法:时间复杂度 O(m + n),空间复杂度 O(m + n),空间消耗来自递归调用栈。

其他语言

Python3

class Solution:
    def mergeTwoLists(self, list1, list2):
        dummy = ListNode(0)
        cur = dummy

        while list1 and list2:
            if list1.val <= list2.val:
                cur.next = list1
                list1 = list1.next
            else:
                cur.next = list2
                list2 = list2.next
            cur = cur.next

        cur.next = list1 if list1 else list2
        return dummy.next

Java

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;

        while (list1 != null && list2 != null) {
            if (list1.val <= list2.val) {
                cur.next = list1;
                list1 = list1.next;
            } else {
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next;
        }

        cur.next = list1 != null ? list1 : list2;
        return dummy.next;
    }
}

Go

func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
    dummy := &ListNode{}
    cur := dummy

    for list1 != nil && list2 != nil {
        if list1.Val <= list2.Val {
            cur.Next = list1
            list1 = list1.Next
        } else {
            cur.Next = list2
            list2 = list2.Next
        }
        cur = cur.Next
    }

    if list1 != nil {
        cur.Next = list1
    } else {
        cur.Next = list2
    }
    return dummy.Next
}

JS

var mergeTwoLists = function(list1, list2{
    const dummy = new ListNode(0);
    let cur = dummy;

    while (list1 !== null && list2 !== null) {
        if (list1.val <= list2.val) {
            cur.next = list1;
            list1 = list1.next;
        } else {
            cur.next = list2;
            list2 = list2.next;
        }
        cur = cur.next;
    }

    cur.next = list1 !== null ? list1 : list2;
    return dummy.next;
};

与代码随想录联系

这道题把代码随想录链表章节里的两个基本功放到了一起:虚拟头结点统一边界处理,指针移动完成链表拼接

建议录友们把本题和203. 移除链表元素、206. 反转链表放在一起练习。前者帮助理解虚拟头结点,后者帮助熟悉保存节点和改变 next 指向。


前往微信阅读全文

内容来自公众号,可前往微信查看原文。

查看作者的更多文章 →