每日一题——力扣88. 合并两个有序数组(举一反三)


一个认为一切根源都是“自己不够强”的INTJ

个人主页:用哲学编程-CSDN博客
专栏:每日一题——举一反三
题目链接

我的写法:
 

#include <string.h> // 引入string.h头文件,用于使用memmove函数

// 定义merge函数,用于合并两个有序数组
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
    if(n==0) // 如果nums2数组为空,则无需合并,直接返回
        return;

    // 使用memmove函数将nums1数组中从第n个位置开始的m个元素向后移动n个位置
    // 这样做的目的是为了给nums2数组中的元素腾出空间
    memmove(nums1+n, nums1, sizeof(int) * m);

    int i=n, j=0, k=0; // 初始化三个指针,i指向nums1中待比较的元素,j指向nums2中待比较的元素,k指向合并后的数组中待填充的位置

    // 循环比较nums1和nums2中的元素,并将较小的元素放入nums1中
    // 循环结束的条件是其中一个数组的元素已经全部被取完
    for(; i<=nums1Size-1 && j<=n-1;)
        nums1[k++] = (nums1[i] <= nums2[j] ? nums1[i++] : nums2[j++]);

    // 如果nums1数组中的元素已经全部被取完,则将nums2中剩余的元素复制到nums1中
    if(i > nums1Size-1)
        memmove(nums1+k, nums2+j, (n-j) * sizeof(int));

    // 如果nums2数组中的元素已经全部被取完,则将nums1中剩余的元素复制到nums1中
    else if(j > n-1)
        memmove(nums1+k, nums1+i, (nums1Size-i) * sizeof(int));
}

代码点评

这段代码实现了两个有序数组的合并操作,其核心思想是利用memmove函数高效地移动数组元素,并通过双指针技术来比较和合并元素。下面是对这段代码的专业点评:

代码优点:

  1. 空间效率高: 代码没有使用额外的数组来存储合并后的结果,而是直接在nums1数组上进行操作,这避免了额外的空间开销。
  2. 逻辑清晰: 代码逻辑清晰,通过指针i和j分别指向nums1和nums2中待比较的元素,通过指针k记录合并后数组的位置,使得合并过程直观易懂。
  3. 边界处理: 代码考虑了nums2为空的情况,以及合并过程中某个数组元素先被取完的情况,进行了合理的边界处理。

代码缺点:

  1. 可读性: 代码中的memmove调用可能会让不熟悉该函数的读者感到困惑,因为它不是C语言初学者常用的函数。
  2. 错误处理: 代码没有包含错误处理逻辑,例如,如果输入的数组大小与实际元素数量不匹配,代码没有提供相应的检查和处理。

时间复杂度分析:

  • 初始的memmove操作时间复杂度为O(m),其中m是nums1中实际元素的数量。
  • 合并过程中的循环时间复杂度为O(m + n),其中m和n分别是nums1和nums2中实际元素的数量。
  • 最后的memmove操作时间复杂度为O(n)或O(m),取决于哪个数组的元素先被取完。
  • 因此,总体时间复杂度为O(m + n),这是合并两个数组的最优时间复杂度。

空间复杂度分析:

  • 代码没有使用额外的数组,所有的操作都在nums1数组上进行,因此空间复杂度为O(1),即常数级别的额外空间使用。

综上所述,这段代码在时间复杂度和空间复杂度上都表现出色,尤其是空间复杂度为常数级别,这在处理大规模数据时尤为重要。然而,为了提高代码的可读性和健壮性,可以考虑添加注释和错误处理逻辑。


哲学和编程思想:

这段代码及其实现方法体现了几个关键的哲学和编程思想,旨在提高代码效率、可读性和可维护性。下面是其中一些显著的思想:

  1. 就地操作(In-place Operation): 通过直接在输入数组nums1上进行操作,而不是创建一个新的数组来存储合并后的结果,这体现了就地操作的思想。就地操作减少了内存使用,改善了空间复杂度,是处理大规模数据时的一种重要策略。
  2. 双指针技术: 代码中使用了两个指针分别遍历两个数组,这是双指针技术的典型应用。双指针技术可以减少不必要的计算,提高算法的执行效率,是解决数组和链表问题的一个常见策略。
  3. 分而治之(Divide and Conquer): 虽然这个例子不是分而治之策略的典型应用,但通过将问题分解为更小的子问题(即将两个数组分别处理,然后合并),再将子问题的解合并起来解决整个问题,体现了分而治之的基本思想。
  4. 渐进增强(Progressive Enhancement): 通过首先移动nums1数组中的元素,为nums2数组中的元素腾出空间,然后根据元素大小顺序逐一填充,这种逐步解决问题的方法可以看作是渐进增强的思想在编程中的应用。
  5. 防御式编程(Defensive Programming): 通过检查nums2是否为空,代码遵循了防御式编程的一个原则,即预先检查输入值的有效性。虽然代码的这部分相对简单,但防御式编程是确保代码健壮性的重要策略。
  6. 最小惊讶原则(Principle of Least Surprise): 代码的行为与其设计目的一致——合并两个有序数组,而且通过就地操作避免了额外的空间开销。这种直观和高效的处理方式符合最小惊讶原则,即代码的行为应该符合使用者的直觉和期待。

举一反三

