본문 바로가기

컴퓨터 공학/알고리즘

BFS (Breath First Search) 너비우선탐색, DFS (Depth First Search) 깊이우선탐색

이번 글에서는 코딩테스트 출제 빈도가 아주 높은 알고리즘은

BFS 너비우선탐색DFS 깊이우선탐색 에 대해 소개하겠습니다.

이번 정리 역시 프로그래머스에서 코딩테스트 연습을 하다가 BFS/DFS 영역의 문제를 풀기전 개념을 다시 한 번 정확히 정리해보고자 정리한 글입니다. 


BFS와 DFS는 그래프 자료구조를 탐색할 때 사용되는 기법입니다. 두 기법 모두 그래프에 속한 모든 노드를 방문하는 것을 목적으로 하는데, 탐색 우선 순위를 어떤 것으로 두는 지에 따라 두 기법의 차이가 있습니다. 

너비 우선 탐색 BFS (Breath First Search)

  • BFS의 개념

BFS는 이름에서 알 수 있듯이, 너비 Breath를 우선으로 탐색하는 기법입니다. 그래프 탐색의 시작점 (루트 또는 특정 노드) 과 같은 거리에 있는 노드를 우선적으로 방문하는 것입니다.그래서 BFS는 매 단계에서 가능한 모든 경우의 수를 확인할 수 있습니다.

  • Queue

BFS 알고리즘 구현의 핵심을 큐 (Queue) 자료구조를 활용하여

현재 방문한 노드의 인접한 노드 중 방문하지 않은 노드를 큐에 넣어 두고, 큐에 들어있는 노드부터 방문하면 됩니다.

파이썬에서는 list 자료형을 큐로 사용할 수 있습니다.

  • Example

너비우선탐색, wiki

너비우선탐색으로 그래프의 노드를 방문하는 과정은 위와 같습니다.

방문한 노드는 검정색으로 칠하고, 방문을 한 노도에 인접한 노드는 큐에 삽입한 뒤 회색으로 칠합니다. 그리고 아예 큐에도 삽입되지 않은 노드는 흰색으로 남겨둡니다.

위 과정을 설명해보자면 a에 방문하여 검정색으로 칠하고, 인접한 노드인 b와 c를 큐에 삽입하고 회색으로 칠합니다.

HEAD b, c TAIL

그 다음 방문할 노드는 큐에서 찾습니다. 큐의 head에서 방문할 노드를 선택하여 b를 방문하게 됩니다.

b에 방문했으니 b는 큐에서 pop되고, 검정색으로 칠합니다. 그리고 b의 인접 노드인 d와 e를 큐에 삽입하고 회색으로 칠합니다.

HEAD c, d, e TAIL

그리고 난 뒤 또 큐의 head에서 c를 방문하고, b를 방문했을 때와 같은 방식으로 큐에서 pop하고 검정색을 칠한 뒤,

c의 인접 노드인 f와 g를 큐에 삽입하고 회색으로 칠합니다. 그럼 큐는 아래와 같은 상태를 가지게 됩니다. 

HEAD d,e,f,g TAIL

이런 방식으로 노드를 차례대로 방문하고, 큐가 비게 되면 모든 노드를 방문한 것이므로 탐색을 종료하게 됩니다.

최종적으로 BFS로 노드 탐색 순서는 아래와 같습니다. 

a-b-c-d-e-f-g-h
  •  Implementation (Python3)

위 그래프는 딕셔너리리스트 자료형을 이용해 코드로 표현했습니다. 

딕셔너리의 키는 해당 노드를 나타내고, 밸류는 키에 인접한 노드를 저장한 리스트를 갖습니다.

 

gragh = {
    'a' : ['b','c'],
    'b' : ['a','d','e'],
    'c' : ['a','f','g'],
    'd' : ['b'],
    'e' : ['b','h'],
    'f' : ['e'],
    'g': ['g'],
    'h': ['e']
}

그리고 BFS는 다음과 같이 구현합니다. 설명은 주석을 참고해주세요 :) 

