问题: 如何在两个包含对象的 JavaScript 数组中,以最低的时间复杂度找到所有完全相同的对象。
核心思路
最优的方法是利用哈希表(在 JavaScript 中通常通过 Set
或 Map
实现)来优化查找过程。暴力方法是嵌套循环比较,时间复杂度为 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()
是一个非常实用的方法:
- 它可以将一个(简单的,不含函数、Symbol、循环引用等的)JavaScript 对象转换成一个唯一的 JSON 字符串。
- 如果两个对象的内容和属性顺序(
JSON.stringify
通常会按字母顺序对 key 排序,但依赖具体实现和对象结构,最可靠的是它对 相同 对象总产生 相同 字符串)完全相同,那么它们的 JSON 字符串表示也会完全相同。
最优解法步骤
- 选择一个数组进行处理(通常选择较短的数组放入 Set 可以略微优化空间,但对时间复杂度影响不大)。
- 创建 Set 存储标识 : 创建一个
Set
用于存储第一个数组中每个对象的 JSON 字符串表示。 - 填充 Set : 遍历第一个数组,将每个对象
JSON.stringify
后的字符串添加到Set
中。 - 查找匹配 : 创建一个结果数组
result
。遍历第二个数组。 - 检查存在 : 对于第二个数组中的每个对象,将其
JSON.stringify
成字符串,并检查该字符串是否存在于之前创建的Set
中 (set.has(stringifiedObj)
)。 - 添加结果 (去重) : 如果字符串存在于
Set
中,说明找到了一个内容完全相同的对象。为了避免在结果中添加重复的对象(例如,如果同一个对象在arr2
中出现多次且在arr1
中也存在),我们可以使用另一个Set
来跟踪已经添加到result
数组的对象的 字符串表示 。只有当该字符串表示不在 结果跟踪Set
中时,才将当前对象(来自arr2
)添加到result
数组,并将其字符串表示添加到结果跟踪Set
。 - 返回结果 : 遍历完第二个数组后,检查
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
时间复杂度分析
-
遍历
arr1
(长度 N) 并进行JSON.stringify
和Set.add
:JSON.stringify
的复杂度取决于对象的大小和嵌套深度,假设平均为 O(K),其中 K 是对象内容的复杂度(大致与其字符串长度相关)。Set.add
平均时间复杂度为 O(1)。- 此步骤总复杂度约为 O(N * (K + 1)) ≈ O(N*K)。
-
遍历
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)。
-
总体时间复杂度 : 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 格式字符串。NaN
和Infinity
会变成null
。- 对象属性的顺序:虽然 ES2015+ 对某些情况下的属性顺序有定义,但
JSON.stringify
不保证 总是完全遵循。然而,对于 相同结构 的对象,它通常会产生一致的输出,特别是 V8 引擎(Node/Chrome)倾向于按字母顺序排列 key。对于比较目的,这通常是可靠的,但不是语言规范强制保证的。
- 无法序列化函数、
- 性能 : 对于非常大或非常深的对象,
JSON.stringify
本身可能成为性能瓶颈,但通常比 O(N*M) 的整体比较要快得多。 - 替代方案 : 如果
JSON.stringify
不适用(例如对象包含函数或需要精确的类型比较),你可能需要实现一个自定义的深比较函数,并可能结合更复杂的哈希策略(例如,基于对象内容的哈希值),但这会显著增加实现的复杂度。对于常见的只包含基本数据类型、数组和嵌套对象的场景,JSON.stringify
是最实用且高效的方法。