从JS数组中删除重复的值
从JS数组中删除重复的值
这个问题已经有答案了:
我有一个非常简单的 JavaScript 数组,它可能包含重复的值,也可能不包含。
var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];
我需要移除重复的值,并将唯一的值放到一个新的数组中。
我可以指向所有我尝试过的代码,但我认为这是没有用的,因为它们不起作用。我也接受 jQuery 的解决方案。
类似的问题:
简要总结
uniq = [...new Set(array)];
(注意,变量var uniq将成为一个数组…… new Set()将其转换为一个集合,但[...]将其再次转换为一个数组)
“聪明但幼稚”的方式
uniqueArray = a.filter(function(item, pos) { return a.indexOf(item) == pos; })
基本上,我们遍历数组,对于每个元素,检查其数组中的第一个位置是否等于当前位置。显然,对于重复的元素,这两个位置是不同的。
使用过滤回调函数的第3个(“此数组”)参数,可以避免对数组变量的闭包:
uniqueArray = a.filter(function(item, pos, self) { return self.indexOf(item) == pos; })
尽管简洁,但此算法对于大型数组并不特别高效(二次时间)。
散列表来拯救
function uniq(a) { var seen = {}; return a.filter(function(item) { return seen.hasOwnProperty(item) ? false : (seen[item] = true); }); }
这通常是这样完成的。思想是将每个元素放入散列表中,然后立即检查其存在性。这使我们拥有线性时间,但至少有两个缺点:
- 由于JavaScript中的哈希键只能是字符串或符号,因此此代码不区分数字和“数字字符串”。也就是说,
uniq([1,“1”])
将只返回[1]
- 出于同样的原因,所有对象都将被视为相等:
uniq([{foo:1},{foo:2}])
将只返回[{foo:1}]
。 li>
话虽如此,如果您的数组仅包含原始值并且不关心类型(例如,它始终是数字),则此解决方案是最佳的。
两种世界中的最佳
一种通用解决方案结合了两种方法:对原始类型使用哈希查找,对对象使用线性查找。
function uniq(a) { var prims = {"boolean":{}, "number":{}, "string":{}}, objs = []; return a.filter(function(item) { var type = typeof item; if(type in prims) return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); else return objs.indexOf(item) >= 0 ? false : objs.push(item); }); }
排序|去重
另一个选择是先对数组进行排序,然后删除与前一个相等的每个元素:
function uniq(a) { return a.sort().filter(function(item, pos, ary) { return !pos || item != ary[pos - 1]; }); }
同样,这无法用于对象(因为所有对象对于sort
来说都是相等的)。此外,我们会在副作用中默默地更改原始数组 - 不好! 但是,如果您的输入已排序,则可以采用这种方式(只需从上面删除sort
即可)。
按 ...唯一
有时根据某些标准而不仅仅是相等的要求将列表唯一化,例如,过滤不同但具有某些属性的对象。通过传递回调,可以优雅地完成这个过程。将此“键”回调应用于每个元素,并删除具有相等“键”的元素。由于key
预期返回原始类型,因此散列表在此处运作良好:
function uniqBy(a, key) { var seen = {}; return a.filter(function(item) { var k = key(item); return seen.hasOwnProperty(k) ? false : (seen[k] = true); }) }
特别有用的key()
是JSON.stringify
其将删除在物理上不同但“看起来”相同的对象:
a = [[1,2,3], [4,5,6], [1,2,3]] b = uniqBy(a, JSON.stringify) console.log(b) // [[1,2,3], [4,5,6]]
如果key
不是原始的,您必须采用线性查找方法:
function uniqBy(a, key) { var index = []; return a.filter(function (item) { var k = key(item); return index.indexOf(k) >= 0 ? false : index.push(k); }); }
在ES6中,您可以使用Set
:
function uniqBy(a, key) { let seen = new Set(); return a.filter(item => { let k = key(item); return seen.has(k) ? false : seen.add(k); }); }
或者使用Map
:
function uniqBy(a, key) { return [ ...new Map( a.map(x => [key(x), x]) ).values() ] }
两者都适用于非原始键。
第一个还是最后一个?
当通过键删除对象时,您可能希望保留“相等”对象的第一个或最后一个。
使用上面的 Set
变量来保留第一个元素,使用 Map
来保留最后一个元素:
function uniqByKeepFirst(a, key) { let seen = new Set(); return a.filter(item => { let k = key(item); return seen.has(k) ? false : seen.add(k); }); } function uniqByKeepLast(a, key) { return [ ...new Map( a.map(x => [key(x), x]) ).values() ] } // data = [ {a:1, u:1}, {a:2, u:2}, {a:3, u:3}, {a:4, u:1}, {a:5, u:2}, {a:6, u:3}, ]; console.log(uniqByKeepFirst(data, it => it.u)) console.log(uniqByKeepLast(data, it => it.u))
库
无论是 underscore 还是 Lo-Dash 都提供了 uniq
方法。他们的算法基本上与上面的第一个代码片段相似,并可归结为以下内容:
var result = []; a.forEach(function(item) { if(result.indexOf(item) < 0) { result.push(item); } });
这是二次方的,但还有其他好东西,例如包装本地的 indexOf
、能够通过键 (iteratee
) 使元素唯一化,并且对已排序的数组进行了优化。
如果你在使用 jQuery,不能忍受没有美元符号,它将变成这样:
$.uniqArray = function(a) { return $.grep(a, function(item, pos) { return $.inArray(item, a) === pos; }); }
再次说明,这是第一个代码片段的变化。
性能
在 JavaScript 中,函数调用代价高,因此上述解决方案虽然精简,但并不特别高效。为了达到最大的性能,用循环替换 filter
并消除其他函数调用:
function uniq_fast(a) { var seen = {}; var out = []; var len = a.length; var j = 0; for(var i = 0; i < len; i++) { var item = a[i]; if(seen[item] !== 1) { seen[item] = 1; out[j++] = item; } } return out; }
这段丑陋的代码与上面的第三个片段执行相同的结果(截至 2017 年,它仅仅快了两倍——JS 核心团队做得很好!)
function uniq(a) { var seen = {}; return a.filter(function(item) { return seen.hasOwnProperty(item) ? false : (seen[item] = true); }); } function uniq_fast(a) { var seen = {}; var out = []; var len = a.length; var j = 0; for(var i = 0; i < len; i++) { var item = a[i]; if(seen[item] !== 1) { seen[item] = 1; out[j++] = item; } } return out; } ///// var r = [0,1,2,3,4,5,6,7,8,9], a = [], LEN = 1000, LOOPS = 1000; while(LEN--) a = a.concat(r); var d = new Date(); for(var i = 0; i < LOOPS; i++) uniq(a); document.write('uniq, ms/loop: ' + (new Date() - d)/LOOPS) var d = new Date(); for(var i = 0; i < LOOPS; i++) uniq_fast(a); document.write('uniq_fast, ms/loop: ' + (new Date() - d)/LOOPS)
ES6
ES6 提供了 Set 对象,使事情变得更加容易:
function uniq(a) { return Array.from(new Set(a)); }
或者
let uniq = a => [...new Set(a)];
请注意,与 Python 不同,在 ES6 中,集合按插入顺序迭代,因此此代码保留了原始数组的顺序。
然而,如果您需要一个具有唯一元素的数组,为什么不从一开始就使用集?
生成器
可以基于相同的基础构建“懒惰”的、基于生成器的版本uniq
:
- 从参数中获取下一个值
- 如果已经看到了它,跳过它
- 否则,产生它并将它添加到已经看到的值的集合中
function* uniqIter(a) { let seen = new Set(); for (let x of a) { if (!seen.has(x)) { seen.add(x); yield x; } } } // example: function* randomsBelow(limit) { while (1) yield Math.floor(Math.random() * limit); } // note that randomsBelow is endless count = 20; limit = 30; for (let r of uniqIter(randomsBelow(limit))) { console.log(r); if (--count === 0) break } // exercise for the reader: what happens if we set `limit` less than `count` and why