본문 바로가기
알고리즘&코딩테스트

[Algorithm] DFS와 BFS를 이해하기 위한 기본 지식들 - 탐색, 스택(Stack)과 큐(Stack), 재귀 함수, 그래프(Graph)

by 책 읽는 개발자_테드 2021. 1. 24.
반응형

DFS와 BFS를 이해하기 위한 기본 지식들 - 탐색, 스택과 큐, 재귀 함수

'이것이 취업을 위한 코딩 테스트다 with 파이썬'을 공부 중입니다. 이 글은 해당 서적을 통해 DFS와 BFS를 이해하기 위한 기본 지식을 공부한 내용입니다. 또한 책의 코드를 모두 자바로 변환하여 글을 작성했습니다.

학습 목표

탐색(Search)

자료구조 - 스택과 큐

재귀 함수

그래프


탐색(Search)

탐색이란 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정을 의미한다. 프로그래밍에서는 그래프, 트리 등의 자료구조 안에서 탐색을 하는 문제를 자주 다룬다. 대표적인 탐색 알고리즘으로 DFS, BFS가 있다.



자료구조 - 스택과 큐

자료구조는 ‘데이터를 표현하고 관리하고 처리하기 위한 구조’를 의미한다. 그중 스택과 큐는 자료구조의 기초 개념으로 두 가지 핵심적인 함수인 삽입(Push), 삭제(Pop)로 구성된다.

 

삽입, 삭제 이외에도 오버플로와 언더플로를 고민해야 한다. 오버플로는 특정한 자료구조가 수용할 수 있는 데이터의 크기를 이미 가득 찬 상태에서 삽입하여 저장공간을 벗어나 데이터가 넘치는 현상이다. 반면에 저장공간에 데이터가 들어 있지 않은 상태에서 삭제 연산을 수행하면 언더플로가 발생한다.

 

스택

스택은 박스 쌓기에 비유할 수 있다. 박스는 아래에서 위로 차곡차곡 쌓고, 아래에 있는 박스를 치우기 위해서는 위에 있는 박스를 먼저 내려야 한다. 이런 구조를 선입후출(First In Last Out) 또는 후입선출(Last In First Out)구조라 한다.

 

큐는 대기 줄에 비유할 수 있다. 놀이공원에 입장하기 위해 줄을 설 때, 먼저 온 사람이 먼저 들어가게 된다. 나중에 온 사람일수록 나중에 들어가기 때문에 ‘공정한’ 자료구조라고 비유된다. 이러한 구조는 선입선출(First In First Out) 구조라고 한다.

 



재귀 함수

재귀 함수란 자기 자신을 다시 호출하는 함수를 의미한다. 아래는 간단한 재귀 함수의 예로 문자열을 무한히 출력한다.

 

public class RecursiveExam {

   public static void main(){

       RecursiveExam recursiveExam = new RecursiveExam();
       recursiveExam.recursiveFuntion();
   }

   void recursiveFuntion(){
       System.out.println("재귀 함수를 호출합니다.");
   }
}

 

이렇게 재귀 함수가 무한히 호출될 수 있기 때문에 종료 조건을 명시해야한다.

예를 들어 다음과 같이 재귀 함수가 100번 호출되도록 작성할 수 있다.

 

public class RecursiveExam {

   public static void main(){

       RecursiveExam recursiveExam = new RecursiveExam();
       recursiveExam.recursiveFuntion(1);
   }

   void recursiveFuntion(int i){

       if(i==100)
           return;

       System.out.println(i+"번째 재귀 함수에서"+(i+1)+"번째 재귀 함수를 호출");
       recursiveFuntion(i +1);
       System.out.println(i+"번쨰 재귀 함수 종료");
   }
}

 

컴퓨터 내부에서 재귀 함수의 수행은 스택 자료구조를 이용한다. 함수를 계속 호출했을 때 가장 마지막에 호출한 함수가 먼저 수행을 끝내야 그 앞의 함수 호출이 종료되기 때문이다. 따라서 스택 자료구조를 활용해야 하는 상당수 알고리즘은 재귀 함수를 이용해서 간편하게 구현할 수 있다. DFS가 대표적인 예다.

 

이번에는 재귀 함수를 이용하는 대표적인 예제로 팩토리얼 문제를 구현해보자. 팩토리얼 문제는 반복적(Iterative) 방법, 재귀적(Recursive) 방법 두 가지로 구현할 수 있다.

 

public class FactorialExam {

   public static void main(String[] args){

       FactorialExam factorialExam = new FactorialExam();

       System.out.println(factorialExam.factorialIterative(5));
       System.out.println(factorialExam.factorialRecursive(5));
   }

   //반복적 방법의 팩토리얼
   int factorialIterative(int n){
   
       int result =1;
       
       for(int i=1; i<=n; i++){
           result = result * i;
       }

       return result;
   }

   //재귀적 방법의 팩토리얼
   int factorialRecursive(int n){

       if(n<=1)
           return 1;
           
       return n * factorialRecursive(n-1);
   }
}

 

FactorialExam결과

 

