字符与数字的碰撞:一网打尽字符串与数组算法

96 阅读10分钟

wallhaven-73rdo3.jpg 在这个数字与字符交织的世界里,字符串和数组构成了计算机科学中最基本的数据结构之一。无论是简单的文本处理还是复杂的算法设计,掌握字符串与数组的操作技巧都是每位程序员不可或缺的能力。本文旨在通过一系列精心挑选的算法题目,带领读者深入探索这些基本数据结构的奥秘,从基础操作到高级应用,我们将逐一解析字符串与数组的典型问题及解决方案。

让我们一同开启这段旅程,迎接字符与数字之间的碰撞,一网打尽那些令人着迷的字符串与数组算法吧!

合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意: 最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

示例 1:

输入: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
解释: 需要合并 [1,2,3][2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

输入: nums1 = [1], m = 1, nums2 = [], n = 0
输出: [1]
解释: 需要合并 [1][] 。
合并结果是 [1]

示例 3:

输入: nums1 = [0], m = 0, nums2 = [1], n = 1
输出: [1]
解释: 需要合并的数组是 [][1] 。
合并结果是 [1] 。

注意,因为 m = 0 ,所以 nums1 中没有元素。
nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

题解

为了合并两个已排序的数组 nums1nums2,我们可以采用一种叫做“双指针”的方法来实现。这种方法可以从数组的末尾开始比较并放置元素,这样就不需要额外的空间来存储合并后的数组。

function merge(nums1, m, nums2, n) {
  // 初始化两个指针和合并数组的写入位置
  let p1 = m - 1,
    p2 = n - 1,
    writePos = m + n - 1;

  // 当两个数组中还有元素时,进行比较并合并
  while (p1 >= 0 && p2 >= 0) {
    if (nums1[p1] > nums2[p2]) {
      nums1[writePos] = nums1[p1];
      p1--;
    } else {
      nums1[writePos] = nums2[p2];
      p2--;
    }
    writePos--;
  }

  // 如果 nums2 中还有剩余元素,则将它们复制到 nums1 的开头
  while (p2 >= 0) {
    nums1[writePos] = nums2[p2];
    p2--;
    writePos--;
  }
}

// 示例
const nums1 = [1, 2, 3, 0, 0, 0];
const m = 3;
const nums2 = [2, 5, 6];
const n = 3;

merge(nums1, m, nums2, n);
console.log(nums1); // 输出: [1, 2, 2, 3, 5, 6]

代码解析

  1. 初始化指针:

    • p1: 指向 nums1 数组最后一个有效元素的索引。
    • p2: 指向 nums2 数组最后一个有效元素的索引。
    • writePos: 指向 nums1 数组中下一个需要写入的索引。
  2. 比较并合并:

    • 从数组的末尾开始比较 nums1 和 nums2 中的元素。
    • 如果 nums1[p1] 大于 nums2[p2],则将较大的元素 nums1[p1] 写入到 nums1[writePos],并将 p1 和 writePos 分别向前移动一位。
    • 否则,将较小的元素 nums2[p2] 写入到 nums1[writePos],并将 p2 和 writePos 分别向前移动一位。
  3. 处理剩余元素:

    • 如果 nums2 中还有剩余元素,则将它们依次写入到 nums1 的剩余位置。

这种方法的时间复杂度为 O(m + n),因为它只需要遍历一次两个数组即可完成合并。同时,这种方法的空间复杂度为 O(1),因为我们直接在 nums1 数组上进行了修改,而没有使用额外的空间。

移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k

示例 1:

输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2,_,_]
解释: 你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

示例 2:

输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3,_,_,_]
解释: 你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

题解

我们可以使用双指针技术来原地移除数组中等于 val 的元素,并返回剩余元素的数量。

function removeElement(nums, val) {
  let writeIndex = 0; // 用于记录写入的位置

  // 遍历数组
  for (let readIndex = 0; readIndex < nums.length; readIndex++) {
    // 如果当前元素不是 val,则将其写入到 writeIndex 位置,并将 writeIndex 向前移动
    if (nums[readIndex] !== val) {
      nums[writeIndex] = nums[readIndex];
      writeIndex++;
    }
  }

  // 截断数组,只保留与 val 不同的元素
  nums.length = writeIndex;

  // 返回与 val 不同的元素数量
  return writeIndex;
}

// 示例
const nums = [3, 2, 2, 3];
const val = 3;
const k = removeElement(nums, val);
console.log(k); // 输出: 2
console.log(nums.slice(0, k)); // 输出: [2, 2]

代码解析

  1. 初始化写入指针:

    • writeIndex 用于记录下一个要写入的元素的位置。
  2. 遍历数组:

    • 使用 readIndex 从头到尾遍历数组 nums
    • 如果当前元素 nums[readIndex] 不等于 val,则将该元素复制到 nums[writeIndex] 位置,并将 writeIndex 向前移动一位。
  3. 截断数组:

    • 最终,writeIndex 的值就是与 val 不同的元素数量。我们将数组的长度设置为 writeIndex,以去除多余的元素。
  4. 返回结果:

    • 返回 writeIndex 作为与 val 不同的元素数量。

