배열이란 연산 비용 동적 배열 문자열 자주 쓰는 조작 📝 문제풀이
◀ 이전 📋 목차 CH 03 ▶
📦 CHAPTER 02 · 자료구조

배열은 "인덱스로 바로 찾는" 그릇이에요

가장 기본이자 가장 많이 쓰이는 자료구조가 배열이에요. 인덱스 접근은 O(1)로 번개같지만, 중간에 넣고 빼는 건 생각보다 비싸요. 그 차이를 정확히 이해하면 코테 문제의 절반이 풀려요.

🎯 이 장을 끝내면
📦
배열이란
같은 종류의 데이터를 줄지어 담는 그릇
인덱스는 0부터 시작해요. 이게 핵심이에요.

배열(Array)은 같은 타입의 데이터를 연속된 자리에 나란히 저장하는 자료구조예요. 각 자리에는 0부터 시작하는 번호(인덱스)가 붙어요. 그래서 arr[0]첫 번째 요소, arr[1]이 두 번째 요소예요.

🔤
요소(Element) — 배열 안에 들어있는 값 하나하나를 가리키는 말이에요. arr[0] 자리의 10, arr[1] 자리의 20처럼, 배열을 이루는 낱개 데이터가 바로 요소예요. "배열엔 요소가 5개 있다" = "칸이 5개 채워져 있다"는 뜻이에요.
📦 배열 한눈에 보기
const arr = [10, 20, 30, 40, 50];
//           0   1   2   3   4    ← 인덱스(0부터!)

console.log(arr[0]);  // 10  (첫 번째 요소)
console.log(arr[2]);  // 30
console.log(arr.length); // 5  (요소 개수)
배열의 시작 위치와 인덱스만 알면 "몇 번째 자리인지"를 곧바로 계산할 수 있어요. 그래서 몇 번째든 단번에 접근해요 → 접근 O(1). 💡 인덱스가 0부터인 이유: 배열의 시작 지점에서 몇 칸 떨어졌는가로 위치를 계산하기 때문이에요. 첫 요소는 0칸 떨어져 있으니 인덱스 0이에요.
🔤
시간복잡도(Time Complexity) / 빅오(Big-O) — "데이터가 늘어날수록 시간이 얼마나 더 걸리는지"를 나타내는 표기예요. 정확한 초 단위가 아니라 늘어나는 정도를 보는 거예요. 예를 들어 데이터가 2배로 늘어날 때: O(1)은 그래도 시간이 그대로(안 늘어남), O(n)은 시간도 2배, O(n²)은 시간이 4배로 늘어나요. 그래서 O(1)이 가장 좋고, 숫자·글자가 붙을수록(O(n) → O(n²)) 점점 느려진다고 기억하면 쉬워요.
🖼️ 그림으로 보기 — 연속된 칸과 인덱스
10
[0]
20
[1]
30
[2] ←
40
[3]
50
[4]
arr[2]를 찾을 때 앞 칸을 하나씩 세지 않아요. "시작 + 2칸" 위치를 바로 계산하니 몇 번째든 단번에 닿아요 → O(1)
자바와 비교. 자바에서도 배열은 int[] arr = {10, 20, 30}; 처럼 쓰고, arr[0]으로 첫 요소에 접근해요. 인덱스가 0부터인 것도, 접근이 O(1)인 것도 완전히 동일해요. 다만 자바 기본 배열은 크기가 고정이라는 점이 달라요(뒤에서 다뤄요).
배열 인덱스 O(1) 접근 0부터 동적 배열 문자열 immutable
⚖️
연산 비용
접근은 빠르지만, 탐색·삽입·삭제는 비싸요
"어디를 건드리느냐"에 따라 비용이 완전히 달라져요.

인덱스로 접근하는 건 O(1)이에요. 하지만 값을 찾는 것(탐색)중간에 넣고 빼는 것은 이야기가 달라요.

🔍 탐색 — 처음부터 훑어야 해요
function indexOf(arr, target) {
  for (let i = 0; i < arr.length; i++) {  // 앞에서부터 하나씩
    if (arr[i] === target) return i;       // 찾으면 위치 반환
  }
  return -1;
}
배열은 값이 어디 있는지 미리 알 수 없어서, 최악의 경우 끝까지 확인해야 해요 → 탐색 O(n).
➕ 중간 삽입·삭제 — 뒤 요소를 다 밀어야 해요
const arr = [10, 20, 30, 40];
// 인덱스 1 자리에 99를 끼워넣으려면?
// 20, 30, 40 을 전부 한 칸씩 뒤로 밀어야 해요.
arr.splice(1, 0, 99);   // [10, 99, 20, 30, 40]
중간에 넣으면 뒤쪽 요소들을 모두 한 칸씩 이동해야 하고, 삭제하면 빈자리를 메우려 당겨야 해요. 그래서 최악의 경우 n개를 옮겨요 → 중간 삽입·삭제 O(n). 💡 반대로 맨 끝에 넣거나 빼는 건 밀 게 없어서 O(1)(평균)이에요. 그래서 배열은 끝에서 다루는 걸 선호해요.
연산복잡도이유
인덱스 접근 arr[i]O(1)위치를 바로 계산해 단번에 접근
값 탐색 (찾기)O(n)처음부터 하나씩 훑어야 함
끝에 삽입 (push)O(1)뒤에 밀 요소가 없음 (평균)
중간 삽입·삭제O(n)뒤 요소를 전부 밀거나 당겨야 함
📈
동적 배열
크기가 알아서 늘어나는 배열
JS의 Array, 자바의 ArrayList가 여기에 해당해요.