def bfs(graph, start_node):
    visit = list()
    # current_node의 인접한 노드를 담을 큐
    queue = list()

    queue.append(start_node)

    # queue가 비게되면 반복문 종료 
    while queue:
        # 큐의 head에 있는 값을 pop해서 current_node에 저장
        current_node = queue.pop(0)
        # current_node가 아직 방문하지 않은 노드라면 
        if current_node not in visit:
            # visit에 current_node 추가 
            visit.append(current_node)
            # current_node를 키로 하는 밸류 리스트 = graph(current_node)를 queue에 extend
            # 즉, current_node의 인접한 노드들을 queue에 삽입 
            queue.extend(graph[current_node])
        
        # current_node가 이미 방문한 노드라서 visit에 있는 경우 if 문을 그냥 지나침
    
    # queue가 빈 상태가 되어 while 문이 끝나면 visit을 반환 
    return visit 

** 참조 블로그에서는 일반적인 리스트로 pop(0)으로 큐를 사용하여 효율이 떨어지기 떄문에 큐 모듈을 사용하여 popleft()하는 것이 좋다고 합니다! 

위 코드를 실행하면 결과는 다음과 같이 나옵니다. 

더보기
def bfs(graph, start_node):
    visit = list()
    # current_node의 인접한 노드를 담을 큐
    queue = list()

    queue.append(start_node)

    # queue가 비게되면 반복문 종료 
    while queue:
        # 큐의 head에 있는 값을 pop해서 current_node에 저장
        current_node = queue.pop(0)
        # current_node가 아직 방문하지 않은 노드라면 
        if current_node not in visit:
            # visit에 current_node 추가 
            visit.append(current_node)
            # current_node를 키로 하는 밸류 리스트 = graph(current_node)를 queue에 extend
            # 즉, current_node의 인접한 노드들을 queue에 삽입 
            queue.extend(graph[current_node])
        
        # current_node가 이미 방문한 노드라서 visit에 있는 경우 if 문을 그냥 지나침
    
    # queue가 빈 상태가 되어 while 문이 끝나면 visit을 반환 
    return visit 


if __name__ == "__main__":

    gragh = {
        'a' : ['b','c'],
        'b' : ['a','d','e'],
        'c' : ['a','f','g'],
        'd' : ['b'],
        'e' : ['b','h'],
        'f' : ['e'],
        'g': ['g'],
        'h': ['e']
    }

    print(bfs(gragh, 'a'))

print(bfs(graph, 'a'))


깊이 우선 탐색 DFS (Depth First Search) 

  • DFS의 개념

DFS는 '깊이'를 우선적으로 탐색하는 기법으로 한 방향으로 갈 수 있는 깊게 탐색하는 것입니다. 

즉, 시작 노드에서 한 방향으로 나아갈 수 있는 한 노드를 방문하고 리프 노드에 도달할 때까지 한 방향으로 탐색을 합니다. 

  • Stack 스택 

DFS는 BFS와 코드가 유사한데 큐를 사용한 BFS와 달리 스택(Stack)을 사용합니다. 시작 노드를 스택에 삽입하고, pop하면서 방문했음을 표시합니다. 그리고 현재 방문한 노드의 자식 노드들을 스택에 삽입합니다. 그리고 스택 top 에 있는 노드를 pop하고 방문했음을 표시하고, 해당 노드의 자식 노드를 또 다시 스택에 삽입합니다.이 과정을 반복하면서 마침내 스택이 비게 되면 모든 노드를 방문한 것입니다.  

  • Example

dfs, wiki

깊이우선탐색이 노드를 방문하는 방식은 위와 같습니다. 

탐색 시작 노드 1 을 스택이 삽입하고, 방문 했음을 표시하면서 pop 합니다. 그리고 1의 자식 노드들을 스택에 삽입하면 스택의 상태는 다음과 같이 됩니다. 

TOP
2
5
9
BOTTOM

그 다음은 스택 탑인 2를 방문하고, 방문했음을 표시하고 pop 합니다. 그리고 2의 자식 노드인 3을 스택에 삽입합니다.

