JS匹配两数组中全相等对象

问题: 如何在两个包含对象的 JavaScript 数组中,以最低的时间复杂度找到所有完全相同的对象。

核心思路

最优的方法是利用哈希表(在 JavaScript 中通常通过 SetMap 实现)来优化查找过程。暴力方法是嵌套循环比较,时间复杂度为 O(N*M),其中 N 和 M 分别是两个数组的长度,这在数组很大时效率低下。

我们可以将一个数组中的对象(或其唯一标识)存入一个 Set 中,然后遍历另一个数组,检查其对象是否存在于该 Set 中。在 Set 中检查元素是否存在的时间复杂度平均为 O(1)。

关键问题:如何比较对象?

JavaScript 中直接比较对象 (obj1 === obj2) 是比较它们的内存地址(引用)。即使两个对象内容完全一样,只要它们是分别创建的,=== 也会返回 false

javascript 复制代码
let objA = { id: 1, name: "apple" };
let objB = { id: 1, name: "apple" };
console.log(objA === objB); // false

我们需要一种方法来比较对象的 内容JSON.stringify() 是一个非常实用的方法:

  1. 它可以将一个(简单的,不含函数、Symbol、循环引用等的)JavaScript 对象转换成一个唯一的 JSON 字符串。
  2. 如果两个对象的内容和属性顺序(JSON.stringify 通常会按字母顺序对 key 排序,但依赖具体实现和对象结构,最可靠的是它对 相同 对象总产生 相同 字符串)完全相同,那么它们的 JSON 字符串表示也会完全相同。

最优解法步骤

  1. 选择一个数组进行处理(通常选择较短的数组放入 Set 可以略微优化空间,但对时间复杂度影响不大)。
  2. 创建 Set 存储标识 : 创建一个 Set 用于存储第一个数组中每个对象的 JSON 字符串表示。
  3. 填充 Set : 遍历第一个数组,将每个对象 JSON.stringify 后的字符串添加到 Set 中。
  4. 查找匹配 : 创建一个结果数组 result。遍历第二个数组。
  5. 检查存在 : 对于第二个数组中的每个对象,将其 JSON.stringify 成字符串,并检查该字符串是否存在于之前创建的 Set 中 (set.has(stringifiedObj))。
  6. 添加结果 (去重) : 如果字符串存在于 Set 中,说明找到了一个内容完全相同的对象。为了避免在结果中添加重复的对象(例如,如果同一个对象在 arr2 中出现多次且在 arr1 中也存在),我们可以使用另一个 Set 来跟踪已经添加到 result 数组的对象的 字符串表示 。只有当该字符串表示不在 结果跟踪 Set 中时,才将当前对象(来自 arr2)添加到 result 数组,并将其字符串表示添加到结果跟踪 Set
  7. 返回结果 : 遍历完第二个数组后,检查 result 数组是否为空。如果为空,返回 null;否则,返回 result 数组。

详细代码讲解

javascript 复制代码
/**
 * 查找两个数组中所有完全相同的对象(基于内容比较)。
 * @param {Array<Object>} arr1 第一个对象数组
 * @param {Array<Object>} arr2 第二个对象数组
 * @returns {Array<Object>|null} 包含所有相同对象的数组,如果没有则返回 null。
 */
function findIdenticalObjects(arr1, arr2) {
  // 处理边界情况:如果任一数组无效或为空,不可能有相同对象
  if (!Array.isArray(arr1) || !Array.isArray(arr2) || arr1.length === 0 || arr2.length === 0) {
    return null;
  }

  // 1. 创建 Set 存储第一个数组对象的字符串表示
  const setOfArr1Strings = new Set();

  // 2. 填充 Set
  for (const obj1 of arr1) {
    try {
      // 确保是对象才进行 stringify,处理数组中可能存在的非对象元素
      if (typeof obj1 === 'object' && obj1 !== null) {
        const stringifiedObj1 = JSON.stringify(obj1);
        setOfArr1Strings.add(stringifiedObj1);
      }
    } catch (error) {
      // JSON.stringify 可能因循环引用等失败,可以选择跳过或记录错误
      console.warn("Skipping object in arr1 due to stringify error:", obj1, error);
    }
  }

  // 如果第一个数组处理后 Set 为空(比如都是非对象或 stringify 失败),直接返回 null
  if (setOfArr1Strings.size === 0) {
      return null;
  }

  // 3. 创建结果数组和用于结果去重的 Set
  const identicalObjects = [];
  const resultSet = new Set(); // 用于跟踪已添加到结果中的对象的字符串表示

  // 4. 遍历第二个数组查找匹配
  for (const obj2 of arr2) {
    try {
      // 同样确保是对象
      if (typeof obj2 === 'object' && obj2 !== null) {
        const stringifiedObj2 = JSON.stringify(obj2);

        // 5. 检查是否存在于第一个数组的 Set 中
        // 6. 检查是否已添加到结果中(去重)
        if (setOfArr1Strings.has(stringifiedObj2) && !resultSet.has(stringifiedObj2)) {
          identicalObjects.push(obj2); // 添加的是原始对象 obj2
          resultSet.add(stringifiedObj2); // 标记此对象的字符串已加入结果
        }
      }
    } catch (error) {
      console.warn("Skipping object in arr2 due to stringify error:", obj2, error);
    }
  }

  // 7. 返回结果
  return identicalObjects.length > 0 ? identicalObjects : null;
}

// --- 示例 ---
const array1 = [
  { id: 1, name: "apple", color: "red" },
  { id: 2, name: "banana", color: "yellow" },
  { id: 3, name: "cherry", color: "red" },
  { id: 4, name: "date" },
  { id: 1, name: "apple", color: "red" }, // 重复对象
  null, // 非对象元素
  { id: 5, complex: { value: true, items: [1, 2] } }
];

