这篇博文向大家介绍几道最近刚学的基于顺序表的算法题,因为看完讲解后我直呼秒啊~,所以想赶快和大家分享!
文章目录
1、移除元素
先看题目描述,再来讲解解题思路:
题目要求我们原地 移除数组中所有等于 val 的元素,并返回移除后数组的新长度。所谓「原地」,就是不能额外开辟一块和原数组等大的空间来存放结果,只能在原数组的存储空间上做文章;同时题目还特别说明,元素的顺序可以改变,不需要保持原有相对顺序,返回的新长度之后的元素是什么内容也无所谓。这一点很关键,它意味着我们不必像删除顺序表中间元素那样老老实实地把后面所有数据整体前移,从而为后面的优化留出了空间。
下面给出三种思路,其中思路一、思路二供参考:思路一的算法复杂度较高,思路二虽然快但不符合「原地移除」的题目条件,所以重点看思路三的双指针解法。


c
int removeElement(int* nums, int numsSize, int val) {
int i = 0;
while(i < numsSize)
{
if(nums[i] == val)
{
for(int j = i; j < numsSize-1;j++)
{
nums[j] = nums[j+1];
}
numsSize--;
}
else
{
i++;
}
}
return numsSize;
}
思路二:创建新数组,新数组的空间大小和原数组的一样大,遍历原数组,将值不等于val的数据放到新数组中去,最后将新数组中的数据拷贝回原数组。
具体来说,用一个下标 j 记录新数组中已经存放了多少个有效元素,遍历原数组时,只要当前元素不等于 val,就把它依次放进新数组的第 j 个位置,然后 j 自增;遍历结束后,j 恰好就是保留下来的元素个数,再把新数组前 j 个元素逐个拷贝回原数组即可。
这个方法用空间换取了时间,时间复杂度为O(n),空间复杂度为O(n)。相比思路一,它把「边找边挪」的双层循环压缩成了一趟遍历,效率明显提升;但代价是多开了一块和原数组等大的辅助空间,当数组规模很大时,这份额外开销并不划算。
c
int removeElement(int* nums, int numsSize, int val) {
int newArr[numsSize];
int j = 0;
for(int i = 0; i < numsSize; i++)
{
if(nums[i] != val)
{
newArr[j++] = nums[i];
}
}
for(int i = 0; i < j; i++)
{
nums[i] = newArr[i];
}
return j;
}
由于题目要求原地移除,所以以上解法不符合题目要求。
思路三:这个思路就略显巧妙了,只需定义两个临时变量,就可以获得O(n)的时间复杂度和O(1)的空间复杂度 。我们称第三种方法为双指针法。
首先,创建两个变量dst和src(dst == destination,src == source,一个是源,一个是目的,在有些情况下需要将源位置的数据赋值给目的位置,所以这样取名。),都初始化为0,"指向"下标为0的位置。
然后开始比较:拿src位置的数据同val进行比较。
- 如果相等,说明这个元素是要被移除的,直接让src往后走跳过它,对应于代码:
if(nums[src] == val) src++; - 如果不相等,说明这个元素需要保留,就把src位置的数据赋值给dst位置,然后两个指针同时往后走,对应于代码:
if(nums[src] != val) { nums[dst] = nums[src]; dst++; src++; }
这里有一个容易忽略的细节:当 dst 和 src 指向同一个位置时,nums[dst] = nums[src] 相当于自己赋值给自己,虽然结果没错,但属于多余操作。不过这种「多余」换来的是代码逻辑的统一------不用额外判断两个指针是否重合,可读性更好,实际运行中这点开销也可以忽略不计。
双指针法,src(快指针)在前面探路,从头到尾遍历数据,负责扫描每一个元素 ;dst(慢指针)标记下一个合法元素应该存放的位置。最后dst的值正好是数组中有效元素的个数。
换个角度理解:src 走过的区域是「已经检查过」的部分,dst 走过的区域是「已经整理好」的部分,而 dst 永远追不上 src,所以把 src 处的元素写到 dst 处时,绝不会覆盖掉还没检查的数据。整个过程中每个元素最多被访问一次、被写入一次,因此时间复杂度为 O(n) ;又因为只用了两个整型变量,没有开辟额外数组,空间复杂度为 O(1),完美满足题目「原地移除」的要求。
具体实现:
c
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];
dst++;
}
src++;
}
return dst;
}

