트리 순회

트리 순회에는 전위(preorder traverse), 중위(inorder traverse), 후위(postorder traversal) 순회가 있습니다.

preorder traverse

전위 순회는 루트 -> 왼쪽 -> 오른쪽 순서대로 방문합니다.

public ArrayList<String> preOrder(Node node, ArrayList<String> list) {
    if (node != null) {
      list.add(node.getData());
      list = preOrder(node.getLeft(), list);
      list = preOrder(node.getRight(), list);
    }
    return list;
}
  • 여기서 먼저 순회를 시작할 node 의 data 를 저장한 후 재귀함수를 통해 Left 를 순회합니다.
  • 이후 right 를 순회한 뒤 list 를 반환합니다.

inorder traverse

중위 순회는 왼쪽 -> 루트 -> 오른쪽 순서대로 방문합니다.

public ArrayList<String> preOrder(Node node, ArrayList<String> list) {
    if (node != null) {
      list = preOrder(node.getLeft(), list);
      list.add(node.getData());
      list = preOrder(node.getRight(), list);
    }
    return list;
}
  • 왼쪽 노드를 다 방문한 후 루트를 방문하고, 오른쪽 노드를 방문합니다.

postorder traverse

후위 순회는 왼쪽 -> 오른쪽 -> 루트 순서대로 방문합니다.

public ArrayList<String> postOrder(Node node, ArrayList<String> list) {
    if (node != null) {
      list = postOrder(node.getLeft(), list);
      list = postOrder(node.getRight(), list);
      list.add(node.getData());
    }
    return list;
}

그래프 순회

그래프 탐색은 정점부터 시작해 그래프의 모든 정점들을 한 번씩 방문(탐색)하는 것이 목적입니다. 그래프 탐색에는 가까운 정점부터 탐색하는 BFS(Breadth-First Search, 너비우선탐색) DFS(Depth-First Search, 깊이우선탐색) 가 있습니다.

BFS

BFS 는 큐를 사용하여 구현됩니다. BFS 에서는 시작 정점을 큐에 삽입한 후, 인접한 정점을 큐에 차례대로 삽입하면서 탐색을 진행합니다. 특징은 다음과 같습니다.

  • 최단 경로 탐색에 유리합니다.
  • 방문한 정점들을 저장해야 하는 경우 메모리 사용이 큽니다. 따라서 그래프의 크기가 큰 경우에는 BFS 보다 DFS 를 사용해야 합니다.
  • 그래프의 밀도가 높거나(간선의 개수가 많거나) 그래프의 크기가 크면(정점의 개수가 많으면) BFS 의 성능이 떨어집니다.
  • visited 배열과 같은 방문 여부를 체크하는 자료 구조를 사용해야 합니다.

다음과 같이 구현할 수 있습니다.

/**
     * BFS 인접행렬 : 너비 우선 탐색을 구현한 템플릿 예제입니다.
     *
     * @param array : 각 정점을 행/열로 하고, 연결된 정점은 1로, 연결되지 않은 정점은 0으로 표기하는 2차원 배열
     * @param visited : 방문여부 확인을 위한 배열
     * @param src : 방문할 정점
     * @param result : 방문했던 정점 리스트를 저장한 List
     *
     **/
    public ArrayList<Integer> bfs_array(int[][] array, boolean[] visited, int src, ArrayList<Integer> result) {
        //bfs의 경우 큐를 사용합니다.
	    Queue<Integer> queue = new LinkedList<>();
        //시작 지점을 큐에 넣어주고, 해당 버택스의 방문 여부를 변경합니다.
	    queue.offer(src);
	    visited[src] = true;
        //큐에 더이상 방문할 요소가 없을 경우까지 반복합니다.
	    while (!queue.isEmpty()) {
            //현재 위치를 큐에서 꺼낸 후
	      int cur = queue.poll();
            // 현재 방문한 정점을 result에 삽입합니다.
				result.add(cur);
        //전체 배열에서 현재 버택스의 행만 확인합니다.
	      for (int i = 0; i < array[cur].length; i++) {
            //길이 존재하고, 아직 방문하지 않았을 경우
	        if(adjArray[cur][i] == 1 && !visited[i]) {
                //큐에 해당 버택스의 위치를 넣어준 이후
	          queue.offer(i);
                //방문 여부를 체크합니다.
	          visited[i] = true;
	        }
	      }
	    }
			//이어진 모든 길을 순회한 후 방문 여부가 담긴 ArrayList를 반환합니다.
	    return result;
  }