const array2 = [
  { id: 2, name: "banana", color: "yellow" }, // 相同
  { id: 5, complex: { items: [1, 2], value: true } }, // 相同 (stringify 会处理 key 顺序)
  { id: 6, name: "fig" },
  { id: 1, name: "apple", color: "red" }, // 相同
  { id: 3, name: "cherry", color: "red", extra: false }, // 不同
  undefined, // 非对象元素
  { id: 2, name: "banana", color: "yellow" } // 相同,但已存在
];

const identical = findIdenticalObjects(array1, array2);

console.log("Identical objects found:", identical);
// 预期输出 (对象的顺序可能不同):
// Identical objects found: [
//   { id: 2, name: 'banana', color: 'yellow' },
//   { id: 5, complex: { value: true, items: [ 1, 2 ] } }, // 注意:stringify 处理了内部对象 key 的顺序
//   { id: 1, name: 'apple', color: 'red' }
// ]

const array3 = [{ a: 1 }];
const array4 = [{ b: 2 }];
console.log("No identical objects:", findIdenticalObjects(array3, array4)); // null

const array5 = [{ c: 3 }];
const array6 = [];
console.log("Empty array:", findIdenticalObjects(array5, array6)); // null

const array7 = [{ d: 4 }];
const array8 = [null, undefined];
console.log("Array with only non-objects:", findIdenticalObjects(array7, array8)); // null

时间复杂度分析

  1. 遍历 arr1 (长度 N) 并进行 JSON.stringifySet.add

    • JSON.stringify 的复杂度取决于对象的大小和嵌套深度,假设平均为 O(K),其中 K 是对象内容的复杂度(大致与其字符串长度相关)。
    • Set.add 平均时间复杂度为 O(1)。
    • 此步骤总复杂度约为 O(N * (K + 1)) ≈ O(N*K)
  2. 遍历 arr2 (长度 M) 并进行 JSON.stringify, Set.has, 和可能的 result.push/resultSet.add

    • JSON.stringify 复杂度 O(K)。
    • Set.has 平均时间复杂度 O(1)。
    • result.push (数组末尾添加) O(1)。
    • resultSet.add 平均 O(1)。
    • 此步骤总复杂度约为 O(M * (K + 1 + 1 + 1)) ≈ O(M*K)
  3. 总体时间复杂度 : O(NK + M K) = O((N + M) * K)

如果 K (对象的平均复杂度/大小) 相对于 N 和 M 来说很小或是常数,那么时间复杂度可以近似看作 O(N + M),这是线性时间复杂度,远优于 O(N*M)。

空间复杂度分析

  • setOfArr1Strings: 最多存储 N 个字符串,每个字符串平均长度 L。空间复杂度 O(N * L)。
  • identicalObjects: 最多存储 min(N, M) 个对象。空间复杂度取决于对象大小。
  • resultSet: 最多存储 min(N, M) 个字符串。空间复杂度 O(min(N, M) * L)。
  • 总体空间复杂度 : 大致为 O(N*L + M)O(N + M) (如果 L 和对象大小视为常数)。

注意事项

  • JSON.stringify 的局限性 :
    • 无法序列化函数、Symbol 值、undefined(在对象中会被忽略,在数组中会变 null)。
    • 无法处理循环引用(会抛出错误)。
    • Date 对象会被转换为 ISO 格式字符串。
    • NaNInfinity 会变成 null
    • 对象属性的顺序:虽然 ES2015+ 对某些情况下的属性顺序有定义,但 JSON.stringify 不保证 总是完全遵循。然而,对于 相同结构 的对象,它通常会产生一致的输出,特别是 V8 引擎(Node/Chrome)倾向于按字母顺序排列 key。对于比较目的,这通常是可靠的,但不是语言规范强制保证的。
  • 性能 : 对于非常大或非常深的对象,JSON.stringify 本身可能成为性能瓶颈,但通常比 O(N*M) 的整体比较要快得多。
  • 替代方案 : 如果 JSON.stringify 不适用(例如对象包含函数或需要精确的类型比较),你可能需要实现一个自定义的深比较函数,并可能结合更复杂的哈希策略(例如,基于对象内容的哈希值),但这会显著增加实现的复杂度。对于常见的只包含基本数据类型、数组和嵌套对象的场景,JSON.stringify 是最实用且高效的方法。
相关推荐
kidding7231 分钟前
gitee新的仓库,Vscode创建新的分支详细步骤
前端·gitee·在仓库创建新的分支
听风吹等浪起4 分钟前
基于html实现的课题随机点名
前端·html
leluckys10 分钟前
flutter 专题 六十三 Flutter入门与实战作者:xiangzhihong8Fluter 应用调试
前端·javascript·flutter
kidding72324 分钟前
微信小程序怎么分包步骤(包括怎么主包跳转到分包)
前端·微信小程序·前端开发·分包·wx.navigateto·subpackages
微学AI38 分钟前
详细介绍:MCP(大模型上下文协议)的架构与组件,以及MCP的开发实践
前端·人工智能·深度学习·架构·llm·mcp
liangshanbo12151 小时前
CSS 包含块
前端·css
Mitchell_C1 小时前
语义化 HTML (Semantic HTML)
前端·html
倒霉男孩1 小时前
CSS文本属性
前端·css
晚风3081 小时前
前端
前端
JiangJiang1 小时前
🚀 Vue 人如何玩转 React 自定义 Hook?从 Mixins 到 Hook 的华丽转身
前端·react.js·面试