실행 결과를 동일하다. 이때 재귀 함수의 장점은 코드가 간결하다는 것이다. 이렇게 코드가 간결한 이유는 재귀 함수가 수학의 점화식(재귀식)을 그대로 소스코드로 옮겼기 때문이다. 수학에서 점화식은 특정한 함수를 자신보다 더 작은 변수에 대한 함수와의 관계로 표현한 것이다. 

 

팩토리얼 점화식 예시

n이 0 혹은 1일 때: factorial(n) = 1

n이 1보다 클 때: factorial(n) = n x factorial(n-1)



*점화식은 다이나믹 프로그래밍에 사용된다.

 

그래프

 

그래프는 노드(Node)와 간선(Edge)으로 표현되며 이때 노드를 정점(Vertex)이라고도 한다. 두 노드가 간선으로 연결되어 있다면 ‘두 노드는 인접하다(Adjacent)’라고 한다. 

 

그래프 탐색이란 하나의 노드를 시작으로 다수의 노드를 방문하는 것이다. 

 

프로그래밍에서 그래프는 크게 2가지 방식으로 표현할 수 있다. 코딩 테스트에서는 이 두 방식 모두 필요하다.

 

1. 인접 행렬(Adjacency Matrix): 2차원 배열로 그래프의 연결 관계를 표현하는 방식

2. 인접 리스트(Adjacency List): 리스트로 그래프의 연결 관계를 표현하는 방식

 

다음과 같은 노드와 간선을 코드로 표현해보자. 연결이 되어 있지 않은 노드끼리는 무한의 비용이라고 작성한다.

 

 

인접 행렬 방식은 2차원 배열에 각 노드가 연결된 형태를 기록하는 방식이다. (파이썬에서는 2차원 리스트 사용)

 

int INF = Integer.MAX_VALUE;

//2차원 배열로 인접 행렬 표현
int [][] graph = {{0,7,5},{7,0,INF},{5,INF,0}};



인접 리스트 방식은 모든 노드에 연결된 노드에 대한 정보를 차례대로 연결하여 저장한다.

 

 

인접 리스트는 ‘연결 리스트’라는 자료구조를 이용해 구현한다. 이를 위해 C++나 자바와 같은 프로그래밍 언어에서는 별도의 연결 리스트 기능을 위한 표준 라이브러리를 제공한다.

 

이에 반해 파이썬은 기본 자료형인 리스트 자료형이 append()와 메소드를 제공한다. 즉, 파이썬은 전통적인 프로그래밍 언어에서의 배열과 연결 리스트의 기능을 모두 기본으로 제공한다. 

 

아래 코드는 앞서 설명한 대로 자바로 구현한 인접 리스트다.

 

public class GraphExam {

   public static void main(String[] args){

       GraphExam graphExam = new GraphExam();

       //행이 3개인 2차원 리스트 생성
       ArrayList<ArrayList<int[]>> graph = graphExam.makeGraph(3);
       int INF = Integer.MAX_VALUE;

       // 노드 0에 연결된 노드 정보 저장(노드, 거리)
       graph.get(0).add(new int[]{1,7});
       graph.get(0).add(new int[]{2,5});

       // 노드 1에 연결된 노드 정보 저장(노드, 거리)
       graph.get(1).add(new int[]{0, 7});

       // 노드 2에 연결된 노드 정보 저장(노드, 거리)
       graph.get(2).add(new int[]{0, 5});
       graphExam.printGraph(graph);
   }

   //그래프 생성
   ArrayList<ArrayList<int[]>> makeGraph(int size){
   ArrayList<ArrayList<int[]>> graph = new ArrayList<>();

       for(int i=0; i<size;i++)
           graph.add(new ArrayList<int[]>());

       return graph;
   }

   //그래프 출력
   void printGraph(ArrayList<ArrayList<int[]>> graph){
   
       for(int i=0; i<graph.size();i++){
           for(int j=0; j<graph.get(i).size(); j++){
               System.out.print("{"+
                       graph.get(i).get(j)[0]+","+
                       graph.get(i).get(j)[1]+
                       "} ");
           }
           System.out.printf("\n");
       }
   }
}

 

두 방식은 차이 점이 있다. 

 

메모리 측면에서 보면 인접 행렬 방식은 모든 관계를 저장하므로 노드 개수가 많을수록 메모리가 불필요하게 낭비된다. 반면에 인접 리스트 방식은 연결된 정보만을 저장하므로 메모리를 효율적으로 사용한다.

 

하지만 인접 리스트 방식은 행렬 방식에 비해 특정한 두 노드가 연결되어 있는지 확인하는 속도가 느리다. 연결된 데이터를 하나씩 확인하기 때문이다. 

 

예를 들어 노드 1과 노드7이 연결되어 있는지 확인한다고 가정하자. 인접 행렬 방식은 graph[1][7]만 확인하면 되지만, 인접 리스트 방식에서는 노드 1에 대한 인접 리스트를 앞에서부터 차례로 확인해야한다.

 

 

DFS와 BFS란? 작동 방식과 구현 방법(with 자바) 설명 https://scshim.tistory.com/241

 

반응형

댓글