从JS数组中删除重复值

IT技术 javascript arrays duplicates unique
2020-12-26 16:15:22

我有一个非常简单的 JavaScript 数组,它可能包含也可能不包含重复项。

var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];

我需要删除重复项并将唯一值放入新数组中。

我可以指出我尝试过的所有代码,但我认为它没有用,因为它们不起作用。我也接受 jQuery 解决方案。

类似问题:

6个回答

TL; 博士

使用Set构造函数和传播语法

uniq = [...new Set(array)];

“聪明”但幼稚的方式

uniqueArray = a.filter(function(item, pos) {
    return a.indexOf(item) == pos;
})

基本上,我们遍历数组,对于每个元素,检查该元素在数组中的第一个位置是否等于当前位置。显然,这两个位置对于重复元素是不同的。

使用过滤器回调的第三个(“这个数组”)参数,我们可以避免数组变量的关闭:

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}]

也就是说,如果您的数组只包含基元并且您不关心类型(例如它总是数字),那么这个解决方案是最佳的。

来自两个世界的最好的

通用解决方案结合了这两种方法:它对原语使用哈希查找,对对象使用线性搜索。

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上面变体保留第一个,并使用 theMap保留最后一个:

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))

图书馆

无论下划线罗短跑提供uniq方法。他们的算法基本上类似于上面的第一个片段,归结为:

var result = [];
a.forEach(function(item) {
     if(result.indexOf(item) < 0) {
         result.push(item);
     }
});

这是二次方的,但还有一些不错的附加功能,例如包装 native indexOf、通过键进行 uniqify 的能力(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;
}

这段丑陋的代码与上面的代码段 #3 相同,但速度快了一个数量级(截至 2017 年,它的速度只有两倍 - JS 核心人员做得很好!)

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

这是O(n^2)解决方案,它可以在大型阵列中运行得非常慢......
2021-02-09 16:15:22
试试这个数组:["toString", "valueOf", "failed"]toString并被valueOf完全剥离。使用Object.create(null)代替{}
2021-02-19 16:15:22
filter 和 indexOf 已经在 ECMAScript 5 中引入,所以这在旧的 IE 版本 (<9) 中不起作用。如果您关心这些浏览器,您将不得不使用具有类似功能的库(jQuery、underscore.js 等)
2021-02-25 16:15:22
与其他解决方案相比,有人知道 Set 转换解决方案的速度有多快吗?
2021-03-02 16:15:22
@RoderickObrist 如果您希望您的页面在旧浏览器中工作,您可能会
2021-03-03 16:15:22

使用 jQuery 快速而肮脏:

var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];
var uniqueNames = [];
$.each(names, function(i, el){
    if($.inArray(el, uniqueNames) === -1) uniqueNames.push(el);
});
我同意@NickSteele,但我发现如果您查看投票而不是接受的答案,它确实会随着时间的推移自然发生。随着旧的弃用答案被否决,最佳答案将被吸引到顶部
2021-02-19 16:15:22
let uniqueNames = names.filter((item, pos ,self) => self.indexOf(item) == pos);
2021-02-23 16:15:22
对于那些不使用它的人,不会介意非 jquery 的答案
2021-02-27 16:15:22
由于这已inArray被有信誉的人恢复为原始解决方案,因此我将再次提及:此解决方案是 O(n^2),因此效率低下。
2021-02-27 16:15:22
我真的希望在 2020 年我们可以开始贬值 jQuery 和其他更过时的答案...... Stackoverflow 在这里开始显示一些年龄......
2021-02-28 16:15:22

厌倦了用 for 循环或 jQuery 看到所有不好的例子。Javascript 现在有完美的工具:排序、映射和减少。

在保持现有顺序的同时减少 Uniq

var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];

var uniq = names.reduce(function(a,b){
    if (a.indexOf(b) < 0 ) a.push(b);
    return a;
  },[]);

console.log(uniq, names) // [ 'Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Carl' ]

// one liner
return names.reduce(function(a,b){if(a.indexOf(b)<0)a.push(b);return a;},[]);

更快的 uniq 排序

可能有更快的方法,但这个方法相当不错。

var uniq = names.slice() // slice makes copy of array before sorting it
  .sort(function(a,b){
    return a > b;
  })
  .reduce(function(a,b){
    if (a.slice(-1)[0] !== b) a.push(b); // slice(-1)[0] means last item in array without removing it (like .pop())
    return a;
  },[]); // this empty array becomes the starting value for a

// one liner
return names.slice().sort(function(a,b){return a > b}).reduce(function(a,b){if (a.slice(-1)[0] !== b) a.push(b);return a;},[]);

