元素删除
通过元素移动的方式来模拟删除操作:将指定下标后的所有元素依次向前移动一位,覆盖要删除的元素,从而达到 "删除" 的效果。
- 通过自定义函数实现删除功能,需要传入数组、数组长度的指针(因为要修改长度)和要删除的下标
- 先判断下标是否合法(是否在有效范围内)
- 核心操作是通过循环将指定下标后的所有元素向前移动一位
- 最后将数组长度减 1(实际数组内存未变,但逻辑长度减小)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
typedef int Data;void select(Data arr[], int length, int pos)
{assert(pos >= 0 && pos < length);//检查下标是否合法for (int i = pos; i <length-1; i++){arr[i] = arr[i+1];}length--;//将要删除位置后数据向前挪动一位}int main()
{int arr[10] = { 23,6,7,5,4,6878,4,423,4,3 };int length = sizeof(arr) / sizeof(arr[0]);printf("请输入:");int n = 0;scanf("%d", &n);select(arr, length, n);for (int i = 0; i < 9; i++){printf("%d ", arr[i]);}return 0;
}
移除元素(力扣)
三个思路
双指针法
int removeElement(int* nums, int numsSize, int val) {//定义两个变量int dst=0,src=0;while(src<numsSize){if(nums[src]!=val){nums[dst]=nums[src];dst++;}src++;}return dst;
}
src在前面探路,dst负责保存数据
src
相当于 “源指针”:负责遍历整个数组,逐个检查元素是否需要保留(nums[src] != val
)。dst
相当于 “目标指针”:负责记录需要保留的元素应该存放的位置,只有当src
指向的元素需要保留时,dst
才会移动(dst++
)。
时间复杂度:o(n) 空间复杂度:o(1)
删除有序数组中重复项
int removeDuplicates(int* nums, int numsSize) {//创建两个变量int dst=0,src=dst+1;while(src<numsSize){//判断dst和src值if(nums[src]!=nums[dst]&&++dst!=src){nums[dst]=nums[src];}src++;}
return dst+1;
}
++dst!=src的用意(自我赋值):
假设数组中存在连续的不重复元素,例如:nums = [1,2,3]
- 初始状态:
dst=0
,src=1
- 第一步:
nums[src]=2 != nums[dst]=1
,满足第一个条件,执行++dst
(此时dst=1
)。
此时dst=1
恰好等于src=1
(因为src
只比初始dst
大 1,dst
自增后两者重合)。
如果没有++dst != src
的判断,就会执行nums[dst] = nums[src]
,也就是nums[1] = nums[1]
—— 这就是无意义的自我赋值(赋值前后元素没有任何变化)。 - 当
src
和dst
相邻(src = dst + 1
)且元素不同时,++dst
后两者会指向同一个位置,此时的赋值操作完全多余。++dst != src
正是通过判断两者是否重合,精准避免了这种无意义的操作,让算法更高效、逻辑更严谨。
如何避免无意义自我赋值
1.先判断,在操作,避免无效计算
比如处理字符串拼接
// 不好的写法:无论str是否为空都执行拼接
char* appendStr(char* result, const char* str) {strcat(result, str); // 如果str是空字符串,这是无意义的操作return result;
}// 优化写法:先判断是否需要拼接
char* appendStr(char* result, const char* str) {if (str != NULL && str[0] != '\0') { // 非空字符串才拼接strcat(result, str);}return result;
}
2.指针判空在解引用,避免无效访问
// 不好的写法:可能对NULL指针解引用
void printValue(int* ptr) {printf("%d\n", *ptr); // 如果ptr是NULL,会导致程序崩溃
}// 优化写法:先判空
void printValue(int* ptr) {if (ptr != NULL) { // 指针有效才访问printf("%d\n", *ptr);} else {printf("指针为空\n");}
}
3.避免重复计算,缓存中间结果
// 不好的写法:重复计算同一个值
int calculateSum(int* arr, int size) {int sum = 0;for (int i = 0; i < size; i++) {sum += arr[i] * (arr[i] + 1) / 2; // 每次循环都计算arr[i]的二次项}return sum;
}// 优化写法:缓存中间结果
int calculateSum(int* arr, int size) {int sum = 0;for (int i = 0; i < size; i++) {int val = arr[i]; // 缓存当前值,避免重复读取arr[i]sum += val * (val + 1) / 2;}return sum;
}
4.条件判断顺序优化,减少分支执行
让更可能成立的条件先执行,减少不必要判断
// 假设场景:90%的情况是a==0,10%是b==0
// 不好的写法:先判断低概率条件
if (b == 0) {handleB();
} else if (a == 0) {handleA();
}// 优化写法:先判断高概率条件
if (a == 0) {handleA();
} else if (b == 0) {handleB();
}
5.容器操作前先检查是否为空
// 不好的写法:对空数组执行遍历
void clearArray(int* arr, int size) {for (int i = 0; i < size; i++) { // 如果size==0,循环仍会执行(虽然不进入)arr[i] = 0;}
}// 优化写法:先判断容器是否为空
void clearArray(int* arr, int size) {if (size <= 0 || arr == NULL) { // 空数组直接返回return;}for (int i = 0; i < size; i++) {arr[i] = 0;}
}
6. 避免重复调用耗时函数
对于返回值不变的耗时函数(如获取系统时间、读取配置),避免在循环中重复调用。
// 不好的写法:循环中重复调用耗时函数
void processData(int* data, int size) {for (int i = 0; i < size; i++) {long timestamp = getCurrentTimestamp(); // 假设这是一个耗时函数data[i] += timestamp;}
}// 优化写法:外部调用一次,复用结果
void processData(int* data, int size) {long timestamp = getCurrentTimestamp(); // 只调用一次for (int i = 0; i < size; i++) {data[i] += timestamp;}
}
合并两个有序数组(力扣)
思路 1:先合并,在排序,(冒泡排序)时间复杂度为o(n^2)
思路 2:空间换时间 创建一个新数组,比大小
思路 3:从后往前比较
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)//l1先越界,来还未全部放入nums1{nums1[l3--]=nums2[l2--];}
}
最后一步很关键