从JS数组中删除重复的值

67 浏览
0 Comments

从JS数组中删除重复的值

这个问题已经有答案了

获取 JavaScript 数组中的所有唯一值(去重)

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

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

我需要移除重复的值,并将唯一的值放到一个新的数组中。

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

类似的问题:

admin 更改状态以发布 2023年5月21日
0
0 Comments

使用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);
});

0
0 Comments

简要总结

使用 Set 构造函数和展开语法

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

话虽如此,如果您的数组仅包含原始值并且不关心类型(例如,它始终是数字),则此解决方案是最佳的。

两种世界中的最佳

一种通用解决方案结合了两种方法:对原始类型使用哈希查找,对对象使用线性查找。

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

0