用可视化学习双指针法
1.移除元素
int removeElement(int* nums, int numsSize, int val) {int dst = 0; // 慢指针,用于收集非目标值元素// 遍历整个数组for (int src = 0; src < numsSize; src++) {// 如果当前元素不等于要删除的值if (nums[src] != val) {// 保留该元素(复制到dst位置)并移动dstnums[dst] = nums[src];dst++;}// 如果当前元素等于要删除的值,只移动src,不做其他操作}// 返回新数组的长度,即dst的值return dst;
}
我们用可视化的方法来看这道题:
目标:把 [😈, 😊, 😊, 😈]
中所有 😈
移除,留下 [😊, 😊]
,返回新长度 2。
初始状态:
数组: [😈, 😊, 😊, 😈] (其中😈代表值3,😊代表值2)dst src
第一轮:检查src=0时
检查: nums[src](😈) == val(😈)吗?✅ 是要删除的! 跳过它!src++ 向后移动一位
数组: [😈, 😊, 😊, 😈]dst src
第二轮:检查src=1时
检查: nums[src](😊) == val(😈)吗?❌ 不是! 这是要保留的!dst复制+移动,src继续++
数组: [😊, 😊, 😊, 😈]dst src
第三轮:检查src=2时
检查: nums[src](😊) == val(😈)吗?❌ 不是! 又是要保留的元素!
dst复制+移动,src++数组: [😊, 😊, 😊, 😈]dst src
第四轮:检查src=3时
检查: nums[src](😈) == val(😈)吗?✅ 是要删除的! 跳过它!
src++(超出范围)数组: [😊, 😊, 😊, 😈]dst src(出界)
最终结果:
更新后数组: [😊, 😊, 😊, 😈]👆dst=2返回值: dst = 2
用mermaid做出的流程图:
2.删除重复元素
先上完整代码,再来可视化
int removeDuplicates(int* nums, int numsSize) {if (numsSize == 0) return 0;int dst = 0; // 慢指针,指向当前无重复区域的末尾// 从索引1开始遍历数组for (int src = 1; src < numsSize; src++) {// 如果发现一个新元素(与慢指针指向的元素不同)if (nums[src] != nums[dst]) {// 将慢指针前移,并复制这个新元素dst++;nums[dst] = nums[src];}// 如果元素重复,只移动快指针,不做其他操作}// 返回无重复区域的长度 = dst + 1return dst + 1;
}
目标:把 [1, 1, 2, 3, 3, 4]
变成 [1, 2, 3, 4]
,返回新长度 4。
初始状态:
数组: [1, 1, 2, 3, 3, 4]dst src
第一轮:检查src=1(值1)
比较: nums[dst]=1 VS nums[src]=1🤔 一样! 不需要保留
src++
数组: [1, 1, 2, 3, 3, 4]dst src
第二轮:检查src=2(值2)
比较: nums[dst]=1 VS nums[src]=2😲 不一样! 发现新数字
dst移动+复制新数字
数组: [1, 2, 2, 3, 3, 4]dst src
src++
数组: [1, 2, 2, 3, 3, 4]dst src
第三轮:检查src=3(值3)
比较: nums[dst]=2 VS nums[src]=3😲 不一样! 又一个新数字
dst移动+复制新数字
数组: [1, 2, 3, 3, 3, 4]dst src
src++
数组: [1, 2, 3, 3, 3, 4]dst src
第四轮:检查src=4(值3)
比较: nums[dst]=3 VS nums[src]=3🤔 一样! 跳过这个重复的
src++
数组: [1, 2, 3, 3, 3, 4]dst src
第四轮:检查src=5(值4)
比较: nums[dst]=3 VS nums[src]=4😲 不一样! 最后一个新数字
dst移动+复制新数字
数组: [1, 2, 3, 4, 3, 4]dst srcsrc++
数组: [1, 2, 3, 4, 3, 4]dstsrc(出界)
最终结果:
更新后数组: [1, 2, 3, 4, 3, 4]👆dst=3返回值: dst+1 = 4
mermaid生成的流程图:
3.合并两个有序数组
依旧是先来代码,再进行可视化
void merge(int* nums1, int m, int* nums2, int n) {int l1 = m - 1; // 指向nums1最后一个有效元素int l2 = n - 1; // 指向nums2最后一个元素int l3 = m + n - 1; // 指向合并后数组的末尾位置// 从后往前合并,每次选取较大的元素放到nums1的末尾while (l1 >= 0 && l2 >= 0) {// 将较大的元素放到l3指向的位置if (nums1[l1] > nums2[l2]) {nums1[l3--] = nums1[l1--];} else {nums1[l3--] = nums2[l2--];}}// 如果nums2还有剩余元素,将它们复制到nums1// 注意: 如果nums1还有剩余元素,它们已经在正确位置上,不需要移动while (l2 >= 0) {nums1[l3--] = nums2[l2--];}
}
目标:将 [1, 2, 3, 0, 0, 0]
(nums1) 和 [2, 5, 6]
(nums2) 合并成 [1, 2, 2, 3, 5, 6]
初始状态:
nums1: [1, 2, 3, 0, 0, 0]l1 l3
nums2: [2, 5, 6]l2
第一轮:比较l1和l2
谁更大? nums1[l1]=3 VS nums2[l2]=6❌ ✅
nums2元素更大! 6 → 放到l3指向的位置 && l2,l3同时向前移动一位nums1: [1, 2, 3, 0, 0, 6]l1 l3
nums2: [2, 5, 0]l2
第二轮:比较l1和l2
谁更大? nums1[l1]=3 VS nums2[l2]=5❌ ✅
nums2元素更大! 5 → 放到l3指向的位置 && l2,l3同时向前移动一位
nums1: [1, 2, 3, 0, 5, 6]l1 l3
nums2: [2, 0, 0]l2
第三轮:比较l1和l2
谁更大? nums1[l1]=3 VS nums2[l2]=2✅ ❌
nums1元素更大! 3 → 放到l3指向的位置 && l1,l3同时向前移动一位
nums1: [1, 2, 3, 3, 5, 6]l1 l3
nums2: [2, 0, 0]l2
第四轮:比较l1和l2
谁更大? nums1[l1]=2 VS nums2[l2]=2🤝 平手! 按规则用nums2的
nums2的2 → 放到l3指向的位置 && l2,l3同时向前移动一位
nums1: [1, 2, 2, 3, 5, 6]l1l3
nums2: [0, 0, 0]l2(出界)
最终检查:l2已经<0,说明nums2元素已全部放入,而nums1剩余元素已经在正确位置,不需要移动。
mermaid生成的流程图:
4.总结
4.1 三种方法对比
特性 | 合并有序数组 | 删除重复元素 | 移除元素 |
---|---|---|---|
指针方向 | 从后往前 | 从前往后 | 从前往后 |
指针数量 | 3个 | 2个 | 2个 |
慢指针功能 | 定位放置位置 | 标记不重复元素边界 | 收集需保留的元素 |
快指针功能 | 比较选择元素 | 探索新元素 | 筛选有效元素 |
元素处理 | 每次选较大的 | 只保留不重复的 | 只保留非目标值 |
返回值 | 无需返回 | dst+1 | dst |
4.2 时间复杂度分析
算法 | 时间复杂度 | 空间复杂度 |
---|---|---|
合并有序数组 | O(m+n) | O(1) |
删除重复元素 | O(n) | O(1) |
移除元素 | O(n) | O(1) |
4.3双指针法的核心思想
-
空间效率:所有算法都是原地操作,不需要额外空间
-
指针分工:
-
慢指针:通常负责构建结果或标记边界
-
快指针:负责探索或提供数据
-
-
处理策略:
-
元素保留:满足特定条件时复制并移动慢指针
-
元素丢弃:不满足条件时只移动快指针
-