题目链接:https://leetcode.cn/problems/remove-element/description/
菜鸡写法:
// 函数定义,移除数组nums中所有值为val的元素,并返回新的数组长度
int removeElement(int* nums, int numsSize, int val) {
// 如果数组长度为0,直接返回0
if (numsSize == 0) {
return 0;
}
// 初始化两个指针,left指向数组的开始,right指向数组的末尾
int left = 0, right = numsSize - 1;
// 初始化一个变量来记录值为val的元素数量
int num_of_val = 0;
// 初始化一个临时变量用于交换元素
int tmp;
// 如果数组只有一个元素
if (left == right) {
// 如果这个元素的值等于val,则将数组指针置为NULL,并返回0
if (*(nums + left) == val) {
nums = NULL;
return 0;
} else {
// 否则返回1,因为数组中没有需要移除的元素
return 1;
}
}
// 开始遍历数组,直到left指针超过right指针
for (; left < right;) {
// 如果left指向的元素等于val,且right指向的元素不等于val
if (*(nums + left) == val && *(nums + right) != val) {
// 增加值为val的元素计数
num_of_val += 1;
// 交换left和right指向的元素
tmp = *(nums + left);
*(nums + left) = *(nums + right);
*(nums + right) = tmp;
// 移动left和right指针
left += 1;
right -= 1;
} else {
// 如果left指向的元素不等于val,则移动left指针
if (*(nums + left) != val) {
left += 1;
}
// 如果right指向的元素等于val,则增加计数并移动right指针
if (*(nums + right) == val) {
num_of_val += 1;
right -= 1;
}
}
}
// 如果left和right指针相遇,且指向的元素等于val
if (left == right && *(nums + left) == val) {
// 将该元素移到数组的末尾,并增加计数
for (; left <= numsSize - num_of_val - 2; left++) {
*(nums + left) = *(nums + left + 1);
}
num_of_val += 1;
}
// 返回新的数组长度,即原数组长度减去值为val的元素数量
return numsSize - num_of_val;
}
这段代码实现了移除数组中指定值的功能,其核心思想是使用双指针技术,一个指针从数组头开始,另一个从数组尾开始,通过交换元素的方式将所有值为val
的元素移到数组的末尾。下面是对这段代码的点评:
代码结构与逻辑
- 初始化:代码首先检查数组长度是否为0,如果是,则直接返回0。然后初始化两个指针
left
和right
,分别指向数组的开始和末尾。 - 单元素处理:对于只有一个元素的数组,代码进行了特殊处理,如果该元素等于
val
,则将数组指针置为NULL
并返回0,否则返回1。 - 双指针遍历:代码使用双指针遍历数组,如果
left
指向的元素等于val
且right
指向的元素不等于val
,则交换这两个元素,并移动指针。如果left
指向的元素不等于val
,则只移动left
指针;如果right
指向的元素等于val
,则只移动right
指针。 - 边界情况处理:当
left
和right
指针相遇时,如果指向的元素等于val
,则将该元素移到数组的末尾。 - 返回结果:最后,代码返回新的数组长度,即原数组长度减去值为
val
的元素数量。
时间复杂度分析
- 最坏情况:在最坏的情况下,即数组中所有的元素都等于
val
,此时需要遍历整个数组,因此时间复杂度为O(n),其中n是数组的长度。 - 最好情况:在最好的情况下,即数组中没有元素等于
val
,此时只需要遍历一次数组,时间复杂度同样为O(n)。
空间复杂度分析
- 额外空间:代码只使用了常数级别的额外空间,包括用于交换元素的临时变量
tmp
和用于计数的变量num_of_val
,因此空间复杂度为O(1)。
改进建议
- 指针移动逻辑:在双指针遍历的过程中,代码中的指针移动逻辑可以进一步优化,以减少不必要的判断和操作。例如,可以直接在交换元素的同时移动指针,而不需要额外的判断。
- 边界处理:对于
left
和right
指针相遇时的处理,可以考虑直接在遍历过程中处理,而不是在遍历结束后单独处理。 - 返回数组指针:如果需要返回修改后的数组指针,可以考虑在函数中返回,而不是仅仅返回新的数组长度。
总体来说,这段代码实现了移除数组中指定值的功能,具有较好的时间复杂度和空间复杂度,但在代码逻辑和边界处理上还有一定的优化空间。
双指针优化法:
int removeElement(int* nums, int numsSize, int val) {
// 初始化两个指针,`i` 用于遍历数组,`j` 用于记录新数组的长度
int i = 0, j = 0;
// 遍历整个数组
for (i = 0; i < numsSize; i++) {
// 如果当前元素不等于 `val`
if (nums[i] != val) {
// 将当前元素移动到新数组的位置 `j`
nums[j] = nums[i];
// 更新新数组的长度
j++;
}
}
// 返回新数组的长度
return j;
}
逐行注释解释:
int i = 0, j = 0;
:初始化两个指针,i
用于遍历数组,j
用于记录新数组的长度。for (i = 0; i < numsSize; i++) {
:开始遍历数组,i
从0开始,直到数组的长度。if (nums[i] != val) {
:检查当前元素是否不等于val
。nums[j] = nums[i];
:如果当前元素不等于val
,将其移动到新数组的位置j
。j++;
:更新新数组的长度,即j
指针向前移动一位。return j;
:遍历结束后,返回新数组的长度。
时间复杂度和空间复杂度:
- 时间复杂度:O(n),其中 n 是数组的长度。我们只需要遍历一次数组。
- 空间复杂度:O(1),我们只使用了常数级别的额外空间。
改进建议:
这段代码已经很高效,没有明显的改进空间。它简洁、直观地实现了移除数组中指定值的功能,同时保持了较低的时间和空间复杂度。
该方法体现了以下哲学和编程思想:
-
简洁性(Simplicity):代码力求简洁明了,避免不必要的复杂性。通过使用两个指针,代码直接实现了移除指定值的功能,没有引入额外的数据结构或复杂的逻辑。
-
效率(Efficiency):双指针方法在时间复杂度上达到了O(n),这意味着它只需要遍历一次数组,是一种非常高效的处理方式。同时,空间复杂度为O(1),表明代码只使用了常数级别的额外空间,符合空间效率的要求。
-
迭代(Iteration):代码通过迭代的方式遍历数组,这是一种常见的编程模式,用于处理数组、列表等线性数据结构。迭代允许我们逐步处理数据,而不是一次性处理,这有助于提高代码的可读性和可维护性。
-
指针操作(Pointer Manipulation):在C语言中,指针是一种强大的工具,可以直接操作内存。双指针方法巧妙地利用了指针来跟踪数组中的元素,这是一种低级别的编程技巧,体现了对内存管理的深刻理解。
-
条件逻辑(Conditional Logic):代码中使用了条件语句(
if
)来决定是否将当前元素添加到新数组中。这种基于条件的逻辑是编程中的基本思想,用于根据不同的情况执行不同的操作。 -
增量式开发(Incremental Development):虽然代码本身很简洁,但在开发过程中,程序员可能会逐步添加功能,测试每个部分,确保代码的正确性。这种增量式的开发方法有助于减少错误,提高代码质量。
-
算法思维(Algorithmic Thinking):双指针方法是一种特定的算法,用于解决特定类型的问题。这种思维方式要求程序员能够识别问题的模式,并设计出有效的解决方案。
-
抽象(Abstraction):代码抽象了移除数组中指定值的逻辑,使得其他开发者可以不关心具体的实现细节,直接使用这个函数来完成任务。
举一反三
基于双指针方法的思想和应用,以下是一些技巧和原则,可以帮助你在其他编程任务中举一反三:
1. 空间优化
- 原地操作:尽可能在输入的数据结构上直接进行修改,减少额外空间的使用。例如,在排序、合并或修改数组和链表时,考虑是否可以通过调整元素位置而不是创建新的数据结构来实现。
2. 时间优化
- 单遍扫描:设计算法时,尽量确保只需要遍历数据一次。多数使用双指针的场景,如去除重复项、查找对应和等,都是基于只遍历一遍数据来实现的。
3. 思维模式
- 对撞指针:在排序数组或链表中寻找两个数的和等特定问题时,可以从两端开始遍历,根据条件向中间逼近。
- 快慢指针:用于解决循环链表的判断、查找中间节点等问题。快指针的移动速度是慢指针的两倍,通过它们的相对速度差来解决问题。
4. 抽象思维
- 泛化问题解决方案:学习双指针技巧的同时,思考其背后的原理,例如减少不必要的遍历、空间优化等。这些原理不仅仅适用于数组和链表,也可以扩展到其他数据结构和问题上。
5. 条件逻辑与增量思想
- 逐步构建解决方案:在解决问题时,从最简单的情况开始,逐渐添加条件和逻辑,以构建完整的解决方案。这有助于避免一开始就陷入复杂的情况,使问题更易于理解和解决。
- 灵活应用条件判断:在使用双指针技巧时,根据问题的需求调整指针的移动条件。例如,满足某条件时移动左指针,不满足时移动右指针,或者两者都移动。
6. 代码优化与重构
- 迭代与重构:在初步实现功能后,通过测试和验证来识别性能瓶颈或不必要的复杂度,然后迭代优化。这可能涉及调整算法、简化逻辑或利用更高效的数据结构。
通过掌握这些技巧和原则,你可以将双指针方法背后的思想应用到更宽泛的编程问题和数据结构上,提高代码的效率和质量。记住,好的编程实践不仅仅是关于解决一个特定问题,而是学会如何以一种可扩展、可维护的方式思考和实现解决方案。