根据这个合并有序数组的方法,你可以将以下技巧和思想应用到其他编程问题中,以实现举一反三的效果:

  1. 双指针技术:
    • 在处理数组或链表问题时,如果问题涉及到两个序列的比较或合并,可以考虑使用双指针技术。例如,在解决“两数之和”、“三数之和”等问题时,可以使用双指针来遍历和比较元素。
    • 对于数组中的滑动窗口问题,可以使用双指针来维护窗口的边界。
  2. 就地操作:
    • 在设计算法时,尽量减少对额外空间的依赖。例如,在数组排序问题中,可以考虑使用就地排序算法,如快速排序、堆排序等。
    • 在处理字符串问题时,如果需要修改字符串内容,可以考虑使用就地操作,避免创建新的字符串对象。
  3. 分而治之:
    • 对于复杂问题,尝试将其分解为更小的、易于解决的子问题。例如,在解决“归并排序”、“快速排序”等排序问题时,可以将数组分解为更小的部分,分别排序后再合并。
    • 在处理树或图的问题时,可以采用递归的方式,将问题分解为子树或子图的问题。
  4. 渐进增强:
    • 在处理复杂问题时,可以先解决一个简化版本,然后逐步增加问题的复杂度。例如,在解决动态规划问题时,可以从最简单的子问题开始,逐步构建完整的解决方案。
    • 在设计软件功能时,可以先实现核心功能,然后逐步添加更多高级功能。
  5. 防御式编程:
    • 在编写代码时,始终考虑输入可能出现的异常情况,并进行相应的处理。例如,在处理用户输入时,应该检查输入的有效性,避免程序因无效输入而崩溃。
    • 在编写函数时,可以添加参数检查,确保输入参数满足函数的要求。
  6. 最小惊讶原则:
  • 在设计接口或函数时,确保其行为直观且符合用户的预期。例如,函数的命名应该清晰反映其功能,参数和返回值应该有明确的含义。
  • 在编写代码时,遵循一致的编码风格和命名规范,使代码易于阅读和理解。

通过将这些技巧和思想应用到不同的编程问题中,你可以提高解决问题的效率,同时编写出更加优雅和健壮的代码。记住,编程不仅仅是解决问题,更是创造性地应用各种技术和思想的过程。


逆向指针法

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
    // 初始化两个指针p1和p2,分别指向nums1和nums2的最后一个有效元素
    int p1 = m - 1, p2 = n - 1;
    // 初始化tail指针,指向nums1的最后一个位置,即合并后数组的末尾
    int tail = m + n - 1;
    // 定义一个变量cur,用于临时存储当前要放入nums1的元素
    int cur;
    // 进入循环,条件是至少有一个数组还有元素未处理
    while (p1 >= 0 || p2 >= 0) {
        // 如果p1已经到达了nums1的起始位置(即nums1的元素已经全部处理完毕)
        if (p1 == -1) {
            // 将nums2的当前元素赋值给cur,并移动p2指针
            cur = nums2[p2--];
        } 
        // 如果p2已经到达了nums2的起始位置(即nums2的元素已经全部处理完毕)
        else if (p2 == -1) {
            // 将nums1的当前元素赋值给cur,并移动p1指针
            cur = nums1[p1--];
        } 
        // 如果nums1的当前元素大于nums2的当前元素
        else if (nums1[p1] > nums2[p2]) {
            // 将nums1的当前元素赋值给cur,并移动p1指针
            cur = nums1[p1--];
        } 
        // 如果nums2的当前元素大于等于nums1的当前元素
        else {
            // 将nums2的当前元素赋值给cur,并移动p2指针
            cur = nums2[p2--];
        }
        // 将cur中的元素放入nums1的tail位置,并移动tail指针
        nums1[tail--] = cur;
    }
}

代码点评
 

这段代码实现了一个高效的合并两个有序数组的算法,其特点是从后向前合并,避免了额外的空间使用,并且保持了时间复杂度在O(m + n)。下面是对这段代码的专业点评:

代码结构与逻辑

  • 指针使用:代码中使用了三个指针(p1、p2、tail)来分别追踪nums1和nums2的当前位置以及合并后数组的末尾位置。这种指针的使用方式简洁高效,避免了不必要的数组遍历。
  • 循环条件:循环条件(p1 >= 0 || p2 >= 0)确保了只有当两个数组中至少有一个还有未处理的元素时,循环才会继续。这保证了所有元素都会被正确处理。
  • 分支逻辑:代码中的分支逻辑清晰地处理了不同的情况,包括一个数组已经处理完毕而另一个还未处理完毕的情况,以及两个数组都还有元素的情况。这种逻辑确保了合并操作的正确性。

时间复杂度

  • 时间复杂度:O(m + n)。代码中的循环会遍历两个数组中的所有元素,其中m和n分别是nums1和nums2的有效元素数量。因此,无论输入数组的大小如何,算法都会进行固定次数的操作,即遍历所有元素。

空间复杂度

  • 空间复杂度:O(1)。代码没有使用任何额外的数组或数据结构来存储元素,所有的操作都是就地进行的。唯一的额外空间是用于存储指针和临时变量的常数级别的空间,因此空间复杂度是常数级别的。

总结

这段代码是一个高效的合并两个有序数组的实现,它通过从后向前的合并策略,避免了额外的空间使用,并且保持了线性的时间复杂度。这种算法适用于需要合并大量数据但内存资源有限的情况,因为它不需要额外的空间来存储合并后的数组。此外,代码的逻辑清晰,易于理解和维护,是一个很好的编程实践。

本节内容到此结束!!感谢阅读!

  • 37
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

用哲学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值