2015 年更新:ES6 版本:

在 ES6 中,您有 Sets 和 Spread,这使得删除所有重复项变得非常容易和高效:

var uniq = [ ...new Set(names) ]; // [ 'Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Carl' ]

根据出现次数排序:

有人问到根据有多少唯一名称对结果进行排序:

var names = ['Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Nancy', 'Carl']

var uniq = names
  .map((name) => {
    return {count: 1, name: name}
  })
  .reduce((a, b) => {
    a[b.name] = (a[b.name] || 0) + b.count
    return a
  }, {})

var sorted = Object.keys(uniq).sort((a, b) => uniq[a] < uniq[b])

console.log(sorted)
ES6 版本很漂亮。
2021-02-10 16:15:22
这是完美的,因为与过滤器不同,它实际上允许对对象进行一些深度操作
2021-02-14 16:15:22
这个答案值得更多赞。非常漂亮,而且只有 OP 要求的 Javascript 解决方案!谢谢!!
2021-02-19 16:15:22
好的!是否可以根据重复对象的频率对数组进行排序?那么"Nancy"在上面的例子中是移动到修改数组的前面(或后面)吗?
2021-02-19 16:15:22
完美的答案,干净且实用。
2021-02-21 16:15:22

Vanilla JS:使用像集合这样的对象删除重复项

你总是可以尝试将它放入一个对象中,然后遍历它的键:

function remove_duplicates(arr) {
    var obj = {};
    var ret_arr = [];
    for (var i = 0; i < arr.length; i++) {
        obj[arr[i]] = true;
    }
    for (var key in obj) {
        ret_arr.push(key);
    }
    return ret_arr;
}

Vanilla JS:通过跟踪已经看到的值来删除重复项(订单安全)

或者,对于订单安全版本,使用一个对象来存储所有以前看到的值,并在添加到数组之前根据它检查值。

function remove_duplicates_safe(arr) {
    var seen = {};
    var ret_arr = [];
    for (var i = 0; i < arr.length; i++) {
        if (!(arr[i] in seen)) {
            ret_arr.push(arr[i]);
            seen[arr[i]] = true;
        }
    }
    return ret_arr;

}

ECMAScript 6:使用新的 Set 数据结构(顺序安全)

ECMAScript 6 添加了新的Set数据结构,它允许您存储任何类型的值。 Set.values按插入顺序返回元素。

function remove_duplicates_es6(arr) {
    let s = new Set(arr);
    let it = s.values();
    return Array.from(it);
}

用法示例:

a = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];

b = remove_duplicates(a);
// b:
// ["Adam", "Carl", "Jenny", "Matt", "Mike", "Nancy"]

c = remove_duplicates_safe(a);
// c:
// ["Mike", "Matt", "Nancy", "Adam", "Jenny", "Carl"]

d = remove_duplicates_es6(a);
// d:
// ["Mike", "Matt", "Nancy", "Adam", "Jenny", "Carl"]
这条线上发生了什么obj[arr[i]] = true;
2021-02-07 16:15:22
还应该注意的是,您可能会丢失数组的顺序,因为对象不会按顺序保持其属性。
2021-02-11 16:15:22
在更新的浏览器中,您甚至可以执行var c = Object.keys(b). 应该注意的是,这种方法仅适用于字符串,但没关系,这就是最初的问题所要求的。
2021-02-12 16:15:22
@JuanMendes 我创建了一个订单安全版本,如果之前没有看到该值,它只会复制到新数组。
2021-02-19 16:15:22
@kittu,即获取i数组第 th 个元素,并将其放入对象中(用作集合)。键是元素,值是true,这完全是任意的,因为我们只关心对象的键。
2021-03-02 16:15:22

使用数组过滤器和 indexOf 函数的单行版本:

arr = arr.filter(function (value, index, array) { 
    return array.indexOf(value) === index;
});
@web_dev:它没有!!我已经更正了之前破坏代码的编辑。希望它现在更有意义。谢谢提问!
2021-02-14 16:15:22
很棒的 SVG 图标 :) 你给了我灵感,也是一个很好的答案。
2021-02-17 16:15:22
小心解释它如何消除欺骗?
2021-03-02 16:15:22
正如@CaseyKuball 所建议的那样,这个解决方案实际上非常慢 - 请参阅stackoverflow.com/questions/67424599/...
2021-03-06 16:15:22
不幸的是,如果这是一个大数组,它的性能很差——arr.indexOf 是 O(n),这使得这个算法 O(n^2)
2021-03-08 16:15:22