JavaScript中的最佳算法分组数据

时间:2018-11-20 09:18:43

标签: javascript algorithm grouping lodash

以下(简化的)json数据类型定义了一个联系人:

{
  id:   number;
  name: string;
  phone: string;
  email: string
}

有以下一组数据:

+---+----------+-------------+---------------------------+ 
|id | name     | phone       |email                      | 
+---+----------+-------------+---------------------------+
|1  | John     | 11111111    |aaaa@test.com              | 
|2  | Marc     | 22222222    |bbbb@test.com              | 
|3  | Ron      | 99999999    |aaaa@test.com              |
|4  | Andrew   | 55555555    |dddd@test.com              |
|5  | Wim      | 99999999    |gggg@test.com              |
|6  | Marc     | 33333333    |cccc@test.com              |
|7  | Dan      | 44444444    |cccc@test.com              |
+---+----------+-------------+---------------------------+

目标是根据以下约束条件,使用javascript(可选地在lodash中,但主要思想是使算法清晰)找到属于同一组的组:当以下任一条件相同时,联系人属于组:姓名,电话或电子邮件。结果显示ID在数组中分组为数组。一组1个联系人将被忽略。

在上面的示例中,这意味着ID为1,3,5的联系人属于同一联系人,因为1,3共享相同的电子邮件,而3和5共享相同的电话号码。同样,2,6,7:2和6具有相同的名称,而6和7具有相同的电子邮件。 5没有任何共同点。 因此,预期结果是:
[[1,3,5], [2,6,7]]

