举一反三:删除排序数组重复项

电商价格列表中 [19,19,23,23,23,56] 因重复数据导致价格展示异常,你需要在不创建新数组的前提下,用O(1)空间复杂度实现去重后渲染UI


问题本质与挑战

需求核心 :在已排序数组 中原地删除重复项,返回去重后长度
四大挑战

  1. 空间限制:O(1)额外空间(禁止新建数组)
  2. 原地操作:直接修改原数组
  3. 顺序保持:保留非重复元素原始顺序
  4. 性能要求:时间复杂度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]

关键代码解析

  1. 双指针分工
    • slow:唯一元素边界(已处理区域的终点)
    • fast:探测器(扫描未处理区域)
  2. 位移条件
    nums[fast] !== nums[slow] 表明发现新唯一元素
  3. 赋值操作
    slow++; nums[slow] = nums[fast] 完成唯一元素前移
  4. 返回值
    slow + 1 唯一元素个数(慢指针索引+1)

双指针法 vs 传统方案

方案 时间复杂度 空间复杂度 是否原地修改 适用场景
双指针法 O(n) O(1) 内存敏感的排序数组
Set去重 O(n) O(n) 简单场景/小数据量
filter创建新数组 O(n) O(n) 可接受新数组的场景
排序后暴力删除 O(n²) O(1) 无序数组(先排序)

💡 为什么双指针是王者?

  1. 内存优势:不创建新数组,减少GC压力(尤其处理10万+商品列表时)
  2. 性能优势:仅单次遍历,避免嵌套循环
  3. 符合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>

优化关键点

  1. 将算法封装为methods避免重复计算
  2. 使用computed属性缓存结果
  3. slice(0, N)获取有效数据避免渲染冗余元素

⚠️ 边界陷阱与防御编码

  1. 空数组处理
javascript 复制代码
if (nums.length === 0) return 0; // 避免fast指针越界
  1. 非排序数组防御
javascript 复制代码
// 开发环境检测
if (process.env.NODE_ENV === 'development') {
  for (let i = 1; i < nums.length; i++) {
    if (nums[i] < nums[i-1]) throw new Error("输入必须为排序数组!");
  }
}
  1. 非数字类型处理
javascript 复制代码
if (typeof nums[fast] !== 'number') {
  console.warn('非数字元素:', nums[fast]);
  continue;
}

工程建议

  1. 大数据量优化

    • 超过10万数据时使用 Web Worker 避免阻塞UI
    javascript 复制代码
    // 主线程
    const worker = new Worker('dedupe-worker.js');
    worker.postMessage(prices);
  2. 与后端协作

    • 在SQL查询用 DISTINCT 初步去重减少前端压力
    sql 复制代码
    SELECT DISTINCT price FROM products ORDER BY price;
相关推荐
布兰妮甜5 分钟前
CSS Houdini 与 React 19 调度器:打造极致流畅的网页体验
前端·css·react.js·houdini
pusue_the_sun8 分钟前
数据结构——顺序表&&单链表oj详解
c语言·数据结构·算法·链表·顺序表
小小愿望18 分钟前
ECharts 实战技巧:揭秘 X 轴末项标签 “莫名加粗” 之谜及破解之道
前端·echarts
小小愿望26 分钟前
移动端浏览器中设置 100vh 却出现滚动条?
前端·javascript·css
fail_to_code27 分钟前
请不要再只会回答宏任务和微任务了
前端
摸着石头过河的石头27 分钟前
taro3.x-4.x路由拦截如何破?
前端·taro
lpfasd12336 分钟前
开发Chrome/Edge插件基本流程
前端·chrome·edge
yi.Ist1 小时前
图论——Djikstra最短路
数据结构·学习·算法·图论·好难
数据爬坡ing1 小时前
过程设计工具深度解析-软件工程之详细设计(补充篇)
大数据·数据结构·算法·apache·软件工程·软件构建·设计语言
练习前端两年半1 小时前
🚀 Vue3 源码深度解析:Diff算法的五步优化策略与最长递增子序列的巧妙应用
前端·vue.js