这种方法的时间复杂度为 O(n),因为它只需要遍历一次数组即可完成元素的移除。同时,这种方法的空间复杂度为 O(1),因为我们直接在 nums 数组上进行了修改,而没有使用额外的空间。

删除有序数组中的重复项

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k

示例 1:

输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2,_,_]
解释: 你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

示例 2:

输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3,_,_,_]
解释: 你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

题解

function removeDuplicates(nums) {
  if (nums.length === 0) return 0;

  const seen = new Set(); // 用于记录已经出现过的元素
  let uniqueIndex = 0; // 用于记录唯一元素的写入位置

  // 遍历数组
  for (let readIndex = 0; readIndex < nums.length; readIndex++) {
    // 如果当前元素还没有出现过,则将该元素复制到 uniqueIndex 位置,并将 uniqueIndex 向前移动
    if (!seen.has(nums[readIndex])) {
      seen.add(nums[readIndex]);
      nums[uniqueIndex] = nums[readIndex];
      uniqueIndex++;
    }
  }

  // 截断数组,只保留唯一元素
  nums.length = uniqueIndex;

  // 返回唯一元素的数量
  return uniqueIndex;
}

我们只需要遍历数组一次,因此时间复杂度为 O(n)。使用了额外的 Set 来存储已经出现过的元素,最坏情况下需要存储所有元素,因此空间复杂度为 O(n)。

删除有序数组中的重复项 II

给你一个有序数组 nums ,请你原地删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在原地并在使用 O(1) 额外空间的条件下完成。

示例 1:

输入: nums = [1,1,1,2,2,3]
输出: 5, nums = [1,1,2,2,3]

解释: 函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 
不需要考虑数组中超出新长度后面的元素。

示例 2:

输入: nums = [0,0,1,1,1,1,2,3,3]
输出: 7, nums = [0,0,1,1,2,3,3]

解释: 函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。
不需要考虑数组中超出新长度后面的元素。

题解

function removeDuplicates(nums) {
  if (nums.length <= 2) return nums.length;

  let writeIndex = 2; // 用于记录写入的位置

  // 遍历数组
  for (let readIndex = 2; readIndex < nums.length; readIndex++) {
    // 如果当前元素与前两个元素不同,或者当前元素与前一个元素相同但前一个元素只出现过一次,
    // 则将该元素复制到 writeIndex 位置,并将 writeIndex 向前移动
    if (
      nums[readIndex] !== nums[writeIndex - 2] ||
      (nums[readIndex] === nums[writeIndex - 1] &&
        nums[writeIndex - 1] !== nums[writeIndex - 2])
    ) {
      nums[writeIndex] = nums[readIndex];
      writeIndex++;
    }
  }

  // 截断数组,只保留最多出现两次的元素
  nums.length = writeIndex;

  // 返回新的数组长度
  return writeIndex;
}

// 示例
const nums = [1, 1, 1, 2, 2, 3];
const k = removeDuplicates(nums);
console.log(k); // 输出: 5
console.log(nums.slice(0, k)); // 输出: [1, 1, 2, 2, 3]

代码解析

  1. 初始化写入指针:

    • writeIndex 用于记录下一个要写入的元素的位置。初始化为 2,因为我们允许每个元素最多出现两次。
  2. 遍历数组:

    • 使用 readIndex 从第三个元素开始遍历数组 nums

    • 对于每一个元素 nums[readIndex],我们需要判断是否应该将它复制到 writeIndex 位置。这里有两个条件:

      • 如果 nums[readIndex] 与前两个元素 nums[writeIndex - 2] 不同,则将该元素复制到 nums[writeIndex] 位置,并将 writeIndex 向前移动一位。
      • 如果 nums[readIndex] 与前一个元素 nums[writeIndex - 1] 相同,但前一个元素 nums[writeIndex - 1] 与前两个元素 nums[writeIndex - 2] 不同,这意味着前一个元素只出现过一次,因此允许当前元素再次出现。
  3. 截断数组:

    • 最终,writeIndex 的值就是新数组的长度。我们将数组的长度设置为 writeIndex,以去除多余的元素。
  4. 返回结果:

    • 返回 writeIndex 作为新数组的长度。

这种方法的时间复杂度为 O(n),因为它只需要遍历一次数组即可完成元素的去重。同时,这种方法的空间复杂度为 O(1),因为我们直接在 nums 数组上进行了修改,而没有使用额外的空间。

多数元素

轮转数组

买卖股票的最佳时机

买卖股票的最佳时机 II

跳跃游戏

跳跃游戏 II

H 指数

O(1) 时间插入、删除和获取随机元素

除自身以外数组的乘积

加油站

分发糖果

接雨水

最后一个单词的长度

最长公共前缀

反转字符串中的单词

Z 字形变换

找出字符串中第一个匹配项的下标

文本左右对齐