背景: 一种有效的解决方案是遍历每个项目,然后检查名称,电子邮件或电话是否相同,以检查列表的其余部分。如果是这样,将它们分组并从列表中删除(在示例中,我们将1与列表中的所有项目进行比较,仅找到3)。问题在于,还需要再次检查这些组的下一个项目,因为在这种情况下,尚未检测到5个作为该组的一部分。这使算法变得复杂,而我怀疑有一种简单的方法可以在线性时间内解决此问题。这类问题可能也有名字吗? `

4 个答案:

答案 0 :(得分:3)

想法:

  • 从0组开始
  • 迭代您的联系人列表
  • 检查是否有包含联系人姓名,电话或电子邮件的群组。将这些组的所有成员合并为一个组。然后将自己添加到该组中。如果不是,请从自己开始创建新群组,然后将名称,电话和电子邮件群组设置为您自己。

联合查找是处理disjoint sets合并的有效结构。来自here的代码。由于它使用路径压缩和按等级合并,因此您可以认为整个代码的联系数量是线性的。

var data = [
      {id:1,name:'John',phone:'11111111',email:'aaaa@test.com'},
      {id:2,name:'Marc',phone:'99999999',email:'bbbb@test.com'},
      {id:3,name:'Ron',phone:'99999999',email:'aaaa@test.com'},
      {id:4,name:'Andrew',phone:'55555555',email:'dddd@test.com'},
      {id:5,name:'Wim',phone:'99999999',email:'gggg@test.com'},
      {id:6,name:'Marc',phone:'33333333',email:'cccc@test.com'},
      {id:7,name:'Dan',phone:'44444444',email:'cccc@test.com'}
];

// UNION-FIND structure, with path comression and union by rank

var UNIONFIND = (function () {
    
    function _find(n)
    {
        if(n.parent == n) return n;	
        n.parent = _find(n.parent);	
        return n.parent;
    }
    
    return {
        makeset:function(id){    
            var newnode = {
                parent: null,
                id: id,
                rank: 0
            };
            newnode.parent = newnode;            
            return newnode;
        },
    
        find: _find,
     
        combine: function(n1, n2) {                                    
            var n1 = _find(n1);
            var n2 = _find(n2);
            
            if (n1 == n2) return;
        
            if(n1.rank < n2.rank)
            {
                n2.parent = n2;
                return n2;
            }
            else if(n2.rank < n1.rank)
            {
                n2.parent = n1;
                return n1;
            }
            else
            {
                n2.parent = n1;
                n1.rank += 1;
                return n1;
            }
        }
    };
})();

var groupHash = {name: {}, phone: {}, email: {}}
var groupNodes = []

data.forEach(function(contact){
  var group = UNIONFIND.makeset(contact.id);
  var groups = new Set();
  ["name", "phone", "email"].forEach(function(attr){
    if (groupHash[attr].hasOwnProperty(contact[attr])) groups.add(groupHash[attr][contact[attr]])
  });
  
  groups = Array.from(groups);
  groups.push(group);
  groupNodes.push(group);
  
  for(var i = 1; i < groups.length; i++) {
    UNIONFIND.combine(groups[0], groups[i]);
  }  
  
  ["name", "phone", "email"].forEach(function(attr){
      groupHash[attr][contact[attr]] = groups[0];
  });
  
})

var contactsInGroup = {}


groupNodes.forEach(function(group){
    var groupId = UNIONFIND.find(group).id;
    
    if (contactsInGroup.hasOwnProperty(groupId) == false) {
      contactsInGroup[groupId] = [];
    }
    
    contactsInGroup[groupId].push(group.id);
})

var result = Object.values(contactsInGroup).filter(function(list){
 return list.length > 1
})

console.log(result)

答案 1 :(得分:2)

在每个n条目上进行迭代,然后在与之匹配的m组列表中进行迭代的任何答案都将具有O(n*m)的最差时间性能(没有任何一项匹配的条目)。

任何在每个条目上然后在组上进行迭代并使用数组测试q选项之间的匹配值的答案将进一步为每次匹配支付O(q)。在最坏的情况下,假设所有电子邮件都相同而所有电话都不同,这将意味着O(n*m)

我认为这个答案是O(n),因为假设要匹配的字段数是一个常数(在这种情况下,是3:namephone和{{1} }),主循环中的所有操作(每个条目运行一次)为email

要解决一个事实,那就是在过程的后期,我们可能会发现两个(甚至3个)组之间的桥梁,因为条目可以在不同的字段上与来自不同组的条目进行匹配,因此存在额外的复杂性。这可能会发生几次。为了避免在主循环中不得不重建组,我们将合并留到最后,在该处我们首先构建一个what-group-ends-up-where的映射,然后最后将所有条目ID移至它们的最终组。所有这些都可以在O(1)中完成,其中的组数为m;在将条目ID实际复制到合并的组时需要额外的O(m):总的来说,我们仍处于O(n)领域。

最后一行从合并的组中构建ID数组,并过滤​​出不超过1个元素的所有ID。

O(n)

答案 2 :(得分:0)

这是您可以选择的另一条建议。这个想法是使用一个Array.reduce来对id进行分组,并将所有值(vls)和组合结果(ids)保留在accumulator object中。

通过这种方式,您可以使用Array.some + Array.includesname/phone/email函数的作用)轻松比较getGroupId

一旦您进行分组并获得几乎最终结果,只需prettify除去其中一个的length组,然后仅选择其余的ids个数组:

var data = [ {id:1,name:'John',phone:'11111111',email:'aaaa@test.com'}, {id:2,name:'Marc',phone:'22222222',email:'bbbb@test.com'}, {id:3,name:'Ron',phone:'99999999',email:'aaaa@test.com'}, {id:4,name:'Andrew',phone:'55555555',email:'dddd@test.com'}, {id:5,name:'Wim',phone:'99999999',email:'gggg@test.com'}, {id:6,name:'Marc',phone:'33333333',email:'cccc@test.com'}, {id:7,name:'Dan',phone:'44444444',email:'cccc@test.com'} ];

const getGroupId = (obj, vals) => Object.entries(obj)
   .find(([k,v]) => v.vls.some(x => vals.includes(x))) || []

const group = d => d.reduce((r, c) => {
   let values = Object.values(c), groupID = getGroupId(r, values)[0]
	
   if(!groupID)	
      r[c.id] = ({ vls: values, ids: [...r[c.id] || [], c.id] })
   else {
      r[groupID] = ({
         vls: [...r[groupID].vls, ...values], ids: [...r[groupID].ids, c.id]
      })
   }
   return r
}, {})

const prettify = grp => Object.values(grp).reduce((r,c) => {
   if(c.ids.length > 1)
     r.push(c.ids)
     return r
}, [])

console.log(prettify(group(data)))

要注意的一件事是,由于我们执行Object.values,因此我们并不关心属性的数量。因此,您可以轻松地将另一个addressfax添加到该列表,并且仍然可以与zero code changes一起使用。

根据反馈,这里是另一个版本,其工作方式略有不同:

var data = [ {id:1,name:'John',phone:'11111111',email:'aaaa@test.com'}, {id:2,name:'Marc',phone:'22222222',email:'bbbb@test.com'}, {id:3,name:'Ron',phone:'99999999',email:'aaaa@test.com'}, {id:4,name:'Andrew',phone:'55555555',email:'dddd@test.com'}, {id:5,name:'Wim',phone:'99999999',email:'gggg@test.com'}, {id:6,name:'Marc',phone:'33333333',email:'cccc@test.com'}, {id:7,name:'Dan',phone:'44444444',email:'cccc@test.com'} ];
var testData = [{ id: 1, name: 'John', phone: '1', email: 'a' }, { id: 2, name: 'Marc', phone: '2', email: 'b' }, { id: 3, name: 'Ron', phone: '1', email: 'b' }]; 

const getGroupId = (obj, vals) => Object.entries(obj)
  .find(([k,v]) => v.vls.some(x => vals.includes(x))) || []

const group = d => d.reduce((r,c,i,a) => {
  let values = Object.values(c), groupID = !i ? i : getGroupId(r, values)[0]

  if (!groupID) {		
    let hits = a.filter(x => 
       x.id != c.id && values.some(v => Object.values(x).includes(v)))
    hits.forEach(h => 
       r[c.id] = ({ vls: [...values, ...Object.values(h)], ids: [c.id, h.id] }))
  }
  else
    r[groupID] = r[groupID].ids.includes(c.id) ? r[groupID] : 
      ({ vls: [...r[groupID].vls, ...values], ids: [...r[groupID].ids, c.id] })      
  return r
}, {})

const prettify = grp => Object.values(grp).reduce((r, c) => {
  if (c.ids.length > 1)
    r.push(c.ids)
  return r
}, [])

console.log(prettify(group(data)))      // OP data
console.log(prettify(group(testData)))  // Test data

此版本的原因是由于testData提供的@Mark的第二个元素与第一个元素不匹配,但与第三个元素匹配,而第三个元素实际上与第一个元素匹配...因此它们都应该是命中。

要找到一个匹配项,我们首先要寻找相同初始匹配项的匹配项,然后推入相同的组,这样我们就可以拥有最大数量的匹配数据。

结果是,一旦我们获得具有第一个元素的第一个组,我们也找到并推入第三个元素,从那里匹配第二个元素要容易得多。逻辑稍微复杂一点,我会想象性能会降低。

答案 3 :(得分:-1)

完成您所需要的一种方法是将联系人分为几组。 每个组将包含namesphonesemails的列表。

然后遍历联系人,并查看当前联系人是否属于任何组。如果不是,请创建一个新组并设置其names/phones/emails,以使下一个联系人可以进入同一组。

var data = [
      {id:1,name:'John',phone:'11111111',email:'aaaa@test.com'},
      {id:2,name:'Marc',phone:'22222222',email:'bbbb@test.com'},
      {id:3,name:'Ron',phone:'99999999',email:'aaaa@test.com'},
      {id:4,name:'Andrew',phone:'55555555',email:'dddd@test.com'},
      {id:5,name:'Wim',phone:'99999999',email:'gggg@test.com'},
      {id:6,name:'Marc',phone:'33333333',email:'cccc@test.com'},
      {id:7,name:'Dan',phone:'44444444',email:'cccc@test.com'}
];

var groups = [];

data.forEach(function(person){
  var phone = person.phone;
  var email = person.email;
  var name = person.name;
  var id = person.id;
  var found = false;
  groups.forEach(function(g){
    if(    g.names.indexOf(name) > -1 
        || g.phones.indexOf(phone)>-1 
        || g.emails.indexOf(email)>-1) {
      found = true;
      g.names.push(name);
      g.phones.push(phone);
      g.emails.push(email);
      g.people.push(id);
    }
  });
  if(!found) {
      groups.push({names:[name],phones:[phone],emails:[email],people:[id]});
  }
  
  
});
var output=[];
groups.forEach(function(g){
  output.push(g.people);
});
console.log(output);   //[ [1,3,5] , [2,6,7] , [4] ]