TOP
3
5
9
BOTTOM

그리고 다시 반복하여 스택 탑에 있는 3을 방문하고, 방문했음을 표시하고, pop 한뒤, 3의 자식 노드인 4를 스택에 삽입합니다.

TOP
4
5
9
BOTTOM

스택 탑에 있는 4를 방문하고, 방문했음을 표시하고, pop 합니다. 4는 자식 노드가 없으므로 스택에 삽입할 노드가 없습니다. 

TOP
5
9
BOTTOM

이렇게 되면 한 방향의 리프노드까지 도달했으므로 한 방향의 탐색은 끝이 난 것입니다. 그리고 처음 분기점이었던2, 5, 9 노드로 돌아가 그 중 아직 방문 표시가 되지 않은 5와 9부터 다시 깊이탐색을 시작하는 것입니다. 간단히 스택의 탑부터 다시 그동안의 과정을 반복해주면 되는 것이죠! 

  • Implementation (Python3) 

그래프 자료 구조는 BFS와 동일하게 표현했습니다.

DFS 함수는 다음과 같이 구현했습니다. 설명은 주석을 참고해주세요 :) 

def dfs(graph, start_node):
    visit = list()
    stack = list()

    stack.append(start_node)

    # stack이 비게되면 반복문 종료 
    while stack:
        # 스택 탑에 있는 값을 pop해서 current_node에 저장
        current_node = stack.pop()
        # current_node가 아직 방문하지 않은 노드라면 
        if current_node not in visit:
            # visit에 current_node 추가해서 방문했음을 표시
            visit.append(current_node)
            # current_node를 키로 하는 밸류 리스트 = graph(current_node)를 queue에 extend
            # 즉, current_node의 자식 노드를 스탭에 삽입  
            stack.extend(graph[current_node])
        
        # current_node가 이미 방문한 노드라서 visit에 있는 경우 if 문을 그냥 지나침
    
    # 스택이 빈 상태가 되어 while 문이 끝나면 visit을 반환 
    return visit 

DFS를 실행한 결과 탐색 순서는 다음과 같습니다.

더보기
def dfs(graph, start_node):
    visit = list()
    stack = list()

    stack.append(start_node)

    # stack이 비게되면 반복문 종료 
    while stack:
        # 스택 탑에 있는 값을 pop해서 current_node에 저장
        current_node = stack.pop()
        # current_node가 아직 방문하지 않은 노드라면 
        if current_node not in visit:
            # visit에 current_node 추가해서 방문했음을 표시
            visit.append(current_node)
            # current_node를 키로 하는 밸류 리스트 = graph(current_node)를 queue에 extend
            # 즉, current_node의 자식 노드를 스탭에 삽입  
            stack.extend(graph[current_node])
        
        # current_node가 이미 방문한 노드라서 visit에 있는 경우 if 문을 그냥 지나침
    
    # 스택이 빈 상태가 되어 while 문이 끝나면 visit을 반환 
    return visit 


if __name__ == "__main__":

    gragh = {
        'a' : ['b','c'],
        'b' : ['a','d','e'],
        'c' : ['a','f','g'],
        'd' : ['b'],
        'e' : ['b','h'],
        'f' : ['e'],
        'g': ['g'],
        'h': ['e']
    }

    print(dfs(gragh, 'a'))

print(dfs(graph, 'a'))


이번 글에서는 BFS와 DFS의 개념에 대해 알아보고 간단히 구현해보았습니다. 

코딩테스트나 기술 면접에 아주 자주 출제되는 개념이라고 하니 잘 알아두어야겠습니다 ㅎㅎ

글을 작성하고 조금 더 구글링을 해보니 제가 작성한 코드를 조금 효율이 떨어지는 것 같습니다. 

** deque를 사용하거나 그래프 구현시 딕셔너리가 아니라 리스트로 구현해서

set 함수를 활용하는 방식 등이 있다고 하네요

더 효율적인 코드로 발전해보겠습니다 ㅎㅎ

감사합니다 :) 


References