- ···
- 12.
[멀티캠퍼스] 풀스택 개발자 아카데미 (12) - Algorithm(1) - 13.[멀티캠퍼스] 풀스택 개발자 아카데미 (13) - Algorithm(2)현재
- 14.
[멀티캠퍼스] 풀스택 개발자 아카데미 (14) - Algorithm(3) - ···
0. 탐색 알고리즘
탐색 알고리즘은 방대한 데이터 속에서 우리가 원하는 특정 정보를 찾아내는 체계적인 방법론을 의미한다. 이는 단순히 '데이터가 존재하는가?' 라는 질문에 답하는 것을 넘어, '가장 효율적인 경로'는 무엇인지, '가능한 모든 해답'은 무엇인지와 같은 복잡한 문제를 해결하기 위함이다.
탐색 전략은 문제의 구조와 요구사항에 따라 달라지며, 모든 가능성을 하나씩 확인하는 단순한 접근법부터, 데이터의 연결 관계를 따라 지능적으로 움직이는 복잡한 접근법까지 다양하다.
이번 글에서는 가장 단순하고 확실한 방법인 **브루트 포스(Brute-Force)**부터, 지도 앱이나 소셜 네트워크처럼 복잡하게 연결된 데이터를 체계적으로 탐색하는 **깊이 우선 탐색(DFS)**과 **너비 우선 탐색(BFS)**까지 알아본다.
1. Brute-Force
브루트포스(Brute-Force) 는 완전 탐색(Exhaustive Search) 이라고도 불리며, 이름 그대로 '무식하게 힘으로' 해결하는 접근법이다.
문제 해결을 위해 생각할 수 있는 모든 경우의 수를 하나도 빠짐없이 전부 탐색하여 정답을 찾는 방법이다. 열쇠 꾸러미에서 맞는 열쇠를 찾을 때까지 첫 번쨰 열쇠부터 모든 열쇠를 하나씩 다 꽂아보는 것과 같다.
1.1. 일곱 난쟁이 문제
Baekjoon Online Judge에 올라운 2309번 일곱 난쟁이 문제(BOJ 서비스 종료로 Codeup으로 대체)는 이를 잘 보여줄 수 있는 문제이다.
- 문제:
즉, 9개의 숫자가 입력되면, 합이 100이 될 수 있는 7개의 숫자 조합을 찾는 문제이다.
접근법:
- (발상) 7명의 난쟁이를 찾는 대신, 포함되지 않는 가짜 난쟁이 2명을 찾는다.
- 포함되지 않는 두 난쟁이의 키의 합은
아홉 난쟁이 전체 키 합 - 100과 같다. - 아홉 명 중 두 명을 고르는 모든 조합을 반복문으로 탐색한다.
- 두 난쟁이의 키 합이 위에서 계산한 값과 같은 조합을 찾는다.
- 해당되는 두 난쟁이를 제외한 일곱 난쟁이의 키를 오름차순으로 출력한다.
코드 설명: 위 코드는 9명 중 2명을 뽑는 모든 조합을 이중 for문으로 탐색한다. j = i + 1로 시작하여 중복된 조합(예: 1번과 2번, 2번과 1번)을 피한다. 조건에 맞는 두 명을 찾으면 즉시 반복문을 중단하여 불필요한 탐색을 줄일 수 있다.
1.2. 정리
- 장단점 및 활용: 브루트 포스는 구현이 간단하고, 가능한 해가 존재한다면 반드시 찾아낸다는 장점이 있다. 하지만 문제의 크기가 조금만 커져도 경우의 수가 기하급수적으로 늘어나(높은 시간 복잡도) 현실적으로 사용하기 어렵다
- 시간 복잡도: 예를 들어, '일곱 난쟁이' 문제에서 9명 중 2명을 찾는 조합의 수는 $_9C_2 = \frac = 36$ 번으로, 매우 적지만, 만약 30명 중 8명을 찾는 문제가 되는 경우, 경우의 수는 580만 개를 훌쩍 넘어가게 된다.
- 따라서 탐색해야 할 데이터의 범위가 작을 때, 또는 다른 효율적인 알고리즘을 구현하기 전 해결법을 검증하는 용도로 유용하다.
2. 비선형 데이터의 탐색
배열이나 연결 리스트 같은 선형 자료구조는 데이터를 일렬로 나열한다. 하지만 세상의 많은 데이터는 계층을 갖거나, 거미줄처럼 복잡하게 얽혀있는데, 이러한 비선형 데이터를 표현하기 위해 우리는 트리(Tree) 와 그래프(Graph) 를 사용한다.
이러한 트리나 그래프처럼 복잡하게 연결된 데이터의 모든 노드를 체계적으로 방문하는 방법을 그래프 순회(Traversal) 라고 하며, 대표적인 방법이 바로 DFS와 BFS다.
2.1. 이진 탐색 트리
순회 방법을 알아보기 전, 트리의 특별한 종류인 이진 탐색 트리(Binary Search Tree) 를 잠시 살펴본다. 이진 탐색 트리는 다음과 같은 중요한 규칙을 가진다.
- 규칙: "모든 노드는 왼쪽 자식보다 크고, 오른쪽 자식보다 작다" (
왼쪽 서브트리 < 부모 노드 < 오른쪽 서브트리) - 장점: 이 규칙 덕분에 데이터를 찾을 때, 현재 노드와의 크기 비교를 통해 탐색할 범위를 절반씩 줄여나갈 수 있다. 따라서 평균적으로 **O(log n)**이라는 매우 빠른 검색 속도를 보장한다.
이후 내용인 DFS와 BFS는 이진 탐색 트리뿐만 아니라 모든 종류의 트리와 그래프에서 노드를 방문하는 일반적인 방법이다.
3. 깊이 우선 탐색(DFS; Depth-First Search)
DFS는 쉽게 말해 한 우물만 파는 전략으로, 한 경로를 따라 최대한 깊이 들어갔다가, 더 이상 갈 곳이 없으면 바로 전 갈림길로 돌아와 다른 경로를 탐색하는 방식이다. 마치 미로를 탐색할 때, 막다른 길을 만날 때까지 한쪽 벽을 따라 계속 가보는 것과 같다.
DFS는 재귀 호출이나 스택(Stack) 자료구조를 이용해 구현한다. 재귀적으로 함수를 호출하는 것 자체가 시스템의 콜 스택을 사용하는 것과 같기 때문에, 결과적으로는 스택 구조를 사용하게 된다.
StackOverflowError가 발생할 수 있다. 반면, 스택을 직접 사용하는 반복문 방식은 더 안정적이며, 매우 큰 데이터나 복잡한 그래프를 다룰 때 선호된다.- 노드 구현 (Java)
- 트리 순회 방식: DFS로 트리를 순회할 때는 부모 노드를 언제 방문하느냐에 따라 3가지 방식으로 나뉜다.
3.1. 전위 순회(Pre-order)
root → left → right- 트리를 복사하거나 구조를 그대로 저장할 때 유용
- java 구현 (스택)
- Java 구현(재귀)
3.2. 중위 순회(In-order)
left → root → right- 이진 탐색 트리에서 사용하면 노드가 오름차순으로 정렬
- Java 구현(스택)
- Java 구현(재귀)
3.3. 후위 순회(Post-order)
left → right → root- 자식 노드를 먼저 처리해야 할 때(예: 파일 디렉터리 용량 계산, 트리 삭제) 사용
- Java 구현(스택)
- Java 구현(재귀)
4. 너비 우선 탐색(BFS; Breadth-First Search)
BFS는 가까운 곳부터 둘러보는 전략으로, 시작점에서 가까운 노드부터 순서대로, 같은 레벨(거리)에 있는 노드들을 모두 탐색한 후 다음 레벨로 넘어가는 방식이다. 마치 잔잔한 호수에 돌을 던졌을 때 물결이 동심원을 그리며 퍼져나가는 모습과 같다.
BFS는 먼저 들어온 것을 먼저 처리해야 하므로 큐(Queue) 자료구조를 사용해 구현한다.
4.1. 동작 과정:
- 시작 노드를 Queue에 넣고, 방문했음을 표시한다.
- 큐에서 노드를 하나 꺼낸다. (Dequeue/Poll)
- 꺼낸 노드에 연결된, 아직 방문하지 않을 모든 이웃 노드를 Queue에 넣고(Enqueue/Offer), 방문했음을 표시한다.
- 큐가 비어있을 때까지 2~3번 과정을 반복한다.
4.2. Java 구현
- 활용: BFS의 가장 큰 특징은 가중치가 없는 그래프에서 두 노드 간의 최단 경로를 찾아준다는 것이다. 이 때문에 소셜 네트워크에서 '나와 친구 사이의 최소 인맥 단계'를 찾거나, 내비게이션에서 '최소 환승 경로'를 찾는 문제 등에 널리 사용될 수 있다.
5. 결론: 어떤 탐색을 선택할 것인가?
Brute-Force는 모든 가능성을 열어두고 탐색하는 매우 기초적인 탐색법으로, DFS와 BFS는 복잡하게 얽혀있는 데이터 속에서 길을 찾을 수 있는 유용한 알고리즘이다.
- DFS(깊이): 하나의 경로를 끝까지 파고 들어야 할 때, 탐색 대상이 시작점에서 매우 멀리 있을 것으로 예상될 때, 검색 공간이 매우 넓어 메모리 사용량이 우려될 때 효과적이다.
- BFS(너비): 시작점에서 가장 가까운 대상을 탐색할 때, 즉 최단 경로를 구하는 문제에서 절대적인 강점을 가진다.
이 두 가지 탐색법은 수많은 알고리즘 문제의 근간으로, 문제의 조건이 '가장 빠른 길'을 요구하는지, 아니면 '단순히 길이 존재하는지'를 묻는지에 따라 적절한 탐색법을 선택하는 것이 문제 해결의 핵심이라고 볼 수 있다.