【算法日记】双指针法元素移除与有序合并

一、原地移除数组中的指定元素(LC27)

问题描述

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k
  • 示例 1:
    输入:nums = [3,2,2,3], val = 3
    输出:2, nums = [2,2,_,_]
    解释: 你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
    你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

  • 示例 2:
    输入:nums = [0,1,2,2,3,0,4,2], val = 2
    输出:5, nums = [0,1,4,0,3,_,_,_]
    解释: 你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
    注意这五个元素可以任意顺序返回。
    你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

解题思路

这道题的关键在于"原地"操作,也就是说我们不能使用额外的数组空间,必须在原数组上进行修改。

最直接的想法是遍历数组,当遇到等于 val 的元素时就将其删除,但数组的删除操作其实是通过后面的元素向前移动来实现的,这样会导致时间复杂度较高。

更高效的方法是使用双指针技术

  1. 创建两个变量分别是scr = 0 dst = 0,作为下标
  2. 如果src 指向的值为valsrc++
  3. 如果src指向的值不是val则将src指向的值赋值给dst指向的值,src++,dst++
  4. 遍历结束后, dst 的值就是不等于 val 的元素的数量
    在这里插入图片描述

代码实现

int removeElement(int* nums, int numsSize, int val) {
    int src = 0;
    int dst = 0;
    while (src != numsSize)
    {
        if (nums[src] != val)
        {
            nums[dst] = nums[src];
            src++;
            dst++;
        }
        else
        {
            src++;
        }
    }
    return dst;
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组的长度。我们只需要遍历一次数组。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

这种方法的优点是高效且简洁,通过一次遍历就完成了所有操作,并且不需要额外的空间。

二、合并两个有序数组(LC88)

问题描述

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

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

注意: 最终,合并后数组不应由函数返回,而是存储在数组 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]
解释: 需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中加粗标注的为 nums1 中的元素。

示例 2:
输入: nums1 = [1], m = 1, nums2 = [], n = 0
输出: [1]
解释: 需要合并 [1] []
合并结果是[1]

示例 3:
输入: nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释: 需要合并的数组是 [][1]
合并结果是 [1]
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保.合并结果可以顺利存放到 nums1 中。

提示:

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • -109 <= nums1[i], nums2[j] <= 109

解题思路

这道题要求我们合并两个有序数组,并且要将结果存储在第一个数组中。

如果我们从前往后合并,可能会覆盖 nums1 中还未处理的元素,这就需要额外的空间来保存这些元素。

更好的方法是从后往前合并

  1. 定义三个指针,i 指向 nums1 有效元素的末尾(m-1),j 指向 nums2 的末尾(n-1),k 指向合并后数组的末尾(m+n-1)
  2. 比较 nums1[i]nums2[j] 的大小,将较大的元素放到 nums1[k] 的位置,然后相应地移动指针
  3. 当其中一个数组的元素全部处理完毕后,将另一个数组中剩余的元素复制到 nums1 的前面
    在这里插入图片描述
    在这里插入图片描述

注意:i < 0j < 0会跳出循环。

  • j < 0,说明num2已经全部移入num1数组,而num1数组本身就是有序的,此时整个数组就是有序的状态,不需要再处理
    在这里插入图片描述

  • i < 0跳出循环,只需要依次把num2中剩余元素移入num1
    在这里插入图片描述
    在这里插入图片描述

代码实现

void merge(int* nums1, int m, int* nums2, int n) {
    int i = m - 1 ;
    int j = n - 1;
    int k = m + n - 1;
    while(i>=0&&j>=0)
    {
        if(nums1[i]>=nums2[j])
        {
            nums1[k]=nums1[i];
            i--;
            k--;
        }
        else
        {
            nums1[k]=nums2[j];
            j--;
            k--;
        }
    }

    while(j>=0)
    {
        nums1[k]=nums2[j];
        k--;
        j--;
    }
}

复杂度分析

  • 时间复杂度:O(m + n),需要遍历两个数组的所有元素
  • 空间复杂度:O(1),只使用了常数级别的额外空间

这种从后往前合并的方法非常巧妙,既利用了两个数组已经有序的特点,又避免了使用额外的数组空间,完美符合题目的要求。

总结

这两道数组操作题体现了算法设计中的一些重要思想:

  1. 双指针技术是处理数组问题的常用技巧,能够在不使用额外空间的情况下高效地完成操作
  2. 逆向思维在合并有序数组问题中起到了关键作用,从后往前操作避免了元素覆盖的问题
  3. 原地操作的要求促使我们思考更高效的空间利用方式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值