文章目录
- 0. 前言
- 1. 移除元素
- 2. 删除有序数组中的重复项
- 3. 合并两个有序数组
- 4. 结语
0. 前言
在上篇博客中,我们使用C语言实现了顺序表。其中我们也对顺序表的接口进行了完整的实现。但是光实现不够,还是需要题目来练习。于是今天我就为大家带来顺序表的三道OJ题。话不多说,我们这就开始。
1. 移除元素
链接:27. 移除元素
描述:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
思路1:
循环遍历nums数组,当碰到val时,将所有元素向前移动,覆盖掉这个下标的值,numsSize–。
注意点:当连续的两个元素都为val时,如果删除了一个元素,后一个元素依然会删除,但是下标已经越过了这个位置,所以需要回退,然后重新判断是否删除该元素。
最后返回numsSize(数组长度)即可。
当数组中元素大多数元素为val时,为时间复杂度最坏情况。
时间复杂度:O(N^2) 空间复杂度:O(1)
但是这种方法时间复杂度太高了,能不能优化到O(N)?看思路2 ↓
思路2:
这个思路就很简单了。额外创建一个数组,把不是val的值放入数组中,最后把元素移动到原数组中,返回新数组的长度。
注意点:当数组为空时,需要额外判断。
能否在不降低时间复杂度的情况下,将时间复杂度优化到O(1)?看思路3↓
思路3(精讲):
题目要求使用O(1)的额外空间,原地修改数组,那么我们肯定是尽量降低时间复杂度,并且不额外开辟数组。
所以,我们使用双指针法:
以给定两个下标:dest
、src
分别从0下标开始。
src遍历数组。若src碰到不等于val的值,就将src对应下标的值,放到原数组中dest下标对应的位置。然后dest++,src++。
如果src碰到等于val的值,就将src++,dest不动。
当src = numsSize时,说明数组已经遍历过一遍了,而元素也已经成功移除。
最后返回dest,就是我数组的长度。
时间复杂度:O(N) 空间复杂度:O(1)
2. 删除有序数组中的重复项
链接:26. 删除有序数组中的重复项
描述:
给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。
将最终结果插入 nums 的前 k 个位置后返回 k 。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums
已按 升序 排列
思路:
本题依旧是需要原地修改数组,所以我们采用三指针法。
说是三指针,其实也就是三个下标。
给定从0开始的下标i、dest,从1开始的下标j。
j用来遍历数组,i则是用来划定重复数字的区域。为什么这么说?接下来我们讲解i和j的分工后,大家就可以理解这个意思。
在数组升序的前提下,j下标在遍历数组时,会遇到两种情况:
(1) 由于第一个数字一定不是重复项,所以j从下标1开始。如果j遇到的数字和下标i所对应的数字相同,那么j++,继续向后走。
// 对应代码段
if (nums[i] == nums[j])
{
j++;
}
(2) 一旦j碰到的元素和i不相等,那么就将dest下标对应的位置填上i下标的值。并且dest++。重点来了,此时就说明j现在所在位置就是一组重复数的下一个位置。那么此时就让i = j,让i到新的区域内,然后让j++,让j继续走,直到找到新边界。
// 对应代码段
if (nums[i] != nums[j])
{
nums[dest++] = nums[i];
i = j;
j++;
}
以上两种情况循环进行,直到j == numsSize停止。
但是请注意,对于最后一个数据来说,此时j++的话,那么j就走到了数组后面,最后一个元素没有放置,但是这时我的 i 对应的元素就是最后一个元素,所以需要处理一下:nums[dest++] = nums[i]
。
最后返回dest,就是不含重复项数组的大小。
时间复杂度:O(N) 空间复杂度:O(1)
3. 合并两个有序数组
链接:88. 合并两个有序数组
描述:
给你两个按 非递减顺序 排列的整数数组 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
- -10^9<= nums1[i], nums2[j] <= 10^9
思路1:
另外创建一个数组,分别遍历两个数组。比较两个数组对应下标的元素,将较小的数组放入新数组中,直到一个数组元素放置完毕停止。由于数组是有序的,于是将未放置完毕的数组的数据放置到新数组中。最后拷贝到原数组中。
时间复杂度:O(M + N) 空间复杂度:O(M + N)
由于代码过长,再贴一份~
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
// 额外创建一个数组
int* newArr = (int*)malloc((m + n) * sizeof(int));
int start1 = 0, start2 = 0;
int count = 0;
while (start1 < m && start2 < n)
{
if (nums1[start1] < nums2[start2])
{
newArr[count++] = nums1[start1++];
}
else
{
newArr[count++] = nums2[start2++];
}
}
// 将未放置的数据放入新数组中
if (start1 == m)
{
for ( ; start2 < n; start2++)
{
newArr[count++] = nums2[start2];
}
}
else
{
for ( ; start1 < m; start1++)
{
newArr[count++] = nums1[start1];
}
}
// 拷贝回原数组
for (int i = 0; i < count; i++)
{
nums1[i] = newArr[i];
}
}
能否在不改变时间复杂度的情况下,将空间复杂度优化到O(1)?看思路2↓
思路2(精讲):
这道题目其实有一个坑,m不是nums1的长度,而是有效数据的长度。n既是nums2的有效数据的长度,也是nums2的长度。nums1的总长度为m + n。
所以我们便可以使用如下方法:
给定end1、end2下标,分别对应着nums1、nums2数组有效数据的末尾位置。给定end下标,为nums1数组的末尾。
首先明确一点,数组是升序的,之后我们的步骤才能正确执行。
如果end1下标对应元素大于end2下标对应元素,则把end1下标对应元素放到end下标处,然后end1- -,end- -;如果end1下标对应元素小于等于end2下标对应元素,则把end2下标对应元素放到end下标处,end2- -,end- -。直到end1或end2中一个值小于0。
如果nums2元素没有放置完毕(end2 >= 0),则将nums2中剩余元素,按照原本顺序依次放入nums1数组;如果nums1数组没有放置完毕,则无需处理,因为这些元素本来就在nums1数组中。
时间复杂度:O(M + N) 空间复杂度:O(1)
4. 结语
到这里,三道题目就讲解完成了。本篇博客也就到此结束了。
如果有兴趣的小伙伴也可以去刷题练练手。毕竟看懂了并不代表会写了,还得多写多练嘛。
下篇博客我会为大家带来链表的相关知识,我们敬请期待~
如果觉得anduin写的还不错的话,还请一键三连!如有错误,还请指正!
我是anduin,一名C语言初学者,我们下期见!