js 배열에서 특정 값이 몇번 나오는지 찾는 방법
for 반복문, filter(), reduce()
1. for 반복문 활용
가장 기본적인 방법은 for 반복문을 사용하여 배열의 모든 요소를 순회하며, 원하는 값과 일치할 때마다 개수를 증가시키는 것입니다. 직관적이고 이해하기 쉽다는 장점이 있습니다.
const fruits = ['apple', 'banana', 'apple', 'orange', 'apple'];
const targetFruit = 'apple';
let count = 0;
for (let i = 0; i < fruits.length; i++) {
if (fruits[i] === targetFruit) {
count++;
}
}
console.log(`${targetFruit}의 개수: ${count}`); // 출력: apple의 개수: 3
설명: 배열 fruits를 처음부터 끝까지 순회하면서, 각 요소가 targetFruit('apple')과 같은지 확인합니다. 같으면 count 변수를 1씩 증가시킵니다.
2. filter() 함수 활용
filter() 함수는 배열에서 특정 조건을 만족하는 요소들만 모아 새로운 배열을 반환하는 강력한 배열 내장 함수입니다. 이 새로운 배열의 length (길이)를 이용하면 특정 값의 개수를 쉽게 얻을 수 있습니다.
const numbers = [1, 2, 3, 4, 2, 5, 2];
const targetNumber = 2;
const filteredNumbers = numbers.filter(element => element === targetNumber);
const count = filteredNumbers.length;
console.log(`${targetNumber}의 개수: ${count}`); // 출력: 2의 개수: 3
설명:
- numbers.filter(element => element === targetNumber): numbers 배열에서 element가 targetNumber(2)와 같은 경우만 필터링하여 새로운 배열 [2, 2, 2]를 반환합니다.
- filteredNumbers.length: 필터링된 배열의 길이를 측정하여 특정 값의 개수를 알아냅니다.
filter() 함수는 원본 배열을 변경하지 않으므로 안전하게 사용할 수 있으며, 코드가 간결하다는 장점이 있습니다.
3. reduce() 함수 활용
reduce() 함수는 배열의 각 요소를 순회하면서 콜백 함수를 실행하고, 최종적으로 단 하나의 결과 값을 반환하는 함수입니다. 이를 활용하여 특정 값의 개수를 누적해서 셀 수 있습니다.
const colors = ['red', 'blue', 'green', 'red', 'yellow', 'red'];
const targetColor = 'red';
const count = colors.reduce((accumulator, currentValue) => {
return accumulator + (currentValue === targetColor ? 1 : 0);
}, 0);
console.log(`${targetColor}의 개수: ${count}`); // 출력: red의 개수: 3
설명:
- colors.reduce((accumulator, currentValue) => { ... }, 0): colors 배열에 대해 reduce 함수를 호출합니다.
- accumulator: 누적된 값을 저장하는 변수로, 초기값은 0으로 설정됩니다.
- currentValue: 현재 배열에서 처리 중인 요소입니다.
- return accumulator + (currentValue === targetColor ? 1 : 0);:
- currentValue가 targetColor('red')와 같으면 1을, 다르면 0을 accumulator에 더합니다.
- 즉, targetColor와 일치할 때마다 accumulator가 1씩 증가합니다.
- 최종적으로 accumulator에 특정 값의 총 개수가 저장되어 반환됩니다.
reduce() 함수는 다양한 계산에 활용될 수 있으며, 간결하면서도 강력한 코드 작성을 가능하게 합니다.
어떤 방법을 선택해야 할까요?
- 가장 직관적인 방법이 필요하다면: for 반복문
- 간결하고 가독성 좋은 코드를 선호한다면: filter().length
- 다양한 누적 계산에 익숙하거나 활용도를 높이고 싶다면: reduce()
각자의 상황과 코드 스타일을 고려하여 가장 적합한 방법을 선택하시면 됩니다.
4. 여러 특정값의 개수를 확인하기 예시
두 개의 문자열 배열 arrA와 arrB가 주어졌을 때, arrA에 있는 각 문자열이 arrB에 몇 번 등장하는지 카운트하는 방법
Map 객체를 사용하여 arrB의 빈도를 미리 계산하기
- Map 객체 초기화 (arrB의 빈도 계산):
- arrB의 모든 요소를 한 번만 순회합니다. (O(M) 시간 복잡도, M은 arrB의 길이)
- Map의 set() 및 get() 연산은 평균적으로 O(1) (상수 시간)입니다. 해시 충돌이 극심한 최악의 경우 O(K) (K는 Map의 키 개수)가 될 수도 있지만, 일반적인 문자열에서는 발생할 가능성이 낮습니다.
- arrA의 각 요소에 대한 조회:
- arrA의 모든 요소를 한 번만 순회합니다. (O(N) 시간 복잡도, N은 arrA의 길이)
- Map의 get() 연산은 평균적으로 O(1) (상수 시간)입니다.
따라서 전체적인 시간 복잡도는 O(N + M)이 됩니다. 이는 각 배열을 한 번씩만 순회하므로 이론적으로 달성할 수 있는 최적의 시간 복잡도입니다.
Map 활용
/**
* 문자열 배열 'arrA'의 각 요소가 문자열 배열 'arrB'에 몇 번 등장하는지 효율적으로 카운트하는 함수.
*
* 이 방법은 O(N + M)의 시간 복잡도를 가지며,
* N은 arrA의 길이, M은 arrB의 길이입니다.
*
* @param {string[]} arrA - 찾고자 하는 문자열들이 담긴 배열.
* @param {string[]} arrB - 대상 문자열들이 담긴 배열.
* @returns {Map<string, number>} 각 'arrA' 요소의 'arrB' 내 출현 횟수를 담은 Map 객체.
*/
function countOccurrencesEfficiently(arrA, arrB) {
// 1. arrB의 각 요소 빈도를 저장할 Map 생성
const bFrequencyMap = new Map();
// arrB를 한 번만 순회하여 각 요소의 개수를 미리 계산합니다.
for (const itemB of arrB) {
bFrequencyMap.set(itemB, (bFrequencyMap.get(itemB) || 0) + 1);
}
// 2. arrA의 각 요소에 대한 결과를 저장할 Map 생성
const finalCounts = new Map();
// arrA를 순회하면서 arrB의 빈도 Map에서 개수를 조회합니다.
for (const itemA of arrA) {
// arrA의 요소가 bFrequencyMap에 있다면 해당 개수를, 없다면 0을 가져옵니다.
const count = bFrequencyMap.get(itemA) || 0;
finalCounts.set(itemA, count);
}
return finalCounts;
}
// 예시 사용
const arrA = ['apple', 'banana', 'grape', 'kiwi', 'orange'];
const arrB = ['apple', 'orange', 'banana', 'apple', 'kiwi', 'banana', 'melon', 'orange'];
console.time('Efficient Counting'); // 성능 측정 시작
const resultCounts = countOccurrencesEfficiently(arrA, arrB);
console.timeEnd('Efficient Counting'); // 성능 측정 종료
console.log("각 문자열의 출현 횟수 (효율적인 방법):");
resultCounts.forEach((count, item) => {
console.log(`'${item}': ${count}번`);
});
/*
예상 출력:
'apple': 2번
'banana': 2번
'grape': 0번
'kiwi': 1번
'orange': 2번
*/
코드 설명
- bFrequencyMap 생성 및 채우기:
- arrB 배열을 한 번만 순회하여 bFrequencyMap에 각 문자열이 몇 번 나타나는지 그 빈도수를 저장합니다. 예를 들어, arrB가 ['apple', 'banana', 'apple']이면 bFrequencyMap은 {'apple' => 2, 'banana' => 1}이 됩니다.
- bFrequencyMap.get(itemB) || 0는 itemB가 Map에 없으면 undefined를 반환하는데, 이때 || 0를 통해 0으로 처리하여 초기 카운팅을 가능하게 합니다.
- finalCounts 생성 및 채우기:
- arrA 배열을 순회합니다.
- 각 itemA에 대해, 위에서 미리 계산해 둔 bFrequencyMap에서 해당 itemA의 카운트 값을 가져옵니다.
- bFrequencyMap.get(itemA) || 0를 사용하여 arrB에 itemA가 아예 없는 경우 0으로 처리합니다.
- finalCounts Map에 itemA와 그 카운트 값을 저장합니다.
다른 방법이 비효율적인 이유
1. 중첩 루프 + filter() 또는 indexOf()
// 비효율적인 방법 예시 (O(N * M) 시간 복잡도)
function countOccurrencesInefficiently(arrA, arrB) {
const result = {};
arrA.forEach(itemA => {
let count = 0;
// arrB를 itemA의 개수만큼 순회 (arrA의 길이만큼 arrB를 반복해서 탐색)
for (const itemB of arrB) {
if (itemA === itemB) {
count++;
}
}
result[itemA] = count;
});
return result;
}
// 또는 더 나쁜 filter() 사용 (filter() 자체도 내부적으로 순회)
function countOccurrencesWithFilter(arrA, arrB) {
const result = {};
arrA.forEach(itemA => {
// itemA당 arrB 전체를 순회 (filter)
result[itemA] = arrB.filter(itemB => itemB === itemA).length;
});
return result;
}
// console.time('Inefficient Counting');
// countOccurrencesInefficiently(arrA, arrB);
// console.timeEnd('Inefficient Counting');
- 시간 복잡도: O(N * M)
- 문제점: arrA의 각 요소에 대해 arrB 전체를 다시 순회해야 합니다.
- 예를 들어 arrA에 1000개 요소, arrB에 1000개 요소가 있다면, 최악의 경우 1000 * 1000 = 1,000,000번의 비교가 발생할 수 있습니다.
- Map 방식이 2000번의 순회로 끝나는 것과 비교하면 매우 비효율적입니다.
2. reduce() (단순 카운팅 목적이라면)
reduce()는 강력한 함수이지만, 위와 같은 특정 값의 빈도를 계산하는 데 있어서는 Map을 미리 만드는 것만큼 직접적으로 효율적이지 않을 수 있습니다. reduce() 내부에서 매번 arrB를 순회하게 코드를 작성한다면 결국 중첩 루프와 유사한 비효율성이 발생합니다.
// reduce를 사용하지만 비효율적일 수 있음
function countOccurrencesWithReduce(arrA, arrB) {
return arrA.reduce((acc, itemA) => {
// 각 itemA마다 arrB를 다시 순회 (비효율적)
const count = arrB.filter(itemB => itemB === itemA).length;
acc.set(itemA, count);
return acc;
}, new Map());
}
이 방식은 arrA의 요소 수(N)만큼 arrB를 filter 함수로 순회(M)하므로, 여전히 O(N * M)의 시간 복잡도를 가집니다.
결론
주어진 문제 상황 (arrA의 각 요소가 arrB에 몇 번 등장하는지)에서는 arrB의 빈도수를 한 번에 계산하여 Map에 저장한 후, arrA의 요소를 순회하며 이 Map에서 조회하는 방식(countOccurrencesEfficiently)이 시간 복잡도 O(N + M)로 가장 효율적인 솔루션입니다. 이는 대규모 데이터셋을 다룰 때 성능에 큰 차이를 가져올 수 있습니다.