순수한 배열은 크기가 고정이에요(자바 기본 배열처럼). 하지만 실무·코테에선 동적 배열(Dynamic Array)을 훨씬 많이 써요. 요소를 계속 추가하면 내부적으로 알아서 공간을 늘려주는 배열이에요.

📈 push로 계속 붙이기
const arr = [];        // 빈 배열로 시작
arr.push(10);          // [10]
arr.push(20);          // [10, 20]
arr.push(30);          // [10, 20, 30]
console.log(arr.length); // 3  ← 크기가 알아서 늘어남
push끝에 추가하는 건 대부분 O(1)이에요(가끔 공간이 꽉 차서 내부적으로 복사하는 순간만 느리지만, 평균적으로 O(1)이에요). 💡 이런 걸 "평균 O(1)"(분할 상환, amortized)이라고 불러요. 대부분 빠르고, 아주 가끔만 느려서, 평균으로 보면 O(1)이라는 뜻이에요.
자바와 비교. 자바에서는 int[]는 크기가 고정이라 늘릴 수 없어요. 대신 ArrayList<Integer>를 쓰면 list.add(10)으로 계속 추가할 수 있어요. 이게 바로 동적 배열이고, JS의 Array·push와 역할이 같아요.
🔤
문자열
문자열은 "문자들의 배열"인데, 바꿀 수 없어요
불변(immutable)이라는 특성이 코테에서 자주 함정이 돼요.

문자열(String)은 문자들이 순서대로 나열된 것이에요. 배열처럼 인덱스로 각 문자에 접근할 수 있어요. "hello"[0]"h"예요.

🔤 인덱스로 문자 하나씩
const s = "hello";
console.log(s[0]);       // "h"
console.log(s[4]);       // "o"
console.log(s.length);   // 5
여기까지는 배열과 똑같아 보여요. 하지만 결정적인 차이가 하나 있어요 — 문자열은 불변(immutable)이에요.
🚫 불변 — 한 번 만들면 내용을 못 바꿔요
let s = "hello";
s[0] = "H";           // ❌ 아무 일도 안 일어나요! (조용히 무시)
console.log(s);       // "hello"  ← 그대로

// 바꾸려면 새 문자열을 만들어야 해요
s = "H" + s.slice(1); // "Hello"  ← 새 문자열 생성
문자열은 한 번 만들어지면 내용을 직접 수정할 수 없어요. 바꾸는 것처럼 보이는 연산은 사실 새 문자열을 만드는 것이에요. 💡 그래서 반복문 안에서 문자열을 계속 +로 이어붙이면 매번 새 문자열이 생겨 느려질 수 있어요. 많이 이어붙일 땐 배열에 모았다가 join하는 게 좋아요.
자바와 비교. 자바의 String불변이에요. 그래서 문자열을 많이 조합할 땐 StringBuilder를 써요. JS에서 배열에 모아 join하는 것과 같은 이유예요 — 매번 새 문자열을 만드는 낭비를 피하려는 거예요.
🛠️
자주 쓰는 조작
코테에서 손에 익혀둘 기본기
순회 · 뒤집기 · 부분 잘라내기 — 이 셋만 있어도 든든해요.
🔁 순회 — for 와 for-of
const arr = [10, 20, 30];

// 방법 1: 인덱스로 (i가 필요할 때)
for (let i = 0; i < arr.length; i++) {
  console.log(i, arr[i]);   // 0 10 / 1 20 / 2 30
}

// 방법 2: for-of (값만 필요할 때 간결)
for (const x of arr) {
  console.log(x);           // 10 / 20 / 30
}
전체를 한 번 훑으니 순회는 O(n)이에요.
↩️ 뒤집기 — reverse
const arr = [1, 2, 3];
arr.reverse();            // [3, 2, 1]  (배열은 제자리에서 뒤집힘)

// 문자열 뒤집기: 문자열은 불변이라 배열로 바꿔서!
const s = "abc";
const rev = s.split("").reverse().join("");  // "cba"
문자열은 불변이라 바로 뒤집을 수 없어서, split("")배열로 바꾼 뒤 뒤집고 다시 join해요.
✂️ 부분 잘라내기 — slice / substring
const s = "hello";
console.log(s.slice(0, 3));     // "hel"  (0번부터 3번 앞까지)
console.log(s.slice(1));        // "ello" (1번부터 끝까지)
console.log(s.substring(1, 4)); // "ell"

const arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 3));   // [2, 3]  (배열에도 slice가 있어요)
slice(시작, 끝)끝 인덱스 바로 앞까지 잘라요. 원본은 건드리지 않고 새 값을 돌려줘요. 💡 문자열에 slice·substring을 써도 원본은 그대로예요(불변이니까!). 잘라낸 결과가 새 문자열로 나올 뿐이에요.
🧪
이 과목의 실습 랩에서 배열·문자열을 직접 다루며 실행·채점해볼 수 있어요. 상단 🧪 실습 버튼에서 열려요. 순회·뒤집기·자르기를 손으로 짜보면 훨씬 오래 남아요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 손으로 직접 짜보고 싶으면 상단 🧪 실습 랩으로!
CH 03 🥞 스택과 큐 — 한 쪽으로 넣고 빼는 LIFO, 줄 서서 처리하는 FIFO