如何使用 Set 来提高程式码的效能
为了保证的可读性,本文采用意译而非直译。我确信有很多开发人员坚持使用基本的全域性物件:数字,字串,物件,阵列和布林值。对于许多用例,这些都是需要的。 但是如果想让你的程式码尽可能快速和可扩充套件,那么这些基本型别并不总是足够好。
在本文中,我们将讨论JS 中Set物件如何让程式码更快—特别扩充套件性方便。 Array 和Set工作方式存在大量的交叉。但是使用Set会比Array在程式码执行速度更有优势。
想阅读更多优质文章请猛戳GitHub部落格,一年百来篇优质文章等着你!
Set 有何不同
最根本的区别是阵列是一个索引集合,这说明阵列中的资料值按索引排序。
const arr = [A, B, C, D];
console.log(arr.indexOf(A)); // Result: 0
console.log(arr.indexOf(C)); // Result: 2
复制程式码
相比之下,set是一个键的集合。set不使用索引,而是使用键对资料排序。set 中的元素按插入顺序是可迭代的,它不能包含任何重复的资料。换句话说,set中的每一项都必须是惟一的。
主要的好处是什么
set 相对于阵列有几个优势,特别是在执行时间方面:
检视元素:使用indexOf()或includes()检查阵列中的项是否存在是比较慢的。删除元素:在Set中,可以根据每项的的 value 来删除该项。在阵列中,等价的方法是使用基于元素的索引的splice()。与前一点一样,依赖于索引的速度很慢。储存 NaN:不能使用indexOf()或 includes() 来查询值 NaN,而 Set 可以储存此值。删除重复项:Set物件只储存惟一的值,如果不想有重复项存在,相对于阵列的一个显著优势,因为阵列需要额外的程式码来处理重复。时间复杂度?
阵列用来搜寻元素的方法时间复杂度为0(N)。换句话说,执行时间的增长速度与资料大小的增长速度相同。
相比之下,Set用于搜寻、删除和插入元素的方法的时间复杂度都只有O(1),这意味着资料的大小实际上与这些方法的执行时间无关。
Set 究竟有多快?
虽然执行时间可能会有很大差异,具体取决于所使用的系统,所提供资料的大小以及其他变数,但我希望我的测试结果能够让你真实地了解Set的速度。 我将分享三个简单的测试和我得到的结果。
准备测试
在执行任何测试之前,建立一个数组和一个 Set,每个阵列和 Set 都有100万个元素。为了简单起见,我从0开始,一直数到999999。
let arr = [], set = new Set(), n = 1000000;
for (let i = 0; i arr.push(i);
set.add(i);
}
复制程式码
测试1:查询元素
我们搜寻数字123123
let result;
console.time(\Array\);
result = arr.indexOf(123123) !== -1;
console.timeEnd(\Array\);
console.time(\Set\);
result = set.has(123123);
console.timeEnd(\Set\);
复制程式码
Array: 0.173msSet: 0.023msSet 速度快了7.54倍
测试2:新增元素
console.time(\Array\);
arr.push(n);
console.timeEnd(\Array\);
console.time(\Set\);
set.add(n);
console.timeEnd(\Set\);
复制程式码
Array: 0.018msSet: 0.003msSet 速度快了6.73倍
测试3:删除元素
最后,删除一个元素,由于阵列没有内建方法,首先先建立一个辅助函式:
const deleteFromArr = (arr, item) => {
let index = arr.indexOf(item);
return index !== -1 && arr.splice(index, 1);
};
复制程式码
这是测试的程式码:
console.time(\Array\);
deleteFromArr(arr, n);
console.timeEnd(\Array\);
console.time(\Set\);
set.delete(n);
console.timeEnd(\Set\);
复制程式码
Array: 1.122msSet: 0.015msSet 速度快了74.13倍
总的来说,我们可以看到,使用Set 极大地改善执行时间。再来看看一些Set有用的实际例子。
案例1:从阵列中删除重复的值
如果想快速地从阵列中删除重复的值,可以将其转换为一个 Set。这是迄今为止过滤惟一值最简洁的方法:
const duplicateCollection = [\A\, \B\, \B\, \C\, \D\, \B\, \C\];
// 将阵列转换为 Set
let uniqueCollection = new Set(duplicateCollection);
console.log(uniqueCollection) // Result: Set(4) {A, B, C, D}
// 值储存在阵列中
let uniqueCollection = [...new Set(duplicateCollection)];
console.log(uniqueCollection) // Result: [A, B, C, D]
复制程式码
案例2:Google面试问题
问题:
给定一个整数无序阵列和变数 sum,如果存在阵列中任意两项和使等于 sum 的值,则返回true。否则,返回false。例如,阵列[3,5,1,4]和 sum = 9,函式应该返回true,因为4 + 5 = 9。
解答
解决这个问题的一个很好的方法是遍历阵列,建立 Set储存相对差值。
当我们遇到3时,我们可以把6加到Set中, 因为我们知道我们需要找到9的和。然后,每当我们接触到阵列中的新值时,我们可以检查它是否在 Set 中。当遇到5时,在 Set 加上4。最后,当我们最终遇到4时,可以在Set中找到它,就返回true。
const findSum = (arr, val) => {
let searchValues = new Set();
searchValues.add(val - arr[0]);
for (let i = 1, length = arr.length; i let searchVal = val - arr[i];
if (searchValues.has(arr[i])) {
return true;
} else {
searchValues.add(searchVal);
}
};
return false;
};
复制程式码
简洁的版本:
const findSum = (arr, sum) =>
arr.some((set => n => set.has(n) !set.add(sum - n))(new Set));
复制程式码
因为Set.prototype.has()的时间复杂度仅为O(1),所以使用 Set 来代替阵列,最终使整个解决方案的线性执行时为O(N)。
如果使用 Array.prototype.indexOf()或Array.prototype.includes(),它们的时间复杂度都为 O(N),则总执行时间将为O(N²),慢得多!