Leetcode 36
1 题目
283. 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums =[0,1,0,3,12]输出:[1,3,12,0,0]
示例 2:
输入: nums =[0]输出:[0]
提示:
1 <= nums.length <= 104-231 <= nums[i] <= 231 - 1
2 代码实现
void moveZeroes(int* nums, int numsSize) {int i = 0 ; for (int j = 0 ; j < numsSize ; j ++){if (nums[j] != 0 ){nums[i] = nums[j];i++;}}for(;i<numsSize; i++){nums[i] = 0 ;}
}
错误百出
void moveZeroes(int* nums, int numsSize) {int i = 0 ; for (int j = 0 ; j < numsSize ; j ++){if (nums[j] != 0 ){nums[i] = nums[j];i++;}}for(;i<numsSize; i++){nums[i+1] = 0 ;i++;}
}
这段代码的意图应该是实现 “将数组中的 0 移动到末尾,同时保持非 0 元素的相对顺序”,但存在明显的逻辑错误,导致无法正确运行。我们来一步步分析:
代码逻辑拆解(意图)
从结构上看,代码试图用两步完成任务:
-
第一步(第一个 for 循环):用双指针
i和j,把所有非 0 元素 “搬运” 到数组前端(类似removeElement的思路)。j遍历所有元素,i记录非 0 元素的位置,当nums[j]≠0时,将其放到nums[i]并让i自增。这一步结束后,i应该指向 “非 0 元素的末尾”,剩下的位置(i到numsSize-1)需要填充 0。 -
第二步(第二个 for 循环):理论上应该把
i及之后的位置全部设为 0,但实际代码写错了。
错误分析
第二个 for 循环的逻辑完全错误:
for(;i<numsSize; i++){nums[i+1] = 0 ; // 越界风险 + 赋值位置错误i++; // 重复自增,跳过元素
}
- 越界问题:当
i = numsSize-1时,i+1 = numsSize,超出数组下标范围(数组最大下标是numsSize-1),会导致内存访问错误。 - 重复自增:for 循环本身已有
i++,循环体内又执行i++,导致i每次增加 2,会跳过部分位置,无法将所有剩余位置设为 0。 - 赋值位置错误:应该给
nums[i]赋值 0(当前位置),而不是nums[i+1](下一个位置)。
修正方案
修正第二个循环,正确填充剩余位置为 0:
void moveZeroes(int* nums, int numsSize) {int i = 0;// 第一步:将所有非0元素移到前端for (int j = 0; j < numsSize; j++) {if (nums[j] != 0) {nums[i] = nums[j];i++;}}// 第二步:将剩余位置填充为0(从i到numsSize-1)for (; i < numsSize; i++) {nums[i] = 0; // 正确赋值当前位置,无越界}
}
修正说明
- 第一步结束后,
i的值等于 “非 0 元素的个数”,此时[0, i-1]已存放所有非 0 元素(保持原顺序)。 - 第二步循环从
i开始,到numsSize-1结束,将这些位置逐个设为 0,确保数组末尾的所有元素都是 0。 - 时间复杂度
O(n),空间复杂度O(1),符合原地修改的要求。
例如,对于输入nums = [0,1,0,3,12],修正后代码会输出[1,3,12,0,0],符合预期。
3 题目
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"] 输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"] 输出:["h","a","n","n","a","H"]
提示:
1 <= s.length <= 105s[i]都是 ASCII 码表中的可打印字符
4 代码实现
void reverseString(char* s, int sSize) {for (int head = 0;head < sSize / 2 ; head++){int end = sSize - head -1 ;char temp = 0 ;temp = s[end] ;s[end] = s[head] ;s[head] = temp ;}}
void reverseString(char* s, int sSize) {int left = 0 ;int right = sSize -1 ;while (left < right){char temp = 0 ;temp = s[left];s[left] = s[right];s[right] = temp ;left ++;right --;}}
第一次写的错误,又是一出惨案......
void reverseString(char* s, int sSize) {for (int head = 0;head < sSize / 2 ; head++){for (int end = sSize - 1 ; end > sSize / 2 ;end -- ){char temp = 0 ;temp = s[end] ;s[end] = s[head] ;s[head] = temp ;}}
}
你提供的这段代码无法正确反转字符串,原因在于使用了双层循环,导致字符交换逻辑出现错误。具体问题分析如下:
核心问题:错误的循环逻辑
反转字符串的正确思路是对称交换:第 i 个字符与第 sSize-1-i 个字符交换(例如第 1 个和最后 1 个交换,第 2 个和倒数第 2 个交换),只需一层循环即可完成。
但你的代码用了两层循环:
- 外层循环
head从0遍历到sSize/2 - 1(这部分没问题,对应前半部分字符)。 - 内层循环
end从sSize-1遍历到sSize/2 + 1(这部分错误,会导致重复交换)。
内层循环会让 head 位置的字符与所有 end > sSize/2 的字符依次交换,而不是只交换一次对应的对称位置。
举例说明(以 s = "abcd",sSize=4 为例)
- 正确目标:反转后应为
"dcba"。 - 你的代码执行过程:
- 外层循环
head=0时,内层循环end从3遍历到2(因为sSize/2=2,end > 2即end=3):- 第一次交换:
s[0]与s[3]交换 →s变为"dbca"。
- 第一次交换:
- 外层循环
head=1时,内层循环end从3遍历到2(end=3):- 第二次交换:
s[1]与s[3]交换 →s变为"dacb"。最终结果为"dacb",显然错误。
- 第二次交换:
- 外层循环
正确代码(单层循环)
void reverseString(char* s, int sSize) {for (int head = 0; head < sSize / 2; head++) {int end = sSize - 1 - head; // 计算对称位置char temp = s[head];s[head] = s[end];s[end] = temp;}
}
逻辑说明:
- 每次循环中,
head对应前半部分字符的索引,end直接计算为其对称位置的索引(sSize-1-head)。 - 仅交换这两个位置的字符一次,循环
sSize/2次即可完成反转。
更优雅的双指针相向而行
你提到的这个思路(双指针相向而行)确实是反转字符串的经典且高效的实现方式,用 C 语言实现会非常简洁且高效,逻辑也更直观。
C 语言版本实现(对应思路):
void reverseString(char* s, int sSize) {int left = 0;int right = sSize - 1;while (left < right) {// 交换左右指针指向的字符char temp = s[left];s[left] = s[right];s[right] = temp;// 指针向中间移动left++;right--;}
}
为什么这个思路更巧妙?
-
时间复杂度最优:只需要遍历字符串的一半(
left和right相遇即停止),时间复杂度为 O(n),且没有多余的循环或计算。 -
空间复杂度最优:仅使用了
left、right、temp三个额外变量,空间复杂度为 O(1),属于原地反转(in-place),不需要额外开辟空间。 -
逻辑直观:双指针从两端向中间逼近,每次交换一对对称字符,过程清晰易懂,不容易出错(避免了嵌套循环的逻辑混乱)。
-
适用性广:不仅适用于字符串,还可以推广到数组等线性结构的反转,是一种通用的双指针技巧。
这个实现确实比之前的嵌套循环版本更优雅,也是实际开发中反转字符串的首选方式。