思路三用for循环是这样实现的,把 src 的自增交给 for 循环的第三部分,代码会更紧凑一些:
c
int removeElement(int* nums, int numsSize, int val) {
int src = 0;
int dst = 0;
for(src = 0; src < numsSize; src++)
{
if(nums[src] != val)
{
nums[dst] = nums[src];
dst++;
}
}
return dst;
}
前面介绍了双指针法,接下来再来看两道可以利用双指针法解答的题目。
2、删除有序数组中的重复项
先看题目描述,再来讲解解题思路:


思路:创建两个变量src和dst,分别"指向"起始位置和下一个位置(这两个变量其实是用作数组的下标,并非指针)。当下标为src的元素等于下标为dst的元素时,src往前走,对应于代码if(nums[src] == nums[dst] src++;);当下标为src的元素不等于下标为dst的元素时,dst往前走,再把下标为src的元素赋值给下标为dst的元素,让src往前走,对应于代码if(nums[src] != nums[dst]) { dst++; nums[dst] = nums[src]; src++; }。
最后有效元素个数就是dst+1。
c
int removeDuplicates(int* nums, int numsSize) {
int dst = 0;
int src = dst + 1;
while(src < numsSize)
{
if(nums[src] != nums[dst])
{
dst++;
nums[dst] = nums[src];
}
src++;
}
return dst + 1;
}
在以上代码中,当下标为src的元素不等于下标为dst的元素时,如果每次dst走到src的位置,再将下标为src的元素赋值给下标为dst的元素,很多余(此时等同于自己赋值给自己),所以需要将代码优化一下:
c
int removeDuplicates(int* nums, int numsSize) {
int dst = 0;
int src = dst + 1;
while(src < numsSize)
{
if(nums[src] != nums[dst] && ++dst != src)
{
nums[dst] = nums[src];
}
src++;
}
return dst + 1;
}
3、合并两个有序数组
先看题目描述,再来讲解解题思路:


思路一:先将两个数组合并,再利用排序算法进行排序。(冒泡排序的时间复杂度为O(n²))(不推荐)
c
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int k = 0;
while(k < n)
{
nums1[m++] = nums2[k++];
}
for(int i = 0; i < m - 1; i++)
{
for(int j = 0; j < m - i - 1; j++)
{
if(nums1[j] > nums1[j+1])
{
int tmp = nums1[j];
nums1[j] = nums1[j+1];
nums1[j+1] = tmp;
}
}
}
思路二:创建新数组,空间大小和nums1一致,遍历两个原数组中的数据比较大小,并按顺序放到新数组中,最后拷贝回nums1数组中。(时间复杂度为O(m+n),空间复杂度为O(m+n)。不推荐)
c
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int newArr[nums1Size];
int i = 0, j = 0, k = 0;
while(i < m && j < n)
{
if(nums1[i] < nums2[j])
{
newArr[k++]=nums1[i++];
}
else
{
newArr[k++]=nums2[j++];
}
}
while(i < m)
{
newArr[k++]=nums1[i++];
}
while(j < n)
{
newArr[k++]=nums2[j++];
}
for(int i = 0; i < k; i++)
{
nums1[i] = newArr[i];
}
}
思路三:定义三个整型变量,都表示下标。l1"指向"num1中的最后一个元素,l2"指向"nums2中的最后一个元素,nums3"指向"nums1空间的最后一个位置。然后比较l1对应的空间元素和l2对应的空间元素的大小,谁大谁就往l3的位置上放,然后两个下标同时减一。实际上就是从后往前比较大小,大的往后放。
最后处理结束时的情形,l1和l2必定有一个先小于0,此时退出while循环。当l2先小于0时,l1下标对应的元素及其前面的元素也已经是有序的,所以无需其他操作;当l1先小于0时,需要把l2"指向"元素及其之前的元素移动到num1中l3指向的位置。
c
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int l1 = m -1;
int l2 = n - 1;
int l3 = m + n - 1;
while(l1 >= 0 && l2 >= 0)
{
if(nums1[l1] > nums2[l2])
{
nums1[l3--] = nums1[l1--];
}
else
{
nums1[l3--] = nums2[l2--];
}
}
while(l2 >= 0)
{
nums1[l3--] = nums2[l2--];
}
妙啊~~吸吸智慧!