一、原地移除数组中的指定元素(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
的元素时就将其删除,但数组的删除操作其实是通过后面的元素向前移动来实现的,这样会导致时间复杂度较高。
更高效的方法是使用双指针技术:
- 创建两个变量分别是
scr = 0
dst = 0
,作为下标 - 如果
src
指向的值为val
则src++
- 如果
src
指向的值不是val
则将src
指向的值赋值给dst
指向的值,src++
,dst++
- 遍历结束后,
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)
问题描述
给你两个按 非递减顺序 排列的整数数组
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]
解释: 需要合并 [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
中还未处理的元素,这就需要额外的空间来保存这些元素。
更好的方法是从后往前合并:
- 定义三个指针,
i
指向nums1
有效元素的末尾(m-1),j
指向nums2
的末尾(n-1),k
指向合并后数组的末尾(m+n-1) - 比较
nums1[i]
和nums2[j]
的大小,将较大的元素放到nums1[k]
的位置,然后相应地移动指针 - 当其中一个数组的元素全部处理完毕后,将另一个数组中剩余的元素复制到 nums1 的前面
注意: 当i < 0
或j < 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),只使用了常数级别的额外空间
这种从后往前合并的方法非常巧妙,既利用了两个数组已经有序的特点,又避免了使用额外的数组空间,完美符合题目的要求。
总结
这两道数组操作题体现了算法设计中的一些重要思想:
- 双指针技术是处理数组问题的常用技巧,能够在不使用额外空间的情况下高效地完成操作
- 逆向思维在合并有序数组问题中起到了关键作用,从后往前操作避免了元素覆盖的问题
- 原地操作的要求促使我们思考更高效的空间利用方式