탐색 알고리즘이란?
탐색은 주어진 데이터 중에서 원하는 값을 찾는 과정을 말한다.
즉, 탐색 알고리즘은 "데이터 구조(배열, 트리, 그래프 등) 안에서 특정한 값이나 노드를 찾기 위한 규칙적인 절차 또는 방법"이라고 정의할 수 있다.
탐색 알고리즘의 분류
| 자료구조 | 대표 탐색 알고리즘 | 예시 |
| 배열 | 선형 탐색, 이진 탐색 | 리스트에서 값 찾기 |
| 트리 | 이진 탐색 트리 탐색, DFS, BFS | 폴더 구조, 계층형 데이터 |
| 그래프 | BFS, DFS | 길찾기, 소셜 네트워크, 네비게이션 |
선형 탐색 (Linear Search / 순차 탐색)
배열의 맨앞에서 하나씩 목표 값(target)과 비교해 나가서 찾으면 멈춘다.
책장에 정리 안 된 책 더미가 있고, 원하는 책을 찾을 때 맨앞부터 하나씩 확인하는 방식과 같다.
언제 쓰나?
- 데이터가 정렬되어 있지 않을 때
- 데이터 크기가 작을 때
- 구현이 간단해야 할 때
시간/공간 복잡도
- 시간 : 최악 O(n), 평균 O(n)
- 공간 : O(1) (추가 메모리 거의 없음)
장단점
- 장점 : 구현 쉬움, 정렬 필요 없음.
- 단점 : 속도 느림(대규모 데이터에는 비효율)
해당 메서드
- indexOf
- includes
- find
- findIndex
구현 예시
function linearSearch(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) return i; // 찾으면 인덱스 반환
}
return -1; // 못 찾음
}
이진 탐색 (Binary Search)
정렬된 배열에서 중간값과 비교해 탐색 범위를 반씩 줄여가며 찾는 방법
전화번호부에서 가운데 쪽을 보고 찾는 이름이 어느 쪽(앞/뒤)에 있는지 판단해 절반만 뒤져나가는 방식이다.
전제조건
- 반드시 정렬된 자료구조(오름/내림차순)여야 함
시간/공간 복잡도
- 시간 : O(log n) - 매우 빠름
- 공간 : O(1) (반복문 사용 시) / O(log n) (재귀 사용 시 스택)
장단점
- 장점 : 큰 데이터에서도 매우 빠름
- 단점 : 데이터가 정렬되어 있어야 함. 삽입/삭제가 잦으면 정렬 유지 비용이 큼
구현 예시
function binarySearch(arr, target) {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
// test cases
console.log(binarySearch([1, 2, 3, 4, 5], 2)); // 1
console.log(binarySearch([1, 2, 3, 4, 5], 6)); // -1
해시 탐색 (Hash Table / 해시 맵)
키(key)를 해시 함수로 숫자(인덱스)로 바꿔서 바로 접근하는 방식. 보통 해시맵/ 객체로 구현
사물함 번호(해시값)를 통해 문을 바로 열어 물건(값)에 접근하는 것과 같다.
시간/공간 복잡도
- 평균 시간 : O(1) (탐색, 삽입, 삭제)
- 최악 시간 : O(n) (충돌이 매우 심한 경우 - 해시 설계/체이닝/리사이징으로 보완)
- 공간 : O(n)
언제 쓰나?
- 키로 값을 바로 찾고 싶을 때(예: 사용자 id ➡️ 사용자 정보)
- 빈번한 삽입/삭제가 있을 때
장단점
- 장점 : 매우 빠름(평균), 키-값 저장에 적합
- 단점 : 순서 유지 안됨(일부 구현은 순서 보장), 메모리 오버헤드, 해시 충돌 처리 필요
구현 예시
const map = new Map();
map.set('alice', {age:25});
console.log(map.get('alice')); // {age:25}
이진 탐색 트리 탐색 (BST Search)
이진 트리의 성질을 이용해 탐색한다. 왼쪽 서브트리는 루트보다 작은 값이고, 오른쪽 서브트리는 큰 값이다.
루트 노드를 기준으로 시작해 원하는 값보다 노드 값이 작으면 오른쪽 서브트리로 이동한다.
원하는 값보다 노드 값이 크면 왼쪽 서브트리로 이동한다.
이 과정을 반복해 원하는 값과 노드 값이 같으면 종료한다.
시간/공간 복잡도
- 평균 시간 : O(log n) (균형 트리일 때)
- 최악 시간 : O(n) (한쪽으로 치우친 경우)
- 공간 : O(height) (재귀스택)
장단점
- 장점 : 정렬된 데이터를 유지하며 데이터를 추가하거나 삭제 가능, 삽입/삭제/탐색 통합
- 단점 : 균형이 안 맞으면 성능 저하 ➡️AVL, 레드-블랙 트리 같은 균형 트리 사용
언제 쓰나?
- 범위 쿼리(예: 특정 범위의 값 전부 조회)나 정렬된 상태 유지하면서 삽입/삭제를 자주 해야 할 때
구현 예시
// 노드 정의
class Node {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// 이진 탐색 트리 클래스
class BinarySearchTree {
constructor() {
this.root = null;
}
// 노드 삽입
insert(value) {
const newNode = new Node(value);
if (this.root === null) {
this.root = newNode;
return;
}
let current = this.root;
while (true) {
if (value === current.value) return; // 중복 방지
if (value < current.value) {
if (current.left === null) {
current.left = newNode;
return;
}
current = current.left;
} else {
if (current.right === null) {
current.right = newNode;
return;
}
current = current.right;
}
}
}
// 탐색
search(value) {
let current = this.root;
while (current !== null) {
if (value === current.value) return true;
if (value < current.value) current = current.left;
else current = current.right;
}
return false; // 찾지 못한 경우
}
}
// 테스트
const bst = new BinarySearchTree();
bst.insert(8);
bst.insert(3);
bst.insert(10);
bst.insert(1);
bst.insert(6);
bst.insert(14);
console.log(bst.search(6)); // true
console.log(bst.search(15)); // false
그래프 탐색 알고리즘 - BFS(너비 우선 탐색) & DFS(깊이 우선 탐색)
그래프란?
그래프(Graph)는 노드(Node)와 간선(Edge)으로 이루어진 자료구조. 즉, 여러 점(노드)이 서로 연결(간선)되어 있는 형태
A ─ B ─ C
│ │
D E
➡️ A, B, C, D, E는 노드(node)
➡️ 선으로 연결된 관계(A-B, B-C, A-D, B-E)는 간선(edge)
인접 리스트 표현
const graph = {
A: ['B', 'D'],
B: ['A', 'C', 'E'],
C: ['B'],
D: ['A'],
E: ['B']
};
- graph = 그래프 전체 데이터
- graph['A'] = ['B', 'D'] → A는 B, D와 연결되어 있다
- 각 노드는 key, 연결된 노드 배열은 value
너비 우선 탐색 (Breadth-First Search)
시작 노드에서 가까운 노드부터 차례로 탐색한다. 즉, 넓게 퍼지면서 탐색
큐 자료구조를 사용해 구현한다.
시간/공간 복잡도
- 시간 : O(V + E) (V는 노드 수, E는 간선 수)
- 공간 : O(V)
동작순서
- 시작 노드에서 출발하여, 해당 노드와 인접한 모든 노드를 우선 탐색한다.
- 탐색 중 방문한 노드는 큐(Queue)에 저장
- 큐에서 하나의 노드를 꺼내어 해당 노드와 인접한 노드를 탐색
- 모든 인접 노드를 탐색한 후, 큐에서 다음 노드를 꺼내어 반복
- 큐가 비거나 모든 노드를 방문할 때까지 반복
장단점
- 장점 : 답이 되는 경로가 여러 개인 경우에도 최단 경로 보장
- 단점 : DFS에 비해 구현이 다소 복잡
노드의 수가 많을수록 탐색 가지가 급격히 증가함에 따라 보다 많은 기억 공간(메모리)를 필요로 하게 됨.
언제 쓰나?
- 최단 거리 문제 (예 : 미로, 지도)
- 레벨별 탐색
구현 예시
function bfs(graph, start) {
const visited = new Set(); // 방문한 노드 기록
const queue = [start]; // 탐색 대기열
visited.add(start);
while (queue.length) {
const node = queue.shift(); // 현재 탐색 노드
console.log(node); // 방문 순서 출력
for (const neigh of graph[node] || []) { // 인접 노드 순회
if (!visited.has(neigh)) { // 아직 방문 안 했다면
visited.add(neigh);
queue.push(neigh); // 큐에 추가
}
}
}
}
➡️ 변수 설명
| 변수 | 의미 | 예시 |
| graph | 그래프 전체 구조 (인접 리스트) | { A:['B','D'], ... } |
| start | 탐색 시작 노드 | 'A' |
| visited | 방문한 노드를 저장 (중복 방지) | Set { 'A', 'B' } |
| queue | 탐색 순서를 관리 (FIFO 구조) | [ 'B', 'D' ] |
| node | 현재 방문 중인 노드 | 'A', 'B' |
| neigh | 현재 노드의 인접 노드 | 'B', 'D', 'C' |
※ 예시 실행 (bfs(graph, 'A'))
1) A 방문 → queue = ['B', 'D']
2) B 방문 → queue = ['D', 'C', 'E']
3) D 방문 → queue = ['C', 'E']
4) C, E 방문
출력 순서:
➡️ A → B → D → C → E
깊이 우선 탐색 (depth-First Search)
한 노드를 시작으로, 갈 수 잇는 곳까지 깊게 들어가며 탐색하고, 막히면 이전 노드로 돌아가는(backtracking) 알고리즘
스택 또는 재귀를 사용한다.
시간/공간 복잡도
- 시간 : O(V + E) (V는 노드 수, E는 간선 수)
- 공간 : O(V)
동작순서
- 시작 노드에서 출발하여, 해당 노드의 자식 노드를 순차적으로 탐색한다.
- 탐색 중 방문한 노드는 스택(Stack)에 저장하거나 재귀 호출을 통해 처리한다.
- 현재 노드에서 더 이상 자식 노드를 탐색할 수 없을 때, 되돌아와서 이전 노드의 다른 자식 노드를 탐색
- 스택이 비거나 모든 노드를 방문할 때까지 반복
장단점
- 장점 : 간단하고 직관적인 구조로 구현이 쉬움
재귀 함수를 활용하면 코드가 간결해짐
현 경로상의 노드를 기억하기 때문에 적은 메모리를 사용 - 단점 : 모든 경로를 탐색하기 때문에 그래프가 순환 구조일 경우 무한 루프에 빠질 수 있음
목표에 이르는 경로가 다수인 경우, DFS는 값에 도착하면 탐색을 종료하므로 얻어진 값이 최단 경로라는 보장 없음
언제 쓰나?
- 경로 탐색, 사이클 탐지
- 백트래킹 문제 (예: 미로 탈출, 조합 탐색)
구현 예시
function dfs(graph, node, visited = new Set()) {
if (visited.has(node)) return; // 이미 방문했으면 중복 방지
visited.add(node); // 방문 처리
console.log(node); // 현재 노드 출력
for (const neigh of graph[node] || []) {
dfs(graph, neigh, visited); // 재귀적으로 깊게 탐색
}
}
| 변수 | 의미 |
| graph | 그래프 전체 구조 |
| node | 현재 탐색 중인 노드 |
| visited | 방문한 노드 기록 |
| neigh | 현재 노드의 인접 노드들 |
※ 예시 실행 (dfs(graph, 'A'))
1) A 방문
➡️ 이웃: ['B', 'D'], 먼저 B로 이동
2) B 방문
➡️ 이웃: ['A', 'C', 'E'], A는 이미 방문 → C로 이동
3) C 방문 ➡️ 이웃: ['B'], 모두 방문 → 돌아감
4) B의 다음 이웃 E 방문
5) E 방문 ➡️ ['B'], 이미 방문 ➡️ 돌아감
6) B 탐색 끝 ➡️ A로 돌아가서 D 방문
출력 순서:
👉 A → B → C → E → D

BFS vs DFS 비교 요약
| 구분 | BFS | DFS |
| 탐색 방식 | 넓게(레벨 순) | 깊게(하나의 경로 끝까지) |
| 자료구조 | 큐(Queue) | 스택(Stack) or 재귀 |
| 주 용도 | 최단 거리, 레벨 탐색 | 경로 탐색, 백트래킹 |
| 대표 특징 | 가까운 노드부터 방문 | 한 경로를 끝까지 탐색 |
'개발지식 > 자료구조' 카테고리의 다른 글
| [자료구조] 그래프(Graph) (0) | 2025.11.17 |
|---|---|
| [자료구조] 트리(Tree) (0) | 2025.11.10 |
| [자료구조] 정렬 (Sort) - 병합 정렬, 퀵 정렬 (0) | 2025.10.27 |
| [자료구조] 힙(Heap)과 우선순위 큐(Priority Queue) (0) | 2025.10.20 |
| [자료구조] 스택(Stack)과 큐(Que) (0) | 2025.10.14 |