BFS 활용

  • 최단 거리를 구해야 하는 경우
  • 검색 대상의 규모가 크지 않고, 검색 시작 지점으로부터 원하는 대상이 별로 멀지 않은 경우

위 두가지 경우에 BFS 를 사용하면 유리합니다. 아래에서 최단 경로를 찾는 문제를 살펴보겠습니다.

먼저 입력입니다.

int[][] graph = {
            {0, 1, 1, 0, 0, 0},
            {1, 0, 0, 1, 1, 0},
            {1, 0, 0, 0, 0, 1},
            {0, 1, 0, 0, 0, 0},
            {0, 1, 0, 0, 0, 1},
            {0, 0, 1, 0, 1, 0}
        };
int start = 0;
int end = 5;

shortestPath(graph, start, end); // [0, 2, 5]

로직입니다.

public ArrayList<Integer> shortestPath(int[][] graph, int start, int end) {
  boolean[] visited = new boolean[graph.length];
  return bfs(graph, start, end, visited);
}

public ArrayList<Integer> bfs(int[][] graph, int start, int end, boolean[] visited) {
  //bfs의 경우 큐를 사용합니다.
  Queue<Integer> queue = new LinkedList<>();
  //i번째 인덱스의 부모노드를 저장합니다. ex) parent[1] = 2 --> 1 의 부모노드는 2
  int[] parent = new int[graph.length];

  //큐에 start 를 저장합니다.
  queue.offer(start);
  //start 방문을 체크합니다.
  visited[start] = true;
  // start 의 부모노드는 없으므로 -1 을 저장합니다.
  parent[start] = -1;

  
  while (!queue.isEmpty()) {
    //현재 노드를 큐에서 빼옵니다.
    int node = queue.poll();

    // 도착점까지 도달한다면
    if (node == end) {
      //반환할 list 를 만듭니다.
      ArrayList<Integer> path = new ArrayList<>();
      while (node != -1) {
        //path 에 현재 노드를 저장합니다.
        path.add(node);
        //parent 를 통해 현재노드의 부모노드로 올라갑니다.
        node = parent[node];
      }
      // 도착점 -> 시작점 순으로 저장되어있으므로 뒤집어줍니다.
      Collections.reverse(path);
      return path;
    }

    //현재 노드에서 방문 가능한 모든 노드를 저장합니다.
    for (int i = 0; i < graph.length; i++) {
      if (graph[node][i] == 1 && !visited[i]) {
        //노드 i 를 큐에 저장합니다.
        queue.add(i);
        //i 번째 visited 를 true 로 변경합니다.
        visited[i] = true;
        //i 번째 부모노드를 현재 노드로 설정합니다.
        parent[i] = node;
      }
    }
  }
  // 끝까지 길을 찾기 못한 경우, null을 반환.
  return null;
}

DFS

DFS 는 스택 또는 재귀를 이용하여 구현할 수 있습니다. 특징은 다음과 같습니다.

  • BFS 보다 탐색 시간은 오래 걸릴지라도 모든 정점을 완전히 탐색할 수 있습니다.
  • 현 경로 상의 정점들만 기억하면 되므로 저장 공간의 수요가 비교적 적습니다.
  • 목표한 정점이 깊은 단계에 있으면 값을 빨리 구할 수 있습니다.
  • 깊이 우선 탑색은 그래프 내의 순환구조를 고려하여 이미 방문한 정점을 다시 방문하지 않도록 해야합니다.
  • 찾아낸 길이 최단 경로가 된다는 보장이 없습니다.

