在C++算法题中,"移除元素"是一个经典的基础练习题,主要考察数组操作和双指针技巧的掌握程度。题目通常要求我们在原地修改数组,移除所有等于特定值的元素,并返回新数组的长度。这类问题看似简单,但实际编码时会遇到不少边界条件需要处理。
这个问题在实际开发中有很强的现实意义。比如在处理用户输入数据时,我们经常需要过滤掉某些无效值;在图像处理中可能需要移除特定像素;在数据分析时可能要排除异常样本。理解这个基础问题的解法,能帮助我们更好地处理这些实际场景。
最直观的解法是使用双层循环:外层遍历数组,内层在发现目标值时将后续元素全部前移一位。这种方法虽然简单直接,但时间复杂度达到了O(n²),在数据量较大时性能会很差。
cpp复制int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) {
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为i以后的元素都前移了一位,所以i也要前移
size--; // 数组大小减1
}
}
return size;
}
注意:内层循环后需要对i进行减1操作,因为后面的元素已经前移,当前位置已经是新的元素了。这是容易出错的一个点。
这种解法虽然逻辑清晰,但存在两个主要问题:
更高效的解法是使用双指针技巧。我们定义一个慢指针slow和一个快指针fast,快指针用于遍历数组,慢指针用于标记新数组的位置。
cpp复制int removeElement(vector<int>& nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
这个解法的时间复杂度是O(n),空间复杂度是O(1),性能有了显著提升。快指针遍历整个数组,慢指针只在遇到非目标值时前进并赋值。
当要移除的元素很少时,我们可以采用另一种双指针策略:一个从头部开始,一个从尾部开始,当遇到目标值时,用尾部元素覆盖它。
cpp复制int removeElement(vector<int>& nums, int val) {
int left = 0;
int right = nums.size();
while (left < right) {
if (nums[left] == val) {
nums[left] = nums[right - 1];
right--;
} else {
left++;
}
}
return left;
}
这种方法减少了元素赋值次数,特别是当目标值较少且位于数组末尾时效率更高。但要注意它改变了元素的相对顺序。
当输入数组为空时,应该直接返回0。虽然现代C++中size()返回的是无符号数,但为了代码健壮性,最好显式处理这种情况。
cpp复制if (nums.empty()) return 0;
如果数组中所有元素都是目标值,双指针法会直接返回0,这是符合预期的。但要注意在这种情况下,我们不需要进行任何元素移动操作。
当数组中不存在目标值时,快慢指针会同步移动,最终返回数组原长度。这种情况下虽然进行了n次比较,但没有进行任何元素移动。
虽然两种双指针解法的时间复杂度相同,但实际性能会有差异:
所有解法都是原地操作,空间复杂度为O(1),没有使用额外空间。
如果需要保持非目标元素的原始顺序,必须使用快慢指针法。首尾指针法会改变元素相对顺序。
这种算法不仅用于算法题,在实际工程中也有很多应用:
掌握了这个问题的解法后,可以解决一系列类似问题:
C++标准库中的remove算法也采用了类似的双指针策略:
cpp复制vector<int>::iterator newEnd = remove(nums.begin(), nums.end(), val);
nums.erase(newEnd, nums.end());
理解了我们手写的解法后,就能明白STL内部是如何实现这些操作的。
在实际工程中,建议使用更具描述性的变量名:
为了提高代码可读性,可以:
完整的测试应该包括:
在使用首尾指针法时,容易忽略right指针的初始值设置。如果初始设为nums.size()-1,可能会导致越界。
在暴力解法中,忘记调整i的位置会导致无限循环或漏检元素。
如果问题要求保持元素顺序,使用首尾指针法会导致错误结果。
可以使用以下方法调试:
虽然我们讨论的是C++实现,但了解其他语言的实现方式也有帮助:
Java中数组长度固定,通常需要返回新数组或使用ArrayList。
Python列表的remove方法虽然简单,但不是原地操作,且时间复杂度不同。
JavaScript中可以使用filter方法,但这会创建新数组而非原地修改。
解决这个问题需要培养以下算法思维:
在实际面试中,面试官通常会期待候选人能先提出暴力解法,然后逐步优化到最优解,并能够分析各种解法的优劣。