电商价格列表中
[19,19,23,23,23,56]
因重复数据导致价格展示异常,你需要在不创建新数组的前提下,用O(1)空间复杂度实现去重后渲染UI。
问题本质与挑战
需求核心 :在已排序数组 中原地删除重复项,返回去重后长度
四大挑战:
- 空间限制:O(1)额外空间(禁止新建数组)
- 原地操作:直接修改原数组
- 顺序保持:保留非重复元素原始顺序
- 性能要求:时间复杂度O(n)
双指针法(首选)
javascript
const removeDuplicates = (nums) => {
if (nums.length === 0) return 0;
// 慢指针:记录唯一元素应插入位置
let slow = 0;
// 快指针:扫描所有元素
for (let fast = 1; fast < nums.length; fast++) {
// 发现新唯一元素 → 慢指针前移并更新值
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1; // 返回唯一元素总数量
};
运行过程动态演示 (输入 [1,1,2,3,3]
):
graph TB
A[初始] --> |nums| B[1,1,2,3,3]
B --> C[slow=0, fast=1]
C -- "1==1 → 跳过" --> D[fast++]
D -- "fast=2: 1!=2 → 赋值" --> E[slow=1, nums=1,2,2,3,3]
E -- "fast=3: 2!=3 → 赋值" --> F[slow=2, nums=1,2,3,3,3]
F -- "fast=4: 3==3 → 跳过" --> G[结束]
G --> H[长度 = slow+1 = 3]
关键代码解析
- 双指针分工
slow
:唯一元素边界(已处理区域的终点)fast
:探测器(扫描未处理区域)
- 位移条件
nums[fast] !== nums[slow]
表明发现新唯一元素 - 赋值操作
slow++; nums[slow] = nums[fast]
完成唯一元素前移 - 返回值
slow + 1
唯一元素个数(慢指针索引+1)
双指针法 vs 传统方案
方案 | 时间复杂度 | 空间复杂度 | 是否原地修改 | 适用场景 |
---|---|---|---|---|
双指针法 | O(n) | O(1) | ✅ | 内存敏感的排序数组 |
Set去重 | O(n) | O(n) | ❌ | 简单场景/小数据量 |
filter创建新数组 | O(n) | O(n) | ❌ | 可接受新数组的场景 |
排序后暴力删除 | O(n²) | O(1) | ✅ | 无序数组(先排序) |
💡 为什么双指针是王者?
- 内存优势:不创建新数组,减少GC压力(尤其处理10万+商品列表时)
- 性能优势:仅单次遍历,避免嵌套循环
- 符合React规范 :直接修改状态数组,避免
setState
额外开销
举一反三:双指针的三大变种应用
变种1:保留K个重复项(电商价格保留2次折扣)
javascript
// 保留最多2个相同元素 输入 [1,1,1,2,2,3] → 返回5(新数组[1,1,2,2,3])
const removeDuplicatesKeepTwo = (nums) => {
if (nums.length <= 2) return nums.length;
let slow = 2; // 从第三位开始检查
for (let fast = 2; fast < nums.length; fast++) {
// 核心:当前元素 ≠ 慢指针前2位的元素
if (nums[fast] !== nums[slow - 2]) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
};
变种2:删除特定值(清除所有0值)
javascript
// 输入 [0,5,0,7] → 返回2(新数组[5,7])
const removeZero = (nums) => {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== 0) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
};
变种3:有序数组合并(商品价格区间合并)
javascript
// 合并两个有序数组(LeetCode88题核心逻辑)
const merge = (nums1, m, nums2, n) => {
let p1 = m - 1, p2 = n - 1, tail = m + n - 1;
while (p2 >= 0) {
// 逆序比较插入尾部
nums1[tail--] = (p1 >= 0 && nums1[p1] > nums2[p2])
? nums1[p1--]
: nums2[p2--];
}
};
💻 前端实战:Vue中高效渲染去重数据
html
<template>
<!-- 商品价格列表渲染 -->
<div v-for="(price, index) in displayPrices" :key="index">
{{ price }}元
</div>
</template>
<script>
export default {
data() {
return {
// 从API获取的含重复价格
prices: [19,19,23,23,56]
};
},
computed: {
displayPrices() {
const uniqueCount = this.removeDuplicates(this.prices);
// slice截取去重后部分
return this.prices.slice(0, uniqueCount);
}
},
methods: {
removeDuplicates(nums) {
// 双指针去重算法(同上文)
}
}
};
</script>
优化关键点:
- 将算法封装为methods避免重复计算
- 使用computed属性缓存结果
slice(0, N)
获取有效数据避免渲染冗余元素
⚠️ 边界陷阱与防御编码
- 空数组处理
javascript
if (nums.length === 0) return 0; // 避免fast指针越界
- 非排序数组防御
javascript
// 开发环境检测
if (process.env.NODE_ENV === 'development') {
for (let i = 1; i < nums.length; i++) {
if (nums[i] < nums[i-1]) throw new Error("输入必须为排序数组!");
}
}
- 非数字类型处理
javascript
if (typeof nums[fast] !== 'number') {
console.warn('非数字元素:', nums[fast]);
continue;
}
工程建议
-
大数据量优化 :
- 超过10万数据时使用
Web Worker
避免阻塞UI
javascript// 主线程 const worker = new Worker('dedupe-worker.js'); worker.postMessage(prices);
- 超过10万数据时使用
-
与后端协作 :
- 在SQL查询用
DISTINCT
初步去重减少前端压力
sqlSELECT DISTINCT price FROM products ORDER BY price;
- 在SQL查询用