如何在JavaScript中合并两个数组并去重

1962

我有两个 JavaScript 数组:

var array1 = ["Vijendra","Singh"];
var array2 = ["Singh", "Shakya"];

我希望输出结果是:

var array3 = ["Vijendra","Singh","Shakya"];

输出数组应该删除重复的单词。

我如何在JavaScript中合并两个数组,以便获取每个数组中唯一的项,并以它们插入原始数组的相同顺序返回?


32
在发布新答案之前,请考虑此问题已经有75个以上的答案。请确保您的答案提供的信息不在现有答案中。 - janniks
5
结果为 [1, 2, 3, 4],这行代码使用了 ES6 的 Set 数据结构和展开运算符将两个数组去重合并。 - Denis Giffeler
如果您想要一个更通用的解决方案,也包括深度合并,请查看这个问题。一些答案也涵盖了数组。 - Martin Braun
简而言之 - 合并数组 (ba) : a=a.concat(b); 从数组 a 中删除重复项 (就地操作) : a=a.filter((i,p)=>a.indexOf(i)===p); - ashleedawg
如果你不想再有更多答案,可以关闭问题。 - Janos Vinceller
问题仍然开放,等待不同、创新、前沿的答案。这也是免责声明的原因。 - Rodrigo Rodrigues
92个回答

30

我知道这个问题与对象数组无关,但搜索者最终会到达这里。

因此,为了日后的读者,值得添加一个适当的ES6方法来合并然后删除重复项。

对象数组

var arr1 = [ {a: 1}, {a: 2}, {a: 3} ];
var arr2 = [ {a: 1}, {a: 2}, {a: 4} ];

var arr3 = arr1.concat(arr2.filter( ({a}) => !arr1.find(f => f.a == a) ));

// [ {a: 1}, {a: 2}, {a: 3}, {a: 4} ]

29

避免嵌套循环(O(n^2)),以及 .indexOf()(+O(n))。

function merge(a, b) {
  var hash = {};
  var i;
  
  for (i = 0; i < a.length; i++) {
    hash[a[i]] = true;
  }
  for (i = 0; i < b.length; i++) {
    hash[b[i]] = true;
  }
  return Object.keys(hash);
}

var array1 = ["Vijendra", "Singh"];
var array2 = ["Singh", "Shakya"];

var array3 = merge(array1, array2);

console.log(array3);


2
这相当惊人,特别是如果你正在处理字符串。数字需要额外的步骤来保持它们原本的状态。 如果你完成后不介意(或不关心)一切都变成字符串,那么这个函数会大大优于所有其他选项。做得好。性能结果在这里:http://jsperf.com/merge-two-arrays-keeping-only-unique-values/21 - slickplaid

27

只是提供我的意见。

function mergeStringArrays(a, b){
    var hash = {};
    var ret = [];

    for(var i=0; i < a.length; i++){
        var e = a[i];
        if (!hash[e]){
            hash[e] = true;
            ret.push(e);
        }
    }

    for(var i=0; i < b.length; i++){
        var e = b[i];
        if (!hash[e]){
            hash[e] = true;
            ret.push(e);
        }
    }

    return ret;
}

这是我经常使用的一种方法,它使用一个对象作为哈希查找表来进行重复检查。假设哈希是O(1),那么它的时间复杂度为O(n),其中n为a.length + b.length。我并不知道浏览器如何进行哈希,但它在成千上万的数据点上表现良好。


干得非常漂亮。通过利用关联数组并避免使用indexOf和其他操作的循环,它击败了这个页面上相当多(如果不是全部)的其他结果。http://jsperf.com/merge-two-arrays-keeping-only-unique-values/21 - slickplaid
你的“哈希”是JavaScript中的String()函数。虽然它对于原始值可能有效(尽管在类型之间存在冲突),但它不适用于对象数组。 - Bergi
我使用类似的解决方案,我允许传递一个hashCode函数或传递一个字符串来标识对象中要用作哈希键的属性。 - Robert Baker

23

编辑:

当项目数量较少时,第一个解决方案是最快的。当有超过400个项目时,Set解决方案变得最快。当有100,000个项目时,它比第一个解决方案快一千倍。

考虑到性能只在存在大量项目时才很重要,并且Set解决方案远远比其他方案易读,所以在大多数情况下应该选择它作为正确的解决方案。

以下perf结果是使用少量项目计算的。


基于jsperf,在新数组中合并两个数组的最快方法(如果项目少于400个)如下:

for (var i = 0; i < array2.length; i++)
    if (array1.indexOf(array2[i]) === -1)
      array1.push(array2[i]);

这个慢了17%:

array2.forEach(v => array1.includes(v) ? null : array1.push(v));

这个比较慢,(注:在少于100个项目时。当有很多项目时,速度会快很多)速度慢了45%:

var a = [...new Set([...array1 ,...array2])];

被接受的答案比其他方法慢了55%(而且编写时间更长)(编辑:当有100,000个项目时,它比任何其他方法都慢几个数量级)

var a = array1.concat(array2);
for (var i = 0; i < a.length; ++i) {
    for (var j = i + 1; j < a.length; ++j) {
        if (a[i] === a[j])
            a.splice(j--, 1);
    }
}

https://jsbench.me/lxlej18ydg


感谢您提供易于理解的性能排名百分比数字。最初我正在寻找基于“Set”的选项,因为它很简单。但考虑到我的数据集可能非常大,性能肯定是更重要的考虑因素! - OXiGEN
1
结果发现Set更快,特别是在记录增加时(至少对于Numbers而言)。请参见可运行的测试者:https://dev59.com/BHI-5IYBdhLWcg3w-9wK#66129415。 - OXiGEN
@OXiGEN 是的,不是浏览器实现的问题就是Set已经改进了,或者它取决于数据的类型。我应该在我的答案中写下我的数组初始化:( - Pitouli

20
Array.prototype.merge = function(/* variable number of arrays */){
    for(var i = 0; i < arguments.length; i++){
        var array = arguments[i];
        for(var j = 0; j < array.length; j++){
            if(this.indexOf(array[j]) === -1) {
                this.push(array[j]);
            }
        }
    }
    return this;
};

一个更好的数组合并函数。


4
var test = ['a', 'b', 'c']; console.log(test);将打印出["a", "b", "c", merge: function] - Doubidou
优秀的解决方案。我已经更新了由@slickplaid发布的jsperf测试(http://jsperf.com/merge-two-arrays-keeping-only-unique-values/3),看起来这是它们中最快的一个。 - Cobra
@Cobra 冒昧地说,使用 Chrome 40.0.2214 (截至2015年2月18日最新版本),这个答案比我的慢53%。另一方面,IE11似乎根本没有为我的答案进行优化。:)不过,Chrome手机版仍然很好用。老实说,如果你正在使用 lodash/_,我们大多数人都应该使用真正的答案,它已经在这个列表中排名相当靠前了。 :) - slickplaid
@slickplaid 是的,而且速度快得多,甚至与lodash/_相比也是如此。我可能会在某个时候将我的实现切换成与你类似的东西。 :D - Cobra
什么是数组?它是你的输出保存的地方吗?它保存在数组中吗?我有一个包含多个数组的数组。例如 -> abc = [['a'], ['b'], ['c', 'd', 'e']] ...那么它会像Array.prototype.merge = function(abc){ //code }这样吗? - Techdive
2
不确定indexOf()方法的成本是多少,但这可能是最快的ES5兼容方法。还值得注意的是,不需要变量长度的参数。该方法是可链接的。@slickplaid加载库永远不是“如何在javascript中实现”的答案。当然,许多库都有一个函数来完成这7行工作。 - dehart

18

性能

今天2020年10月15日,我在MacOs HighSierra 10.13.6上使用Chrome v86、Safari v13.1.2和Firefox v81进行了选择解决方案的测试。

结果

对于所有浏览器

  • 解决方案H是最快的/最快的
  • 解决方案L很快
  • 解决方案D对于大型数组在Chrome上最快
  • 解决方案G在小型数组上表现良好
  • 解决方案M对于小型数组最慢
  • 解决方案E对于大型数组最慢

enter image description here

详情

我执行了2个测试用例:

  • 对于2个元素的数组-您可以在此处运行它
  • 对于10000个元素的数组-您可以在此处运行它

对于ABCDEGHJLM, 在下面的代码片段中呈现。

// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#10499519
function A(arr1,arr2) {
  return _.union(arr1,arr2)
}

// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#53149853
function B(arr1,arr2) {
  return _.unionWith(arr1, arr2, _.isEqual);
}

// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#27664971
function C(arr1,arr2) {
  return [...new Set([...arr1,...arr2])]
}

// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#48130841
function D(arr1,arr2) {
  return Array.from(new Set(arr1.concat(arr2)))
}

// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#23080662
function E(arr1,arr2) {
  return arr1.concat(arr2.filter((item) => arr1.indexOf(item) < 0))
}


// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#28631880
function G(arr1,arr2) {
  var hash = {};
  var i;
  
  for (i = 0; i < arr1.length; i++) {
    hash[arr1[i]] = true;
  }
  for (i = 0; i < arr2.length; i++) {
    hash[arr2[i]] = true;
  }
  return Object.keys(hash);
}

// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#13847481
function H(a, b){
    var hash = {};
    var ret = [];

    for(var i=0; i < a.length; i++){
        var e = a[i];
        if (!hash[e]){
            hash[e] = true;
            ret.push(e);
        }
    }

    for(var i=0; i < b.length; i++){
        var e = b[i];
        if (!hash[e]){
            hash[e] = true;
            ret.push(e);
        }
    }

    return ret;
}



// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#1584377
function J(arr1,arr2) {
  function arrayUnique(array) {
      var a = array.concat();
      for(var i=0; i<a.length; ++i) {
          for(var j=i+1; j<a.length; ++j) {
              if(a[i] === a[j])
                  a.splice(j--, 1);
          }
      }

      return a;
  }

  return arrayUnique(arr1.concat(arr2));
}


// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#25120770
function L(array1, array2) {
    const array3 = array1.slice(0);
    let len1 = array1.length;
    let len2 = array2.length;
    const assoc = {};

    while (len1--) {
        assoc[array1[len1]] = null;
    }

    while (len2--) {
        let itm = array2[len2];

        if (assoc[itm] === undefined) { // Eliminate the indexOf call
            array3.push(itm);
            assoc[itm] = null;
        }
    }

    return array3;
}

// https://dev59.com/BHI-5IYBdhLWcg3w-9wK#39336712
function M(arr1,arr2) {
  const comp = f => g => x => f(g(x));
  const apply = f => a => f(a);
  const flip = f => b => a => f(a) (b);
  const concat = xs => y => xs.concat(y);
  const afrom = apply(Array.from);
  const createSet = xs => new Set(xs);
  const filter = f => xs => xs.filter(apply(f));

  const dedupe = comp(afrom) (createSet);

  const union = xs => ys => {
    const zs = createSet(xs);  
    return concat(xs) (
      filter(x => zs.has(x)
       ? false
       : zs.add(x)
    ) (ys));
  }

  return union(dedupe(arr1)) (arr2)
}



// -------------
// TEST
// -------------

var array1 = ["Vijendra","Singh"];
var array2 = ["Singh", "Shakya"];

[A,B,C,D,E,G,H,J,L,M].forEach(f=> {
  console.log(`${f.name} [${f([...array1],[...array2])}]`);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" integrity="sha512-90vH1Z83AJY9DmlWa8WkjkV79yfS2n2Oxhsi2dZbIv0nC4E6m5AbH8Nh156kkM7JePmqD6tcZsfad1ueoaovww==" crossorigin="anonymous"></script>
  
This snippet only presents functions used in performance tests - it not perform tests itself!

以下是Chrome浏览器的测试运行示例:

enter image description here

更新:

我删除了F、I、K三个案例,因为它们会修改输入数组,导致基准测试结果不正确。


你为什么不改进第一个代码片段并消除代码重复呢? - Marco
1
@Marco,我不知道如何在不降低性能或简单性的情况下改进第一个片段 - 但我愿意听取您的解决方案 - 请随意创建新答案,在其中以这种方式改进此解决方案 - 每个人都会很高兴 :) - Kamil Kiełczewski
@KamilKiełczewski:小心!我强烈怀疑测试中存在一个bug。当您添加一个打印数组长度的console.log时,您会发现在大多数情况下,长度为0。感觉像是数组在每次运行之间没有正确重置。然后,合并两个空数组是非常快的操作;)这似乎得到了这个答案https://dev59.com/BHI-5IYBdhLWcg3w-9wK#66129415的确认,其中K解决方案很快,但小于C解决方案(小心;只查看%比较;代码片段中有错误,计时器有误) - Pitouli
我确认了我的怀疑。我更新了测试工作台,所以数组是从未经修改的JSON中解析出来的。显然,每个测试都会慢一点,但这不会影响排名。而且,在Mac Chrome上,K测试比C、D、L和M测试要慢得多。 https://jsbench.me/mpklq0sj6l/1 - Pitouli
@Pitouli,你是对的 - 我更新了答案并删除了更改输入数组F、I、K的解决方案 - 因为基准测试会给出错误的结果(将来有时间时,我会再次尝试基准测试被删除的解决方案)。 - Kamil Kiełczewski

17

为什么不使用对象呢?看起来你试图建模一个集合。但这种方法不会保留顺序。

var set1 = {"Vijendra":true, "Singh":true}
var set2 = {"Singh":true,  "Shakya":true}

// Merge second object into first
function merge(set1, set2){
  for (var key in set2){
    if (set2.hasOwnProperty(key))
      set1[key] = set2[key]
  }
  return set1
}

merge(set1, set2)

// Create set from array
function setify(array){
  var result = {}
  for (var item in array){
    if (array.hasOwnProperty(item))
      result[array[item]] = true
  }
  return result
}

你的意思是不是 if (!set1.hasOwnProperty(key)) - Gumbo
2
我为什么这么说呢?那个条件的目的是忽略可能存在于对象原型中的属性。 - Nick Retallack
在每个用例中都将其转换为对象并不高效。例如,我们可能希望从两个 Object.keys() 数组中获取键的并集。 - OXiGEN

14

对于ES6,只需一行代码:

a = [1, 2, 3, 4]
b = [4, 5]
[...new Set(a.concat(b))]  // [1, 2, 3, 4, 5]

12

最佳解决方案...

您可以通过在浏览器控制台中输入以下命令直接检查...

无重复项

a = [1, 2, 3];
b = [3, 2, 1, "prince"];

a.concat(b.filter(function(el) {
    return a.indexOf(el) === -1;
}));

有重复

["prince", "asish", 5].concat(["ravi", 4])

如果你想要去重,可以尝试从这里找到更好的解决方案 - Shouting Code

[1, 2, 3].concat([3, 2, 1, "prince"].filter(function(el) {
    return [1, 2, 3].indexOf(el) === -1;
}));

在Chrome浏览器控制台中尝试

 f12 > console

输出:

["prince", "asish", 5, "ravi", 4]

[1, 2, 3, "prince"]

它不会从输出数组中删除重复项。 - Shahar

10

我的一分半钱:

Array.prototype.concat_n_dedupe = function(other_array) {
  return this
    .concat(other_array) // add second
    .reduce(function(uniques, item) { // dedupe all
      if (uniques.indexOf(item) == -1) {
        uniques.push(item);
      }
      return uniques;
    }, []);
};

var array1 = ["Vijendra","Singh"];
var array2 = ["Singh", "Shakya"];

var result = array1.concat_n_dedupe(array2);

console.log(result);

它没有使用任何ES6中的新功能,我有哪些遗漏了吗? - Bergi
@Bergi:是的,你说得对。谢谢你指出来。我可能在玩这个脚本时使用了一些ES6函数版本,但现在它包含了存在了几个世纪的indexOf函数。我的错误,抱歉。 - Hero Qu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接