算法题——合并两个已排序链表(不使用额外空间)

题目如下:

        输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。

        数据范围: 0≤n≤10000≤n≤1000,−1000≤节点值≤1000−1000≤节点值≤1000
要求:空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)

        如输入{1,3,5},{2,4,6}时,合并后的链表为{1,2,3,4,5,6},所以对应的输出为{1,2,3,4,5,6},转换过程如下图所示:

        

或输入{-1,2,4},{1,3,4}时,合并后的链表为{-1,1,2,3,4,4},所以对应的输出为{-1,1,2,3,4,4},转换过程如下图所示:


一、解决方案

(一)迭代法

        迭代法是一种通过逐步比较和连接节点来合并两个已排序链表的方法。首先,创建一个虚拟头节点,用于存储合并后的链表。这个虚拟头节点的作用是方便我们在迭代过程中始终有一个固定的起始点来连接节点。

        然后,使用两个指针分别指向两个待合并的链表的头节点。在迭代过程中,比较这两个指针所指向的节点的值,将较小值的节点连接到虚拟头节点后面。接着,移动指向较小值节点的指针到下一个节点,并更新用于连接节点的指针,使其始终指向合并后链表的最后一个节点。

        例如,假设有两个已排序链表:链表 A 为 2->4->6,链表 B 为 1->3->5。首先,比较链表 A 的头节点值 2 和链表 B 的头节点值 1,将值为 1 的节点连接到虚拟头节点后面,然后移动指向链表 B 的指针到下一个节点。接着比较链表 A 的头节点值 2 和链表 B 的新头节点值 3,将值为 2 的节点连接到合并后的链表中,再移动指向链表 A 的指针。依此类推,直到两个链表中的所有节点都被处理完毕。

        迭代法的优点是直观易懂,实现起来相对简单。时间复杂度为 O (n),其中 n 是两个链表的总长度,因为需要遍历两个链表的所有节点。空间复杂度为 O (1),因为只使用了常量级别的额外空间,即虚拟头节点和几个指针。

(二)递归法

        递归法是另一种实现合并两个已排序链表的方法。递归的基本思想是将问题分解为更小的子问题,直到子问题可以直接解决,然后再将子问题的解组合起来得到原问题的解。

        在合并两个已排序链表的问题中,递归的实现方式如下:首先,比较两个链表的头节点值。如果一个链表的头节点值较小,那么将这个节点作为合并后链表的头节点,并递归地调用函数来合并这个链表的下一个节点和另一个链表。如果一个链表为空,直接返回另一个链表。

        例如,对于链表 A 为 2->4->6 和链表 B 为 1->3->5,首先比较链表 A 的头节点值 2 和链表 B 的头节点值 1,由于 1 较小,所以将值为 1 的节点作为合并后链表的头节点,然后递归地合并链表 A 和链表 B 的下一个节点,即合并 2->4->6 和 3->5。

        递归法的优点是代码简洁,易于理解。时间复杂度也是 O (n),因为需要遍历两个链表的所有节点。然而,递归法可能会导致栈溢出的问题,特别是当链表很长时,因为递归调用会占用大量的栈空间。空间复杂度在理论上是 O (n),但由于实际实现中编译器的优化等因素,可能会小于 O (n)。不过,相比迭代法,递归法通常会占用更多的空间。

二、C语言实现

(一)迭代法实现

#include <stdio.h>
#include <stdlib.h>

// 链表节点结构
struct ListNode {
    int val;
    struct ListNode *next;
};

// 创建新节点
struct ListNode* createNode(int val) {
    // 为新节点分配内存
    struct ListNode* newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
    // 初始化新节点的值
    newNode->val = val;
    // 新节点的下一个指针初始化为 NULL
    newNode->next = NULL;
    // 返回新创建的节点指针
    return newNode;
}

// 迭代法合并两个已排序链表
struct ListNode* mergeTwoListsIterative(struct ListNode* l1, struct ListNode* l2) {
    // 创建一个虚拟头节点,这个节点不存储实际数据,只是为了方便操作结果链表
    struct ListNode dummy;
    // tail 指针用于指向结果链表的尾节点
    struct ListNode* tail = &dummy;

    // 当两个输入链表都不为空时循环
    while (l1 && l2) {
        // 如果 l1 节点的值小于 l2 节点的值
        if (l1->val < l2->val) {
            // 将 l1 节点连接到结果链表的尾部
            tail->next = l1;
            // 移动 l1 指针指向下一个节点
            l1 = l1->next;
        } else {
            // 将 l2 节点连接到结果链表的尾部
            tail->next = l2;
            // 移动 l2 指针指向下一个节点
            l2 = l2->next;
        }
        // 更新 tail 指针指向结果链表的新尾节点
        tail = tail->next;
    }

