在LeetCode做简单数组题的第1天

标签:#LeetCode #数组 #双指针 #数据结构 #刷题复盘
阅读时长:5分钟

前言

本人是一只学过数据结构但死活不理解咋用的绝望蘑菇,今日初到力扣,半小时做了第88题【合并两个有序数组】,发现了一个绝望的事实:课堂上学过双指针的思路,但是自己写的时候想不出,只能依赖AI
这篇随笔用于记录我的思考过程、踩坑、排错,总结我将如何把一个算法思路真正固化下来。

一、题目描述(合并两个有序数组)

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n。

测试示例1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]

测试示例2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]

二、蘑菇的简单思路(复制+排序)

看到题目,认真思考后的想法:把nums2的元素全部复制到nums1后面预留的0位置,之后直接对nums1排序。

代码:

class Solution:
    def merge(self, nums1: list[int], m: int, nums2: list[int], n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        """
        for i in range(n):
            nums1[m+i] = nums2[i]
        nums1.sort()

计算时间复杂度:

  • for循环:时间复杂度(O(n))
  • sort排序:Timsort算法,时间复杂度O((m+n)log(m+n))

备注:算法的时间复杂度取代码里最高阶的部分,所以整体复杂度是 O((m+n)log(m+n))。

三、本题的最优思路:从后往前双指针

思路推理过程

  1. 什么是数组有序:题目中,两个数组都是非递减有序,而结果返回的nums1数组也是非递减有序。非递减有序,就是数组的有效元素顺序递增,小数在头大数在尾,可参见示例1。
  2. 如何利用数组有序的特点:题目本质是,把两个递增数组(对nums1而言不太恰当,因为它尾部预留了全0空间),就较长的那个数组(nums1)去就地合并,得到一个新的递增数组(新nums1),那么,我们会在白纸上先圈出两个数组的有效位的尾部(即最大值)进行比较,选中较大的那个放到nums1的尾部,然后,划掉选中的大数,并往前一位去圈出本数组里的新大数,继续与另一个数组的大数比较,直到一个数组圈完为止。
  3. 用代码去理解刚才的思路:我们设置两个指针分别指向nums1、nums2的有效位的尾部(p1、p2),以及一个指向nums1实际尾部的指针(p3),循环条件为p1或p2大于等于0,在循环内部,设置if判断,条件为若nums1[p1]>nums[p2],则nums1[p3]等于nums1[p1],否则,nums1[p3]等于nums1[p2],并且每次循环,值更大的数的指针,p1或p2,必须减去1,让指针重新指向剩下的数组里那个新的最大值,同时,p3也要减去1,因为当前循环中指向的位置已经放入一个最大值,那下一次循环就必须把下一次的最大值放到这次指向的前一位,这样才能得到一个递增数组。
  4. 写代码第一步,设置3个指针变量:
    • p1:nums1有效元素的最后一位下标 m-1
    • p2:nums2有效元素的最后一位下标 n-1
    • p3:当前写入位置,合并完成数组的最后一位 m+n-1
  5. 主循环:只要p1和p2都大于等于0,就持续比较,把更大的值放到nums1[p3],对应指针左移,指针p3左移。
  6. 收尾循环:主循环结束之后,如果nums2还有剩下没有合并的元素该怎么办呢?应知剩下的元素不仅是最小的哪些,而且是递增的,可直接复制到nums1前头去。如果是nums1有剩余元素呢?因为我们要的就是合并后的nums1,那些数本身就在正确的原位,无需处理。

nums1之所以在尾部预留出全零空间,就是指引我们去想到从后往前这个转换。

五、我的bug(低级代码错误)

按照双指针的思路,我写出了代码,但是提交无法通过样例:

class Solution:
    def merge(self, nums1: list[int], m: int, nums2: list[int], n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        """
        p1 = m-1
        p2 = n-1
        p3 = m+n-1
        while p1 >= 0 and p2 >=0:
            if p1 > p2:  # ❗BUG在这里!
                nums1[p3] = nums1[p1]
                p1 -= 1
                p3 -= 1
            else:
                nums1[p3] = nums2[p2]
                p2 -= 1
                p3 -= 1
        while p2 >=0:
            nums1[p3] = nums2[p2]
            p2 -= 1
            p3 -= 1

问题分析

我错误地比较指针下标p1、p2本身的大小,而不是比较指针指向的数组元素的值 nums1[p1] 和 nums2[p2]。

用样例手动复现错误:
nums1 = [1,2,3,0,0,0],nums2 = [2,5,6]
初始 p1=2,p2=2,p3=5
第一轮判断 p1>p2 → 2>2 为false,取nums2[2]=6,数组变成[1,2,3,0,0,6]
第二轮p1=2,p2=1,p1>p2 → 2>1 为true,错误取nums1[2]=3,放到p3=4的位置。
数组变成[1,2,3,0,3,6],这一步就已经出错,后续全部结果跑偏。

bug总结:
p1只是数组下标编号;nums1[p1]才是下标对应的数组数值。
写双指针判断时,一定要问自己:我比较的是下标,还是下标对应的元素?

六、完整AC代码

class Solution:
    def merge(self, nums1: list[int], m: int, nums2: list[int], n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        """
        p1 = m - 1
        p2 = n - 1
        p3 = m + n - 1
        # 主循环,两个数组都还有元素时
        while p1 >= 0 and p2 >= 0:
            if nums1[p1] > nums2[p2]:
                nums1[p3] = nums1[p1]
                p1 -= 1
            else:
                nums1[p3] = nums2[p2]
                p2 -= 1
            p3 -= 1
        # 如果nums2还有剩余元素,继续拷贝
        while p2 >= 0:
            nums1[p3] = nums2[p2]
            p2 -= 1
            p3 -= 1

✅ 时间复杂度:(O(m+n)),只遍历两个数组一遍
✅ 空间复杂度:(O(1)),原地修改,不额外开辟数组

样例手动推演:
nums1 = [1,2,3,0,0,0], nums2=[2,5,6]
p1=2(val=3), p2=2(val=6), p3=5

  1. 3<6 → nums1[5]=6,p2=1,p3=4
  2. 3<5 → nums1[4]=5,p2=0,p3=3
  3. 3>2 → nums1[3]=3,p1=1,p3=2
  4. 2==2 → nums1[2]=2,p2=-1,p3=1
    主循环结束,p2=-1,第二个while不执行
    最终nums1 = [1,2,2,3,5,6],符合预期。

七、如何记住一个算法思路?(本次学习最大收获)

误区:看懂代码 ≠ 能独立写出代码。看懂是跟着别人的思路走;独立写,需要自己完成完整推理。

  1. 记住场景
    两个有序数组,原地合并到第一个数组,第一个数组尾部预留空位 → 优先思考从后往前双指针。

  2. 算法的推理链条
    从前向后合并会覆盖原有数据 → 改为尾部写入 → 每次选最大值放到末尾 → 设置3个指针 → 主循环合并 → 处理nums2剩余元素。

记住「为什么要反向遍历」,而不是记住有3个指针。

  1. 纸笔模拟优先于写代码
    拿到题目,先拿草稿纸画数组方框,标注指针,手动模拟2轮完整流程,再动手写代码。指针类题目,画图能规避绝大多数下标bug。

  2. 分模块写代码
    分步写:初始化指针 → 主循环 → 收尾循环。写完一块验证一块。

  3. 变形自测检验是否真理解
    做完原题给自己提问:

  • 如果数组是非递增有序,思路怎么变?
  • 如果nums1没有预留空位,不能原地修改怎么办?
  • 如果nums2为空数组,代码是否正常运行?
    灵活变通,而不是背题。
  1. 间隔重复复习
    把学习记录成一篇博客,是为了每天有空扒出来读读画画,不能指望蘑菇脑袋进化到一夜之间内化任何哪怕简单的算法。

八、总结

第一次刷题给我的感受:想学算法就不能不动手,否则get不到适用场景,没学过数据结构也不行,因为如果当年没学,我还得翻书查什么是时间复杂度。
双指针这道题,在于场景分析和方向转换,还有写代码时细心区分下标和数组元素。
刷题的价值,不在于AC,而在于发现自己思维的盲区、手写代码的低级失误,把思路真正内化(祝见过了记住了的总有用上的一日)。

posted @ 2026-10-20 23:55  绝望蘑菇头  阅读(4)  评论(0)    收藏  举报