前置知识(可跳过)
1.splice() 方法
splice() 方法用于修改数组的内容,可以删除、替换或添加元素。它会直接修改原始数组。
语法:
array.splice(start, deleteCount, item1, item2, ...)
参数:
-
start: 必需。指定修改开始的位置(索引)。- 如果
start超出数组的长度,则从数组末尾开始添加元素。 - 如果
start是负数,则从数组末尾开始计算,例如-1表示数组的最后一个元素。
- 如果
-
deleteCount: 可选。指定要删除的元素数量。- 如果
deleteCount为 0,则不删除任何元素。 - 如果
deleteCount大于start之后的元素总数,则从start开始删除所有元素。 - 如果省略
deleteCount,则从start开始删除所有元素。
- 如果
-
item1, item2, ...: 可选。要添加到数组中的元素。从start位置开始插入。
返回值:
splice() 方法返回一个包含被删除元素的数组。如果没有删除任何元素,则返回一个空数组。
2.push() 方法
push() 方法用于将一个或多个元素添加到数组的末尾,并返回修改后的数组的新长度。
语法:
array.push(item1, item2, ...);
参数:
item1, item2, ...: 要添加到数组末尾的元素。
返回值:
push() 方法返回修改后的数组的新长度。
二、题目描述——移动零
给定一个数组 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
进阶: 你能尽量减少完成的操作次数吗?
三、题解
本人错误解法
var moveZeroes = function(nums) {
let count = 0;
for(let i=0;i<nums.length;i++){
if(nums[i]===0){
nums.splice(i,1);
count++;
}
}
for(let i=0; i<count;i++){
nums.push(0)
}
return nums;
};
为何错误
1.splice 导致索引错乱: 这是最主要的问题。 当你在循环中使用 splice(i, 1) 删除一个元素后,数组的长度会减小,并且从 i 之后的所有元素的索引都会向前移动一位。 这会导致以下问题:
- **跳过元素:** 如果 `nums[i]` 是 0, 你删除了它。然后,原来在 `nums[i+1]` 的元素会移动到 `nums[i]`。 但是,循环会直接进入 `i+1` 的下一轮迭代。 这样就跳过了原来 `nums[i+1]` (现在在 `nums[i]` 的位置) 的元素,导致可能没有正确处理它。
- **越界访问:** 极端情况下,当数组末尾有连续的 0 时, 可能会导致数组越界访问。
2.未完全满足原地操作的要求: 虽然结果是在原数组上修改的,但在循环中使用 splice 操作涉及到数组的重构,严格意义上来说,不算是最高效的原地操作 (虽然题目没有明确禁止使用)。
正确题解一
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let insertPos = 0; //指向下一个要插入非零元素的位置
// 第一次遍历:将所有非零元素移动到数组的前面
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) {
nums[insertPos] = nums[i];
insertPos++;
}
}
// 第二次遍历:将数组剩余的位置填充为 0
while (insertPos < nums.length) {
nums[insertPos] = 0;
insertPos++;
}
};
-
思路:
- 把所有非零的元素挪到数组的前面,保持它们的相对顺序。
- 剩下的位置都用零填充。
-
具体步骤:
1.第一次遍历(收集非零元素) :
insertPos相当于一个指针,它始终指向下一个应该放置非零元素的位置。- 当
nums[i]不是 0 时,把它放到nums[insertPos]的位置上。 然后,insertPos向后移动一位,准备存放下一个非零元素。 - 循环结束后,
insertPos前面的所有元素都变成了非零元素,并且保持了它们在原数组中的相对顺序。 关键是,insertPos也记录了非零元素有多少个。
2.第二次遍历(填充零) :
- 从
insertPos开始到数组的末尾,把所有元素都设置为 0。 因为第一次遍历后,insertPos指向的位置以及后面的所有位置,原来都是零或者被非零元素覆盖掉的位置,现在应该填充为零.
正确题解二(最优题解)
function moveZeroes(nums) {
if (!nums || nums.length === 0) {
return;
}
let left = 0;
for (let right = 0; right < nums.length; right++) {
if (nums[right] !== 0) {
if (left !== right) { // 避免不必要的交换
nums[left] = nums[right];
nums[right] = 0; // 直接将 right 位置设为 0
}
left++;
}
}
}
-
核心思路:
- 使用两个指针
left和right,left指向下一个非零元素应该放置的位置,right用于遍历数组。 right指针遍历数组遇到非零元素时,将其移动到left指针指向的位置。- 移动后,
right原来的位置设置为0。 这就保证了所有非零元素都在数组的前面,而所有零都在数组的后面。 - 关键优化: 避免不必要的交换。 只有当
left和right指针不相等时,才进行交换操作。 这可以减少当非零元素已经在正确位置上时的操作。
- 使用两个指针
-
具体步骤分解:
-
初始化:
left = 0:left指针初始化为 0,表示数组的起始位置,也就是下一个非零元素应该放的位置。
-
遍历数组:
-
right指针从数组的起始位置开始,向右移动,遍历整个数组。 -
如果
nums[right] !== 0(遇到非零元素) :- 检查是否需要交换:
if (left !== right)。 如果left和right指向不同的位置,说明right指针当前指向的非零元素需要移动到left指针指向的位置。 - 交换元素:
nums[left] = nums[right]; nums[right] = 0;将right指针指向的非零元素移动到left指针指向的位置,并将right指针指向的位置设置为 0。 这一步是关键,它保证了零被移动到数组的末尾。 left++:left指针向右移动一位,指向下一个非零元素应该放置的位置。
- 检查是否需要交换:
-
-
遍历完成: 当
right指针遍历完整个数组后,所有非零元素都被移动到了数组的前面,而所有零都被移动到了数组的末尾。
-
-
为什么这个解法更优?
- 一次遍历: 只需要一次遍历即可完成操作,提高了效率。
- 减少交换次数: 通过
if (left !== right)的判断,避免了不必要的交换操作,进一步提高了效率。 例如,如果数组前几个元素都是非零元素,left和right会同步前进,此时不需要进行任何交换。 - 原地修改: 直接在原数组上操作,空间复杂度为 O(1)。
-
对比前一个解法
- 前一个解法使用了两次遍历,第一次遍历找到非零元素并移动,第二次遍历填充零。
- 这个解法只使用一次遍历,通过一次遍历完成元素的移动和零的填充。
实例与展示
四、结语
再见