数组与字符串进阶操作

本节目标

技巧一:双指针(快慢 / 左右)——原地去重、移除元素的标准姿势。

// 运行环境:Node.js 14+
// 保存为 dsa-l4.js,执行:node dsa-l4.js

// 快慢指针:原地移除目标值,返回新长度
function removeElement(nums, val) {
  let slow = 0
  for (let fast = 0; fast < nums.length; fast++) {
    if (nums[fast] !== val) nums[slow++] = nums[fast] // 不等于 val 才保留
  }
  return slow // [0, slow) 是保留区
}

// 左右对撞:反转、两数之和有序版、回文判断
function reverseArr(arr) {
  let l = 0, r = arr.length - 1
  while (l < r) [arr[l++], arr[r--]] = [arr[r], arr[l]]
}

// 调用示例
const a = [3, 2, 2, 3]
console.log(removeElement(a, 3), a) // 2 [2, 2, 3, 3](前 2 个有效)
const b = [1, 2, 3, 4]
reverseArr(b); console.log(b) // [4, 3, 2, 1]

技巧二:覆盖式原地修改——不删除元素,用写入指针覆盖即可。

技巧三:区间合并(面试高频):

// 合并重叠区间:先按起点排序,再逐个合并
function merge(intervals) {
  if (!intervals.length) return []
  intervals.sort((a, b) => a[0] - b[0])
  const res = [intervals[0]]
  for (let i = 1; i < intervals.length; i++) {
    const last = res[res.length - 1]
    const cur = intervals[i]
    if (cur[0] <= last[1]) last[1] = Math.max(last[1], cur[1]) // 重叠则扩展右边界
    else res.push(cur)
  }
  return res
}
console.log(merge([[1, 3], [2, 6], [8, 10], [15, 18]])) // [[1,6],[8,10],[15,18]]

字符串常用操作:

// 字符频次统计(字母异位词的判断手段)
function countChars(s) {
  const cnt = new Array(26).fill(0)
  for (const ch of s) cnt[ch.charCodeAt(0) - 97]++
  return cnt.join(',') // 作为"频次指纹"
}
console.log(countChars('aab') === countChars('aba')) // true(异位词)

边界意识:数组题先问"是否允许修改原数组 / 额外空间 / 元素范围";写代码前列 2-3 个用例(空数组、单元素、重复元素)。

名词解释

课后练习

  1. 原地移除数组重复项(有序数组,O(1) 空间)怎么写?
    • 答案:快慢指针,slow 指"已去重区末尾",if(nums[fast]!==nums[slow]) nums[++slow]=nums[fast];初始 slow=0。返回 slow+1。
  2. 写 merge(intervals) 时如果输入是空数组会怎样?
    • 答案:上面代码开头 if(!intervals.length) return [] 直接返回空,避免 intervals[0] 取 undefined 报错。边界用例要覆盖。

总结

数组和字符串是算法题的"主战场",而双指针是这片战场最通用的武器。它解决的核心矛盾是:很多题目表面上要求"删除/去重/反转",但真正的难点是"怎么在不额外开数组的前提下完成"。快慢指针用"写指针覆盖"把删除变成 O(1) 空间,左右对撞把反转/回文/两数之和变成一次遍历。我常跟学员说:看到"有序数组""去重""原地"这几个词,脑子里就该立刻冒出双指针。区间合并则教会我们一个更重要的思维——先排序再贪心,往往能把混乱的 O(n²) 暴力化简成 O(n log n)。记住,数组题永远先问边界(空?单元素?重复?),列用例再动手,比写完调试省十倍时间。