문제 소개
리터럴 값(숫자, 문자열 등)으로 구성된 배열을 인수로 받는 JavaScript 함수를 작성해야 합니다. 이 함수는 입력 배열에 포함된 각 요소가 몇 번 등장했는지 빈도를 계산한 뒤, 그 결과를 바탕으로 새로운 배열을 생성해 반환해야 합니다.
예를 들어, 입력 배열이 다음과 같다고 가정해 보겠습니다.
const arr = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4];
그렇다면 기대되는 출력 결과는 다음과 같습니다.
const output = [
[5, 3],
[2, 5],
[9, 1],
[4, 1]
];
여기서 각 하위 배열의 첫 번째 요소는 원본 배열의 고유한 값이고, 두 번째 요소는 해당 값이 배열 안에서 등장한 횟수입니다. 즉, 숫자 5는 3번, 숫자 2는 5번, 숫자 9와 4는 각각 1번 등장했다는 의미입니다.
구현 코드
forEach 메서드와 임시 객체를 활용하면 이 문제를 간단하게 해결할 수 있습니다. 전체 코드는 다음과 같습니다.
const arr = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4];
const frequencyArray = (arr = []) => {
const res = [];
arr.forEach(el => {
if (!this[el]) {
this[el] = [el, 0];
res.push(this[el]);
};
this[el][1]++;
}, {});
return res;
};
console.log(frequencyArray(arr));
코드 동작 원리
이 코드의 핵심 로직을 단계별로 살펴보겠습니다.
1. 결과 배열과 카운터 객체 준비
최종 결과를 담을 res 배열을 선언합니다. forEach의 두 번째 인수로 빈 객체 {}를 전달하면, 콜백 함수 내부의 this가 이 객체를 가리키게 되어 각 요소의 개수를 저장하는 카운터 역할을 하게 됩니다.
2. 처음 등장한 요소 처리
배열을 순회하면서 this[el]이 아직 존재하지 않는다면, 해당 값이 처음 등장한 것이므로 [el, 0] 형태의 하위 배열을 만들어 카운터 객체에 저장하고, 동시에 결과 배열 res에 참조를 추가합니다.
3. 등장 횟수 증가
하위 배열이 이미 존재한다면 두 번째 요소(카운트)를 1씩 증가시킵니다. 참조를 공유하기 때문에 카운터 객체의 값을 변경하면 결과 배열의 값도 함께 갱신됩니다.
실행 결과
위 코드를 실행하면 콘솔에 다음과 같은 결과가 출력됩니다.
[ [ 5, 3 ], [ 2, 5 ], [ 9, 1 ], [ 4, 1 ] ]
대안: reduce를 활용한 방법
같은 문제는 reduce 메서드와 Map 객체를 조합해서도 해결할 수 있습니다.
const arr = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4];
const frequencyArray = arr =>
[...arr.reduce((map, el) => map.set(el, (map.get(el) || 0) + 1), new Map())];
console.log(frequencyArray(arr));
Map은 삽입 순서를 유지하므로 요소가 처음 등장한 순서대로 결과가 정렬된다는 장점이 있으며, 코드도 더욱 간결해집니다.
마무리
배열 요소의 빈도수를 계산하는 작업은 데이터 분석, 중복 제거, 통계 처리 등 실무에서 자주 활용되는 패턴입니다. forEach와 임시 객체를 사용하는 방식과 reduce와 Map을 사용하는 방식 모두 시간 복잡도가 O(n)으로 효율적이므로, 상황에 맞게 선택하여 사용하시기 바랍니다.