사람들의 이름과 이메일 정보를 담고 있는 배열의 배열(2차원 배열)이 있다고 가정해 보겠습니다.
const arr = [
["John", "johnsmith@mail.com", "john00@mail.com"],
["John", "johnnybravo@mail.com"],
["John", "johnsmith@mail.com", "john_newyork@mail.com"],
["Mary", "mary@mail.com"]
];배열의 각 요소는 문자열로 이루어진 하위 배열이며, 첫 번째 요소는 이름, 나머지 요소들은 해당 이름에 속한 이메일 주소들입니다.
문제 정의
이제 이러한 하위 배열들을 하나로 병합하고자 합니다. 두 하위 배열에 공통으로 포함된 이메일이 하나라도 존재한다면, 그 두 하위 배열은 확실히 같은 사람의 것으로 판단할 수 있습니다.
다만 주의할 점이 있습니다. 이름이 같다고 해서 반드시 같은 사람이라고 볼 수 없습니다. 실제로 동명이인일 수 있기 때문입니다. 한 사람이 처음에 소유한 계정의 수는 몇 개든 상관없지만, 한 사람의 모든 계정에는 반드시 동일한 이름이 사용됩니다.
병합이 완료된 후에는 다음과 같은 형식으로 결과를 반환해야 합니다. 각 하위 배열의 첫 번째 요소는 이름이며, 나머지 요소들은 오름차순으로 정렬된 이메일입니다. 하위 배열 자체는 어떤 순서로 반환해도 무방합니다.
따라서 위 입력 배열에 대한 최종 출력은 다음과 같아야 합니다.
const output = [
["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'],
["John", "johnnybravo@mail.com"],
["Mary", "mary@mail.com"]
];여기서 첫 번째와 세 번째 원본 하위 배열은 johnsmith@mail.com이라는 이메일을 공유하기 때문에 하나로 병합되었음을 확인할 수 있습니다.
예시 코드
이 문제를 해결하는 전체 코드는 다음과 같습니다.
const arr = [
["John", "johnsmith@mail.com", "john00@mail.com"],
["John", "johnnybravo@mail.com"],
["John", "johnsmith@mail.com", "john_newyork@mail.com"],
["Mary", "mary@mail.com"]
];
const recusiveMatch = (included, i, tmp, arr, res) => {
for(let j = 1; j < arr[i].length; j += 1) {
let currentEmail = arr[i][j];
if(included.has(currentEmail)) continue;
res.push(currentEmail);
included.add(currentEmail);
let currentAccountIndexes = tmp.get(currentEmail);
for(let c = 0; c < currentAccountIndexes.length; c += 1) {
let currentIndex = currentAccountIndexes[c];
if(i !== currentIndex) {
recusiveMatch(included, currentIndex, tmp, arr, res);
}
}
}
};
const merge = (arr) => {
const tmp = new Map(),
included = new Set(),
res = [];
arr.forEach((account, i) => {
for(let u = 1; u < account.length; u += 1) {
let currentEMail = account[u];
tmp.set(currentEMail, tmp.get(currentEMail) || []);
tmp.get(currentEMail).push(i);
}
});
arr.forEach((account, i) => {
if(!included.has(arr[1])) {
let u = [];
recusiveMatch(included, i, tmp, arr, u);
if(u.length) {
res.push(u);
u.sort();
u.unshift(account[0]);
}
}
});
return res;
};
console.log(merge(arr));코드 작동 방식
핵심 로직을 단계별로 살펴보면 다음과 같습니다.
1단계 — 이메일 인덱스 맵 생성: 먼저 각 이메일을 키(key)로, 해당 이메일이 등장하는 계정의 인덱스 목록을 값(value)으로 하는 Map(tmp)을 만듭니다. 이를 통해 특정 이메일을 공유하는 모든 계정을 빠르게 찾을 수 있습니다.
2단계 — 재귀적 매칭: recusiveMatch 함수는 현재 계정의 이메일을 하나씩 확인하면서, 아직 처리되지 않은 이메일이라면 결과 배열에 추가하고 Set(included)에 기록하여 중복 처리를 방지합니다. 그런 다음 같은 이메일을 공유하는 다른 계정들도 재귀적으로 탐색하여 연결된 모든 계정을 하나의 그룹으로 묶습니다.
3단계 — 정렬 및 결과 조립: 각 그룹이 완성되면 이메일 목록을 sort()로 정렬한 뒤, unshift()를 사용해 이름을 첫 번째 요소로 삽입합니다.
출력 결과
콘솔에 출력되는 결과는 다음과 같습니다.
[
[
'John',
'john00@mail.com',
'john_newyork@mail.com',
'johnsmith@mail.com'
],
[ 'John', 'johnnybravo@mail.com' ],
[ 'Mary', 'mary@mail.com' ]
]결과에서 볼 수 있듯이, 공유된 이메일이 있는 계정들은 하나의 그룹으로 성공적으로 병합되었고, 각 그룹의 이메일은 사전순으로 정렬되어 있습니다. 이 접근 방식은 이메일을 노드로, 공유 관계를 간선으로 보는 그래프 탐색(DFS)과 유사한 구조를 가지므로, 유니온 파인드(Union-Find) 등의 대안적인 방법과 비교해볼 만한 가치가 있습니다.