DFS 는 일반적으로 재귀를 통해 구현합니다.

**
     * DFS 인접행렬 : 깊이 우선 탐색을 구현한 템플릿 예제입니다.
     *
     * @param array :  정점을 /열로 하고, 연결된 정점은 1, 연결되지 않은 정점은 0으로 표기하는 2차원 배열
     * @param visited : 방문 여부 확인을 위한 배열
     * @param node : 방문할 정점
     * @param result : 방문했던 정점 리스트를 저장한 List
     *
     **/
    public ArrayList<Integer> dfs(int[][] array, boolean[] visited, int node, ArrayList<Integer> result) {
        // 이미 방문했다면
        if (visited[node] == true) {
                result.add(node);    // 방문한 정점을 저장
                return result;      // 저장한 데이터를 반환하며, 재귀호출을 종료
            }

        // 아직 방문하지 않았다면
        visited[node] = true;	// 방문한 정점을 표기
        
		// 현재 정점에서 이동할 수 있는 정점을 순회하며 재귀 호출
        for (int index = 0; index < array.length; index++) {
            if (array[node][i] == 1) {
            	// 재귀 호출을 통해, 방문 여부를 담은 데이터를 반환과 동시에 할당
            	result = dfs(array, visited, i, result);
            }
        }
        return result;
    }

DFS 활용

  • 경로의 특징을 저장해야 하는 경우
    • 예를 들면 각 정점에 숫자가 적혀있고 a부터 b까지 가는 경로를 구하는데 경로에 같은 숫자가 있으면 안 된다는 문제 등, 각각의 경로마다 특징을 저장해 둬야 할 때는 DFS를 사용합니다. (BFS는 경로의 특징을 가지지 못합니다)
  • 미로 문제
  • 검색 대상이 클 경우

다음은 2차원 배열에서 섬의 개수는 구하는 문제입니다. 2차원 배열에서 1은 땅을 나타내고, 0은 물을 나타냅니다.상하좌우 인접한 땅은 같은 섬으로 간주합니다. 이 때 섬의 개수를 구하는 알고리즘입니다.

입력

int grid[][] = new int[][]{
	{"1","1","1","0","0"},
  {"1","1","0","0","0"},
  {"1","1","0","0","1"},
  {"0","0","0","1","1"}
};

numIslands(grid) // 2

로직

class Solution {
    public int numIslands(char[][] grid) {
        // 입력된 grid가 비어있을 경우, 섬이 존재하지 않으므로 0을 반환합니다.
        if (grid == null || grid.length == 0) {
            return 0;
        }

        int numIslands = 0; // 섬의 수를 담을 변수를 선언, 초기화
        int numRows = grid.length; // Row의 최대 길이
        int numCols = grid[0].length; //Col의 최대 길이

        // 대륙을 순회합니다. 이중 loop
        for (int i = 0; i < numRows; i++) {
            for (int j = 0; j < numCols; j++) {
                // 현재 방문한 정점이 1이라면(섬이라면)
                if (grid[i][j] == '1') {
                    numIslands++; // 섬의 수를 증가시키고
                    dfs(grid, i, j); // DFS를 호출하여 주변의 이어진 섬이 있는지 확인한 후 모두 0 으로 변경합니다.
                }
            }
        }

        return numIslands;
    }

    private void dfs(char[][] grid, int row, int col) {
        // 섬의 최대 길이 확인
        int numRows = grid.length;
        int numCols = grid[0].length;

        // 이동 후, 섬의 크기를 벗어난 경우(예외 처리)
        if (row < 0 || col < 0 || row >= numRows || col >= numCols || grid[row][col] == '0') {
            return;
        }

        // 현재 방문한 땅을 0 할당(방문 여부 체크)
        grid[row][col] = '0';

        // Down, Up, Right, Left 이동을 재귀 호출을 통해서 처리
        dfs(grid, row + 1, col);
        dfs(grid, row - 1, col);
        dfs(grid, row, col + 1);
        dfs(grid, row, col - 1);
    }
}

댓글남기기