文章目录
-
- [1、27 移除元素](#1、27 移除元素)
- [2、26 删除有序数组中的重复项](#2、26 删除有序数组中的重复项)
-
- 优化点
- 如何形象地理解鲁棒性?
- 在编程中,鲁棒性意味着什么?
-
- [1. 处理异常输入](#1. 处理异常输入)
- [2. 应对极端边界](#2. 应对极端边界)
- [3. 容错能力](#3. 容错能力)
- 为什么鲁棒性很重要?
- 总结
- [3、283 移动零](#3、283 移动零)
- [4、844 比较含退格的字符串](#4、844 比较含退格的字符串)
- [5、997 有序数组的平方](#5、997 有序数组的平方)
-
- [1. 观察切入点:寻找"单调性"](#1. 观察切入点:寻找“单调性”)
- [2. 核心原理:对撞指针(Two Pointers)](#2. 核心原理:对撞指针(Two Pointers))
- [3. 思路推演:从结果倒推](#3. 思路推演:从结果倒推)
- [4. 为什么这样做最优?](#4. 为什么这样做最优?)
- [5. 避坑指南(面试常问)](#5. 避坑指南(面试常问))
- 杂记
-
- [1. 核心快捷键](#1. 核心快捷键)
- [2. 其他常用的"规范化"小技巧](#2. 其他常用的“规范化”小技巧)
- [3. 如何在界面上找到它?](#3. 如何在界面上找到它?)
- [4. 为什么代码规范很重要?](#4. 为什么代码规范很重要?)
本系列不出意外的话会按照代码随想录的题单进行刷题~
xixi 复试加油~一志愿上岸!!!
1、27 移除元素
题目

代码:
阴差阳错属于双指针法了(快慢指针)
- i 充当了快指针:遍历原数组。
- count 充当了慢指针:指向新数组(去除了 val 后)的下一个存放位置。
c
int removeElement(int* nums, int numsSize, int val) {
int i=0;
int count=0;
for(i=0;i<numsSize;i++){
if(nums[i] != val){
nums[count] = nums[i];
count++;
}
else{
continue;
}
}
return count;
}
时间复杂度:O(n)
空间复杂度:O(1)
可以优化的地方
- 简洁性优化
else { continue; } 是多余的。在 for 循环中,如果没有命中 if,它自然会进入下一次循环。去掉它可以让代码更清爽。
c
int removeElement(int* nums, int numsSize, int val) {
int count=0;
for(int i = 0;i < numsSize;i++){
if(nums[i] != val){
nums[count++] = nums[i];
}
}
return count;
}
- 双指针从两端移动
现在的做法(快慢指针)在某些情况下会有冗余操作。
**假设场景:**数组是 [1, 2, 3, 4, 5],你要删除 val = 1。
- 你的代码会把 2, 3, 4, 5 这四个数全都向左移动一位。
- 优化思路:如果 val 在数组开头,而数组很长,与其移动后面所有的元素,不如直接把最后一个元素覆盖到开头。
这种方法叫 "双指针优化(交换/覆盖法)",特别适用于 "要删除的元素很少" 的情况.
c
int removeElement(int* nums, int numsSize, int val) {
int left = 0,right = numsSize-1;
while(left <= right){
if(nums[left] == val){
nums[left] = nums[right];
right--;
}
else{
left++;
}
}
return left;
}
2、26 删除有序数组中的重复项
题目:

代码:
c
int removeDuplicates(int* nums, int numsSize) {
int left = 0;
for(int right = 1;right<numsSize;right++){
if(nums[left] != nums[right]){
left++;
nums[left] = nums[right];
}
}
return left+1;
}
时间复杂度:O(n)
空间复杂度:O(1)
优化点
减少"自己给自己赋值"
虽然你的代码是对的,但考虑一个极端情况:如果数组里完全没有重复项(例如 [1, 2, 3, 4, 5])。
- 当前逻辑:当 right = 1 时,nums[0] != nums[1],执行 nums[1] = nums[1]。
- 改进点:在赋值前增加一个判断 if (right - left > 1)。只有当两个指针之间有间距(即跳过了一些重复项)时,才执行赋值。
代码:
c
int removeDuplicates(int* nums, int numsSize) {
if(numsSize == 0) return 0;//增加空数组检查,增加鲁棒性
int left = 0;
for(int right = 1;right < numsSize;right++){
if(nums[left] != nums[right]){
left++;
if(right > left){//只有指针不相邻时才移动数据
nums[left] = nums[right];
}
}
}
return left + 1;
}
- 深度思考:通用模板掌握了这一题,其实你已经可以解决一类问题了。比如这道题的进阶版:LeetCode 第 80 题(删除有序数组中的重复项 II)。要求:每个元素最多出现 两次。解题套路(针对"最多保留 k 个重复项"):只要把判断条件改一下即可:if (nums[right] != nums[left - (k-1)])对于本题( k = 1 k=1 k=1),其实逻辑就是 nums[right] != nums[left]。
何为鲁棒性
gimini老师的解答:简单来说,"鲁棒性" 是英文 Robustness 的音译。在计算机和工程领域,它的核心含义是系统的健壮性 或强健性。
如果一个系统具有很强的鲁棒性,就说明它**"抗造"、"皮实"**。即使面对意外情况、错误的输入或者恶劣的环境,它也能保持运行,而不会轻易崩溃。
如何形象地理解鲁棒性?
我们可以用几个生活中的例子来对比:
-
玻璃杯 vs. 不锈钢杯:
-
玻璃杯掉在地上会碎(鲁棒性低)。
-
不锈钢杯掉在地上可能只是一点凹痕,依然能装水(鲁棒性高)。
-
普通人 vs. 运动员:
-
普通人可能吹点凉风就感冒了。
-
运动员在严寒酷暑中依然能保持高水平竞技状态,这说明运动员的身体系统更"鲁棒"。
在编程中,鲁棒性意味着什么?
回到你写的代码,一个"鲁棒性高"的程序通常能处理以下三种情况:
1. 处理异常输入
比如一个计算平方根的函数,如果用户输入了负数、字母或者空值,程序是直接蓝屏崩溃,还是能优雅地报错提示"输入无效"?
- 不鲁棒:直接死机。
- 鲁棒 :通过
if (x < 0)判断并给回反馈。
2. 应对极端边界
正如你刚才写的 removeDuplicates 函数:
c
if (numsSize == 0) return 0; // 这就是为了增加鲁棒性
如果我不传数组(长度为 0),没有这行代码,你的 for 循环或访问 nums[0] 可能就会导致内存非法访问。加上这一行,你的代码就变得更"鲁棒"了。
3. 容错能力
在分布式系统或网络程序中,如果网络突然断了一秒钟,鲁棒的系统会自动重连,而不是直接退出程序。
为什么鲁棒性很重要?
在实际开发中,"墨菲定律" 总是起作用的:如果一个错误可能发生,它就一定会发生。
- 面试中 :面试官非常看重代码的鲁棒性。他们不仅看你能不能写出核心算法,还看你有没有考虑到
null指针、数组越界、极大值溢出等边界情况。 - 工程中:鲁棒性差的软件意味着频繁的宕机和高昂的维护成本。
总结
鲁棒性 = 稳定性 + 容错能力。
以后在刷题时,你可以多问自己一句话:"如果输入的数据非常奇怪(很大、很小、空的、重复的),我的代码还会正常运行吗?"
3、283 移动零
题目

代码
覆盖法
先统计非零元素全部挪到前面,最后把后面的位置全部手动补 0。
c
void moveZeroes(int* nums, int numsSize) {
int n = numsSize;
int i = 0;
int left = 0;
int count = 0;
for(i = 0;i < n;i++){
if(nums[i] != 0){
nums[left++] = nums[i];
count++;
}
}
for(i=count;i<n;i++){
nums[i] = 0;
}
}
时间复杂度:O(n)
空间复杂度:O(1)
简洁版
c
void moveZeroes(int* nums, int numsSize) {
int n = numsSize;
int i = 0;
int left = 0;
for(i = 0;i < n;i++){
if(nums[i] != 0){
nums[left++] = nums[i];
}
}
while(left < n){
nums[left++] = 0;
}
}
双指针交换法
指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
注意到以下性质:
左指针左边均为非零数;
右指针左边直到左指针处均为零。
因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变。
作者:力扣官方题解
链接:https://leetcode.cn/problems/move-zeroes/solutions/489622/yi-dong-ling-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
c
void moveZeroes(int* nums, int numsSize) {
int left = 0;
for (int right = 0; right < numsSize; right++) {
if (nums[right] != 0) {
// 只有当两者不相等时才交换,避免自交换
if (right > left) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
left++;
}
}
}
紧凑版
c
void moveZeroes(int* nums, int numsSize) {
int left = 0;
for(int right = 0;right < numsSize;right++){
if(nums[right] != 0){
if(right > left){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
left++;
}
}
}
4、844 比较含退格的字符串
题目:

代码:
c
bool backspaceCompare(char* s, char* t) {
int len_s = strlen(s);
int len_t = strlen(t);
int a = 0;
for(int i = 0;i < len_s;i++){
if(s[i] != '#'){
s[a++] = s[i];
}
else{
if(a>0){
a--;
}
}
}
s[a] = '\0';
a = 0;
for(int i = 0;i < len_t;i++){
if(t[i] != '#'){
t[a++] = t[i];
}
else{
if(a>0){
a--;
}
}
}
t[a] = '\0';
if(strcmp(s,t) == 0){
return true;
}
else{
return false;
}
}
简直就是垃圾~唉目前还不太会对字符串进行操作,加油加油
时间复杂度:O(n)
空间复杂度:O(1)
优化版
1.不用strlen,直接判断是否到'\0'即结束
c
bool backspaceCompare(char* s, char* t) {
int a = 0;
for(int i = 0;s[i] != '\0';i++){
if(s[i] != '#'){
s[a++] = s[i];
}
else{
if(a>0){
a--;
}
}
}
s[a] = '\0';
a = 0;
for(int i = 0;t[i] != '\0';i++){
if(t[i] != '#'){
t[a++] = t[i];
}
else{
if(a>0){
a--;
}
}
}
t[a] = '\0';
if(strcmp(s,t) == 0){
return true;
}
else{
return false;
}
}
2.封装成函数
c
void processstring(char* s){
int slow = 0;
for(int fast = 0;s[fast] != '\0';fast++){
if(s[fast] != '#'){
s[slow++] = s[fast];
}
else{
if(slow>0){
slow--;
}
}
}
s[slow] = '\0';
}
bool backspaceCompare(char* s, char* t) {
processstring(s);
processstring(t);
return strcmp(s,t) == 0;
}
return strcmp(s, t) == 0;为什么可以直接return这个?
gimini老师的解答:
这是一个非常好的问题。这涉及到 C 语言中 布尔值(Boolean) 和 函数返回值 的底层逻辑。
我们可以拆成两部分来理解:
1. strcmp 函数的返回值
首先,我们要明确 strcmp(s, t) 返回的是什么。它不是返回 true 或 false,而是返回一个整数:
- 0:表示两个字符串完全相等。
- 正数 :表示
s的 ASCII 码顺序在t之后。 - 负数 :表示
s的 ASCII 码顺序在t之前。
2. 逻辑表达式 == 0 的本质
当你写 strcmp(s, t) == 0 时,这实际上是一个判断题。计算机在执行这行代码时,会进行以下逻辑转换:
- 执行函数 :先算出
strcmp(s, t)的结果。假设结果是0。 - 进行比较 :判断
0 == 0是否成立。 - 得出结果:
- 如果成立(即字符串相等),整个表达式的结果就是 真 (true)。
- 如果不成立(结果是非 0),整个表达式的结果就是 假 (false)。
3. 对比两种写法
你可以把你的代码想象成这两个版本的演变:
版本 A(新手常用):
c
int result = strcmp(s, t);
if (result == 0) {
return true;
} else {
return false;
}
版本 B(进阶写法):
c
// 直接把判断的结果返回给调用者
return strcmp(s, t) == 0;
为什么版本 B 更好?
- 简洁 :省去了冗余的
if-else分支。 - 效率:编译器生成的汇编指令更少,执行速度理论上更快。
- 清晰:对于有经验的开发者来说,一眼就能看出这个函数的意图是"返回这两个字符串是否相等的判断结果"。
4. 常见的坑:千万别漏掉 == 0
在 C 语言中,如果你直接写 return strcmp(s, t);,那就大错特错了。
- 因为在 C 语言里,0 代表"假",非 0 代表"真"。
- 如果字符串相等 ,
strcmp返回0,函数会返回false。 - 如果字符串不相等 ,
strcmp返回非 0,函数反而会返回true。
这和我们的逻辑正好相反!所以必须写上== 0。
总结
return strcmp(s, t) == 0; 的意思就是:"请告诉我『strcmp 的结果等于 0』这件事是否为真。"
优化版:模拟栈(空间 O ( N ) O(N) O(N))
这是最直观的方法:利用双指针模拟栈的操作。遇到非 # 入栈,遇到 # 且栈不为空则出栈。
跟封装成函数是一样的,直接贴代码~
c
void build(char *s){
int slow = 0;
for(int fast = 0;s[fast] != '\0';fast++){
if(s[fast] != '#'){
s[slow++] = s[fast];
}
else if(slow > 0){
slow--;//退格:慢指针往回走一步
}
}
s[slow] = '\0';//必须加上字符串结束符
}
bool backspaceCompare(char* s, char* t) {
build(s);
build(t);
return strcmp(s,t) == 0;
}
优化版:从后往前双指针(空间 O ( 1 ) O(1) O(1))
面试官最喜欢的其实是从后往前遍历。 为什么? 因为退格符只会影响它前面的字符。如果我们从后往前看,就能立刻知道当前的字符是否应该被跳过。
算法逻辑:
准备两个指针 i, j 分别指向 s 和 t 的末尾。
用变量 skipS 和 skipT 记录当前需要跳过的字符数。
从后往前找第一个"有效"字符:
如果是 #,skipS++。
如果是普通字符且 skipS > 0,说明它被退格了,skipS--。
否则,这个字符就是最终留下的。
比较这两个有效字符是否相等。
c
bool backspaceCompare(char* s, char* t) {
int i = strlen(s) - 1, j = strlen(t) - 1;
int skipS = 0, skipT = 0;
while (i >= 0 || j >= 0) {
// 找到s中逆序第一个有效的字符
while (i >= 0) {
if (s[i] == '#') {
skipS++;
i--;
} else if (skipS > 0) {
skipS--;
i--;
} else {
break;
}
}
// 找到t中逆序第一个有效的字符
while (j >= 0) {
if (t[j] == '#') {
skipT++;
j--;
} else if (skipT > 0) {
skipT--;
j--;
} else {
break;
}
}
// 比较
if (i >= 0 && j >= 0) {
if (s[i] != t[j]) {
return false;
}
}
else {
if (i >= 0 || j >= 0) {//一个等于0,另一个大于0
return false;
}
}
i--;
j--;
}
return true;
}
5、997 有序数组的平方
题目

思路 from gimini
- 观察切入点(为什么这道题要用这个方法?)
- 核心原理(这个算法解决的是什么根本矛盾?)
- 推演过程(逻辑是怎么一步步从模糊变得清晰的?)
- 避坑指南(这里的陷阱在哪里?)
拆解:
1. 观察切入点:寻找"单调性"
- 初级观察 :题目给的是有序数组。一看到"有序",第一反应通常是二分查找或双指针。
- 深度观察 :平方运算有个特性------越小的负数,平方后越大;越大的正数,平方后也越大。
- 发现矛盾 :原数组是递增的(例如
[-4, -1, 0, 3, 10]),但平方之后,最大值分布在数组的两端 (16 和 100),最小值则在数组的中间靠拢。
2. 核心原理:对撞指针(Two Pointers)
这道题的本质是:两个单调序列的合并 。
如果你把负数部分看作一个序列,正数部分看作一个序列,平方后,这两个序列其实是从两头往中间"变小"的。
- 既然最大值一定在两头,我们就从两头往中间找。
3. 思路推演:从结果倒推
既然我们能确定"当前最大值"一定在 left 或 right 指向的位置,那我们就:
- 准备一个新数组,长度和原数组一样。
- 倒着填 :从新数组的最后一个位置(索引为
numsSize - 1)开始填入。 - 对撞比较:
- 比较
nums[left] * nums[left]和nums[right] * nums[right]。 - 谁大,谁就填入新数组的当前位置。
- 被选中的指针向中间移动一步,而落选的指针保持不动,等待下一轮比较。
4. 为什么这样做最优?
- 时间复杂度:。我们只遍历了一遍数组,没有用到任何排序算法(排序通常需要 )。
- 逻辑美感:它利用了题目给出的"有序"条件,将原本混乱的平方结果,通过"两头取大"的方式,直接变成了有序输出。
5. 避坑指南(面试常问)
- 空间换时间:注意这道题必须开辟一个新数组,因为如果你在原数组上直接修改,会覆盖掉还没参与比较的数字。
- 循环终止条件 :这里必须是
while(left <= right),因为最后一个元素(即平方后最小的那个)也需要被放入新数组。
你需要特别注意:
- 如何分配返回数组的空间?(使用
malloc) - 如何定义三个指针(
left,right, 以及新数组的填充指针k)?
代码:
c
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
int left = 0,right = numsSize-1;
int ans = numsSize - 1;
int* s = (int*)malloc(sizeof(int)*numsSize);
*returnSize = numsSize;
while(left <= right){
int m = pow(nums[left],2);
int n = pow(nums[right],2);
if(m > n){
s[ans--] = m;
left++;
}
else{
s[ans--] = n;
right--;
}
}
return s;
}
时间复杂度:O(n)
空间复杂度:O(1)
优化方向:
A. 避开 pow() 函数
原因:pow(x, 2) 是一个通用的数学函数,它内部是通过对数运算来实现的,返回值是 double 类型。对于简单的整数平方计算,pow 显得太重了(计算量大且存在浮点数转换开销)。
优化:直接用 nums[left] * nums[left]。这不仅快,而且直接得到整数,没有精度损失。
c
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
int left = 0,right = numsSize-1;
int ans = numsSize - 1;
int* s = (int*)malloc(sizeof(int)*numsSize);
*returnSize = numsSize;
while(left <= right){
int m = nums[left] * nums[left];
int n = nums[right] * nums[right];
if(m > n){
s[ans--] = m;
left++;
}
else{
s[ans--] = n;
right--;
}
}
return s;
}
暴力解:先全部平方再排序
代码:
c
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int cmp(const void * a, const void * b){
return *(int*)a - *(int*)b;
}
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
*returnSize = numsSize;
int* s = (int*)malloc(sizeof(int)*numsSize);
for(int i = 0;i < numsSize;i++){
s[i] = nums[i] * nums[i];
}
qsort(s,numsSize,sizeof(int),cmp);
return s;
}
杂记
快捷键 from gimini
在力扣(LeetCode)的编辑器中,确实有快捷键可以一键让混乱的代码变得整齐规范。
这个功能通常被称为 "格式化代码" (Format Code)。
1. 核心快捷键
根据你使用的操作系统,快捷键如下:
- Windows / Linux :
Shift+Alt+F - macOS :
Shift+Option+F
按下之后,编辑器会自动帮你调整缩进、空格、括号位置等,让代码符合该语言的标准规范(例如 C 语言通常会遵循类似 Google 或 LLVM 的风格)。
2. 其他常用的"规范化"小技巧
除了格式化,这几个快捷键也能帮你快速理清代码逻辑:
- 全选代码 :
Ctrl+A(Mac:Cmd+A) - 缩进/反缩进 : 选中多行后按
Tab增加缩进,按Shift+Tab减少缩进。 - 注释/取消注释 :
Ctrl+/(Mac:Cmd+/)。这在调试时非常有用,不用手动去打//。 - 智能提示 : 如果你写一半卡住了,可以按
Ctrl+Space强制触发自动补全。
3. 如何在界面上找到它?
如果你不习惯用快捷键,也可以手动操作:
- 点击代码编辑区域右上角的 "设置"图标(齿轮状)。
- 在弹出的菜单中,你可以看到 "自动格式化" 的选项。
- 有些版本的力扣,在编辑器的右下角或者右键菜单里也有
Format Code选项。
4. 为什么代码规范很重要?
作为初学者,养成格式化代码的习惯有几个好处:
- 容易排错 :缩进对齐了,你一眼就能看出
if或for的括号是不是漏写了。 - 鲁棒性暗示:整洁的代码会给面试官一种"这人逻辑严密、基本功扎实"的职业印象。
- 阅读顺畅:当你回看自己一周前写的代码时,规范的格式能帮你快速找回思路。