배열
배열 개념
같은 타입의 원소들을 관리할 수 있는 기본 자료형.
순서가 있고, 중복이 허용된다.
배열 선언
1. 리터럴
const arr = [1, 2, 3, 4, 5, 6];
2. 생성자
const arr1 = new Array(6); // [undefined, undefined, ...]
const arr2 = [...new Array(6)].map((_, i) => i + 1); // [1, 2, 3, 4, 5, 6]
3. Array.fill()
const arr = new Array(6).fill(0); // [0, 0, 0, 0, 0, 0]
배열의 index 는 0부터 시작한다. 3번째 데이터에 접근하려면 arr[2]로 접근하면 도니다.
배열과 차원
다차원의 배열도 실제로는 1차원 공간에 저장한다. -> 배열은 차원과는 무관하게 메모리에 연속할당한다.
2차원 배열
// 2차원 배열을 리터럴로 표현
const arr = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]];
// arr[2][3]에 저장된 값을 출력
console.log(arr[2][3]); // 12
// arr[2][3]에 저장된 값을 15로 변경
arr[2][3] = 15;
//변경된 값을 출력
console.log(arr[2][3]); // 15
배열의 효율성
배열의 시간 복잡도
배열은 임의 접근이라는 방법으로 배열의 모든 위치에 있는 데이터에 단 한 번에 접근할 수 있다. O(1)
또 데이터를 어디에 저장하느냐에 따라 시간 복잡도가 달라진다.
맨 뒤에 삽입할 경우
바로 접근할 수 있으며 다른 데이터 위치에 영향을 주지 않는다.
O(1)
맨 앞에 삽입할 경우
기존 데이터를 한 칸씩 밀어야한다.
O(N)
중간에 삽입할 경우
삽입한 데이터 뒤에 있는 개수만큼 밀어야한다.
O(N)
배열을 선택할 때 고려할 점
데이터에 자주 접근하거나 읽어야 하는 경우 배열을 사용하면 좋은 성능을 낼 수 있다.
자주 활용하는 배열의 기법
배열에 데이터 추가 제거
push(): 배열 끝에 요소 추가
const arr = [1, 2];
arr.push(3); // [1, 2, 3]
pop(): 배열 끝의 요소 제거
const arr = [1, 2, 3];
arr.pop(); // [1, 2]
unshift(): 배열 시작에 요소 추가
const arr = [1, 2];
arr.unshift(0); // [0, 1, 2]
shift(): 배열 시작의 요소 제거
const arr = [0, 1, 2];
arr.shift(); // [1, 2]
탐색
indexOf(): 특정 요소의 첫 번째 인덱스 반환
const arr = [1, 2, 3, 2];
arr.indexOf(2); // 1
lastIndexOf(): 특정 요소의 마지막 인덱스 반환
arr.lastIndexOf(2); // 3
includes(): 배열에 특정 요소가 있는지 확인
arr.includes(3); // true
순회
forEach(): 배열의 각 요소에 대해 실행
const arr = [1, 2, 3];
arr.forEach(el => console.log(el)); // 1, 2, 3
변환
map(): 각 요소를 변환하여 새로운 배열 반환
const arr = [1, 2, 3];
const doubled = arr.map(el => el * 2); // [2, 4, 6]
filter(): 조건에 맞는 요소만 추출한 배열 반환
const arr = [1, 2, 3, 4];
const evens = arr.filter(el => el % 2 === 0); // [2, 4]
reduce(): 배열 요소를 누적하여 단일 값 반환
const arr = [1, 2, 3];
const sum = arr.reduce((acc, el) => acc + el, 0); // 6
정렬 및 반전
sort(): 배열을 정렬 (기본적으로 문자열로 정렬)
const arr = [3, 1, 2];
arr.sort(); // [1, 2, 3]
arr.sort((a, b) => b - a); // [3, 2, 1]
reverse(): 배열 순서를 반전
arr.reverse(); // [1, 2, 3] -> [3, 2, 1]
병합
concat(): 배열 병합
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = arr1.concat(arr2); // [1, 2, 3, 4]
spread 연산자
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
추출
slice(): 배열 일부를 복사하여 새로운 배열 반환
const arr = [1, 2, 3, 4];
const part = arr.slice(1, 3); // [2, 3]
splice(): 배열에서 요소를 제거하거나 추가
const arr = [1, 2, 3, 4];
arr.splice(1, 2); // [1, 4]
arr.splice(1, 0, 2, 3); // [1, 2, 3, 4]
유용한 고급 메서드
flat(): 중첩 배열을 평탄화
const arr = [1, [2, [3, 4]]];
arr.flat(2); // [1, 2, 3, 4]
find(): 조건을 만족하는 첫 번째 요소 반환
const arr = [1, 2, 3, 4];
const found = arr.find(x => x > 2); // 3
findIndex(): 조건을 만족하는 첫 번째 요소의 인덱스 반환
const index = arr.findIndex(x => x > 2); // 2
some(): 조건을 만족하는 요소가 하나라도 있으면 true
arr.some(x => x > 3); // true
every(): 모든 요소가 조건을 만족하면 true
arr.every(x => x > 0); // true
배열을 다룰 때 성능 이슈를 피하려면 필요한 경우에만 메서드를 사용하며, 대규모 데이터에 대해서는 for문 또는 효율적인 알고리즘을 사용하는 것이 좋다.
배열 메서드의 성능 문제
- 메서드의 반복 호출로 인한 성능 저하
- map, filter, reduce 같은 메서드는 새로운 배열을 생성하므로 메모리를 추가로 사용한다.
- 여러 메서드를 연속으로 호출하면 중간 결과 배열들이 계속 생성되어 성능이 떨어질 수 있다.
- 콜백 함수 호출
- 배열 메서드는 각 요소마다 콜백을 실행하므로, 배열 크기가 클수록 많은 함수 호출이 이루어집니다. 이는 단순 반복문(for)에 비해 비용이 크다.
- JavaScript 엔진 최적화
- 대부분의 배열 메서드는 내부적으로 복잡한 로직을 포함하며, 일부는 느릴 수 있습니다. 단순한 반복 작업에는 for나 while 루프가 더 빠를 때가 많다.
const arr = [1, 2, 3, 4, 5];
const result = arr.filter(x => x % 2 === 0).map(x => x * 2); // filter와 map 모두 새 배열 생성
효율적인 배열 처리 방법
1. 단일 반복문으로 작업 합치기
- 여러 메서드를 체이닝하는 대신, 단일 반복문으로 작업을 결합하면 중간 배열 생성을 피할 수 있다.
const arr = [1, 2, 3, 4, 5];
const result = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] % 2 === 0) {
result.push(arr[i] * 2);
}
}
2. 데이터 구조 선택
- 배열 대신 목적에 맞는 데이터 구조를 사용하는 것이 효율적일 수 있다.
- 빈번한 삽입/삭제: 배열 대신 LinkedList.
- 고속 조회: 배열 대신 Set 또는 Map.