    // 如果 l1 不为空,将 l1 连接到结果链表尾部;如果 l1 为空,l2 必然不为空,将 l2 连接到结果链表尾部
    tail->next = l1? l1 : l2;

    // 返回虚拟头节点的下一个节点,即合并后的链表的头节点
    return dummy.next;
}

(二)递归法实现

// 递归法合并两个已排序链表
struct ListNode* mergeTwoListsRecursive(struct ListNode* l1, struct ListNode* l2) {
    // 如果 l1 为空,直接返回 l2
    if (!l1) return l2;
    // 如果 l2 为空,直接返回 l1
    if (!l2) return l1;

    // 如果 l1 的值小于 l2 的值
    if (l1->val < l2->val) {
        // 将 l1 的下一个节点设置为递归调用 mergeTwoListsRecursive 处理 l1 的下一个节点和 l2 的结果
        l1->next = mergeTwoListsRecursive(l1->next, l2);
        // 返回 l1,因为它是当前合并后的链表的头部
        return l1;
    } else {
        // 将 l2 的下一个节点设置为递归调用 mergeTwoListsRecursive 处理 l1 和 l2 的下一个节点的结果
        l2->next = mergeTwoListsRecursive(l1, l2->next);
        // 返回 l2,因为它是当前合并后的链表的头部
        return l2;
    }
}

        以上两段代码实现了两种方法来合并两个已排序的链表,并且不使用额外的空间。

        迭代法通过比较两个链表的当前节点值,将较小值的节点连接到结果链表中,直到其中一个链表遍历完,然后将剩余的链表连接到结果链表的末尾。

        递归法通过比较两个链表的当前节点值,将较小值的节点连接到结果链表中,然后递归地处理剩余的链表部分。

三、应用场景

合并两个已排序链表的算法在实际中有广泛的应用场景,充分体现了其强大的实用性。

(一)合并有序用户列表

        在许多软件系统中,用户信息通常以链表的形式存储。例如,一个社交平台可能有两个已按照用户注册时间排序的用户链表,一个是新注册用户列表,另一个是老用户列表。当需要对所有用户进行统一管理或进行特定的用户数据分析时,就可以使用合并两个已排序链表的算法来将这两个用户列表合并为一个有序的用户列表。这样可以方便地进行用户信息的查询、排序和处理,提高系统的性能和用户体验。

(二)合并有序日志文件

        在软件开发和系统运维中,日志文件是非常重要的信息来源。假设一个系统有两个分别记录不同类型事件的日志文件,并且这两个日志文件都是按照时间顺序排序的。为了进行综合的日志分析,就可以使用合并两个已排序链表的算法来将这两个日志文件合并为一个有序的日志文件。这样可以更方便地追踪系统的运行状态,发现问题并进行故障排除。

        除了上述场景,该算法还可以应用于数据库管理系统中的索引合并、文件系统中的文件合并等场景。在数据库管理系统中,合并有序的索引链表可以优化查询性能,提高数据检索的速度。在文件系统中,将多个有序的小文件合并为一个大的有序文件可以减少文件碎片,提高文件读写的效率。

        总之,合并两个已排序链表的算法在各种实际应用场景中都具有重要的价值,能够帮助我们更高效地处理数据,提高系统的性能和稳定性。

四、总结展望

        合并两个已排序链表的算法具有诸多优势。首先,它简单高效,无论是迭代法还是递归法,都能在不使用额外空间的情况下实现链表的合并,时间复杂度为 ,其中 是两个链表的总长度。这使得在内存资源有限的环境下,如嵌入式系统开发中,能够有效地节省内存开销,提高程序的性能和效率。

        其次,该算法具有广泛的应用前景。在实际的软件开发中,无论是合并有序用户列表、合并有序日志文件,还是在数据库管理系统中的索引合并、文件系统中的文件合并等场景,都能发挥重要作用。它可以方便地进行数据的整合和排序,提高系统的性能和稳定性。

        然而,该算法也可能面临一些挑战。例如,递归法可能会导致栈溢出的问题,特别是当链表很长时。在实际应用中,需要根据具体情况选择合适的方法。如果对内存使用有严格要求,迭代法可能是更好的选择;如果追求代码简洁性,递归法可能更合适。

        总之,合并两个已排序链表的算法虽然存在一些挑战,但凭借其简单高效和广泛的应用前景,在计算机科学领域中具有重要的地位。随着技术的不断发展,相信该算法将在更多的领域得到应用和优化。


//该题来源如下,BM4题https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Fintelligent%3FquestionJobId%3D10

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值