大数跨境
0
0

LeetCode 21合并两个有序链表详解

LeetCode 21合并两个有序链表详解 点点技术屋
2025-11-27
0
题目解析:为什么这道题是链表操作的入门必刷题

LeetCode 21题 "合并两个有序链表" 是链表操作的经典入门题,题目要求将两个升序排列的链表合并为一个新的升序链表并返回。这个问题看似简单,却蕴含了链表操作的核心思想,包括指针移动、节点连接和边界处理。

题目给出的示例非常直观:

  • 输入:l1 = [1,2,4], l2 = [1,3,4]

  • 输出:[1,1,2,3,4,4]

这道题的价值在于它的解法多样性和可拓展性。无论是迭代法还是递归法,都能很好地训练我们的逻辑思维能力。而且掌握了这道题的解法后,我们还能将其拓展应用到"合并K个有序链表"等更复杂的问题中。

链表结构基础:为什么链表操作需要特殊技巧

在讲解具体解法之前,我们需要先回顾一下链表的基本结构。与数组不同,链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

每个链表节点包含两个部分:

  • 数据域:存储数据元素

  • 指针域:存储下一个节点的指针

这种结构使得链表在插入和删除操作上比数组更高效,但也带来了一些操作上的复杂性。比如在我们今天要解决的合并问题中,我们需要小心地处理指针的指向,避免出现断链或循环引用的情况。

迭代法详解:最直观的链表合并思路

迭代法是解决这个问题最直观的思路,它的核心思想是创建一个新的链表,然后依次比较两个输入链表的节点值,将较小的节点添加到新链表中。

具体步骤如下:

  1. 创建一个虚拟头节点(dummy head),这可以简化边界情况的处理

  2. 维护一个当前指针,用于构建新链表

  3. 同时遍历两个输入链表,比较当前节点值

  4. 将较小的节点接到当前指针后面,并移动相应的指针

  5. 当一个链表遍历完后,将另一个链表的剩余部分接到新链表末尾

下面是Python实现代码:

class ListNode:    def __init__(self, val=0next=None):        self.val = val        self.next = next
def mergeTwoLists(l1: ListNode, l2: ListNode) -> ListNode:    # 创建虚拟头节点    dummy = ListNode(-1)    current = dummy
    # 同时遍历两个链表    while l1 and l2:        if l1.val <= l2.val:            current.next = l1            l1 = l1.next        else:            current.next = l2            l2 = l2.next        current = current.next
    # 处理剩余节点    current.next = l1 if l1 else l2
    return dummy.next

Java实现:

public class ListNode {    int val;    ListNode next;    ListNode() {}    ListNode(int val) { this.val = val; }    ListNode(int val, ListNode next) { this.val = valthis.next = next; }}
class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        ListNode dummy = new ListNode(-1);        ListNode current = dummy;
        while (l1 != null && l2 != null) {            if (l1.val <= l2.val) {                current.next = l1;                l1 = l1.next;            } else {                current.next = l2;                l2 = l2.next;            }            current = current.next;        }
        current.next = l1 != null ? l1 : l2;
        return dummy.next;    }}

JavaScript实现:

function ListNode(val, next) {    this.val = (val===undefined ? 0 : val)    this.next = (next===undefined ? null : next)}
var mergeTwoLists = function(l1, l2) {    const dummy = new ListNode(-1);    let current = dummy;
    while (l1 && l2) {        if (l1.val <= l2.val) {            current.next = l1;            l1 = l1.next;        } else {            current.next = l2;            l2 = l2.next;        }        current = current.next;    }
    current.next = l1 || l2;
    return dummy.next;};

迭代法的时间复杂度是O(n + m),其中n和m分别是两个输入链表的长度。空间复杂度是O(1),因为我们只使用了常数级别的额外空间。

递归法详解:用数学归纳法理解链表合并

递归法虽然不如迭代法直观,但它提供了一种更优雅的解决方案。递归的核心思想是将大问题分解为小问题:要合并两个链表,我们可以先比较它们的头节点,然后递归地合并剩余的节点。

递归思路:

  1. 基线条件:如果其中一个链表为空,返回另一个链表

  2. 递归条件:比较两个链表的头节点值

  3. 将较小节点的next指向合并剩余节点的结果

  4. 返回较小的节点

Python实现代码:

def mergeTwoLists(l1: ListNode, l2: ListNode) -> ListNode:    # 基线条件    if not l1:        return l2    if not l2:        return l1
    # 递归条件    if l1.val <= l2.val:        l1.next = mergeTwoLists(l1.next, l2)        return l1    else:        l2.next = mergeTwoLists(l1, l2.next)        return l2

Java实现:

class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        // 基线条件        if (l1 == null) {            return l2;        }        if (l2 == null) {            return l1;        }
        // 递归条件        if (l1.val <= l2.val) {            l1.next = mergeTwoLists(l1.next, l2);            return l1;        } else {            l2.next = mergeTwoLists(l1, l2.next);            return l2;        }    }}

JavaScript实现:

var mergeTwoLists = function(l1, l2) {    // 基线条件    if (!l1) return l2;    if (!l2) return l1;
    // 递归条件    if (l1.val <= l2.val) {        l1.next = mergeTwoLists(l1.next, l2);        return l1;    } else {        l2.next = mergeTwoLists(l1, l2.next);        return l2;    }};

递归法的时间复杂度同样是O(n + m),但空间复杂度是O(n + m),因为递归调用会占用栈空间,递归深度最多为n + m。

边界情况处理:这些特殊情况你考虑到了吗

在解决链表问题时,边界情况的处理尤为重要。对于合并两个有序链表的问题,我们需要考虑以下几种特殊情况:

  1. 其中一个链表为空

    • 解决方案:直接返回另一个链表

  2. 两个链表都为空

    • 解决方案:返回空链表

  3. 一个链表是另一个链表的前缀

    • 例如:l1 = [1,2,3], l2 = [4,5,6]

    • 解决方案:迭代法中自然处理,当一个链表遍历完后,将另一个链表的剩余部分接到新链表末尾

  4. 链表中存在重复元素

    • 例如:l1 = [1,1,2], l2 = [1,3,4]

    • 解决方案:比较时使用<=,保证稳定性

  5. 其中一个链表只有一个节点

    • 解决方案:迭代法和递归法都能自然处理

算法优化与拓展:从合并两个到合并K个

掌握了合并两个有序链表的方法后,我们可以思考如何将其拓展应用到更复杂的问题中。最典型的拓展就是"合并K个有序链表"(LeetCode 23题)。

有两种主要思路可以解决这个问题:

  1. 顺序合并:重复应用合并两个有序链表的函数,依次合并所有链表

    • 时间复杂度:O(k^2 * n),其中k是链表数量,n是平均长度

    • 空间复杂度:O(1)

  2. 分治合并:将K个链表分成两半,递归合并每一半,最后合并两个结果

    • 时间复杂度:O(k * n * log k)

    • 空间复杂度:O(log k),递归调用栈的深度

下面是分治合并的Python实现思路:

def mergeKLists(lists):    if not lists:        return None    return merge_range_lists(lists, 0, len(lists) - 1)
def merge_range_lists(lists, leftright):    if left == right:        return lists[left]    mid = (left + right// 2    l1 = merge_range_lists(lists, left, mid)    l2 = merge_range_lists(lists, mid + 1right)    return mergeTwoLists(l1, l2)  # 这里调用我们之前实现的合并两个链表的函数

除了分治算法,我们还可以使用优先队列(最小堆)来解决合并K个有序链表的问题:

  1. 将所有链表的头节点加入优先队列

  2. 取出最小节点加入结果链表

  3. 如果该节点有下一个节点,将其加入优先队列

  4. 重复步骤2和3,直到优先队列为空

这种方法的时间复杂度是O(k * n * log k),空间复杂度是O(k)。

总结与思考:这道题教会我们什么

LeetCode 21题虽然看似简单,但它蕴含了很多重要的算法思想和编程技巧:

  1. 虚拟头节点技巧:这是链表操作中非常常用的技巧,可以简化边界情况的处理,避免对头节点进行特殊判断。

  2. 迭代 vs 递归:同一问题往往有多种解法,迭代法通常更高效,而递归法更简洁优雅。理解两种方法的优缺点和适用场景,有助于我们在实际问题中做出正确选择。

  3. 分治思想:将大问题分解为小问题,解决小问题后合并结果,这种思想在很多算法中都有应用,如排序算法中的归并排序、快速排序等。

  4. 边界情况处理:在编写代码时,充分考虑各种边界情况,能让我们的程序更加健壮。

这道题虽然简单,但它是很多复杂链表问题的基础。掌握了它的解法,我们就能更好地理解和解决更复杂的链表问题,如合并K个有序链表、排序链表等。

最后,我想强调的是,算法学习不仅是为了解题,更是为了培养一种解决问题的思维方式。希望通过这道题的讲解,大家不仅能掌握合并有序链表的方法,更能理解背后的算法思想,将其应用到更广泛的问题中去。

【声明】内容源于网络
0
0
点点技术屋
写自己擅长的Nginx,Linux,Redis,JVM和Golang 写自己学习的算法,运维总结 写一些散文随笔 写我最爱的大熊猫,发大熊猫的图片
内容 120
粉丝 0
点点技术屋 写自己擅长的Nginx,Linux,Redis,JVM和Golang 写自己学习的算法,运维总结 写一些散文随笔 写我最爱的大熊猫,发大熊猫的图片
总阅读4
粉丝0
内容120