以下(简化的)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个作为该组的一部分。这使算法变得复杂,而我怀疑有一种简单的方法可以在线性时间内解决此问题。这类问题可能也有名字吗? `
答案 0 :(得分:3)
想法:
联合查找是处理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:name
,phone
和{{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.includes
(name/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
,因此我们并不关心属性的数量。因此,您可以轻松地将另一个address
或fax
添加到该列表,并且仍然可以与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)
完成您所需要的一种方法是将联系人分为几组。
每个组将包含names
,phones
和emails
的列表。
然后遍历联系人,并查看当前联系人是否属于任何组。如果不是,请创建一个新组并设置其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] ]