깊이 우선 탐색

  • 깊이 우선 탐색 개념

    • 그래프의 모든 정점을 발견하는 가장 단순하고 고전적인 방법
    • 현재 정점과 인접한 간선들을 하나씩 검사하다가, 아직 방문하지 않은 정점으로 향하는 간선이 있다면 그 간선을 무조건 탐색
    • 해당 과정에서 더이상 갈 곳이 없는 막힌 정점에 도달하면 포기하고, 마지막에 따라왔던 간선을 따라 뒤로 돌아간다.
    • 모든 노드를 방문하고자 하는 경우에 선택
    • 깊이 우선 탐색(DFS)가 너비 우선 탐색(BFS)보다 간단하다.
    • 단순 검색 속도 자체는 너비 우선 탐색(BFS)에 비해 느리다.
  • 깊이 우선 탐색(DFS)의 특징

    • 순환 알고리즘의 형태를 갖고 있다.
    • 전위 순회(Pre-Order)를 포함한 다른 형태의 트리 순회는 모두 DFS의 한 종류이다.
    • 그래프 탐색할 때 어떤 노드를 방문했었는지 여부를 반드시 검사한다.

코드 설계

  • 시작 노드(1)로부터 인접 노드(2, 3)를 모두 방문
  • 인접 노드(2, 3)를 스택에 넣고 방문처리
  • 시작 노드(1)로부터 인접한 노드 중에 방문하지 않은 노드가 더이상 없는 경우
  • 이전의 위치로 돌아와 찾아가지 않은 다른 길로 탐색한다.

코드 구현

  • 순환 호출을 이용한 DFS 구현
    public class DFSList {

     static class Graph {
         private int V;
         private LinkedList<Integer>[] adjacent;

         @SuppressWarnings("unchecked")
         public Graph(int V) {
             this.V = V;
             this.adjacent = new LinkedList[V];
             for (int i = 0; i < V; i++)  adjacent[i] = new LinkedList<>();
         }

         public void addEdge(Graph graph, int src, int dest) {
             graph.adjacent[src].add(dest);
             graph.adjacent[dest].add(src);
         }

         public void printGraph(Graph graph) {
             for (int v = 1; v < graph.V; v++) {
                 System.out.println("Node [" + v + "]");
                 System.out.print("adjacent Node: ");
                 for (Integer node : graph.adjacent[v]) {
                     System.out.print("[" + node + "]\t");
                 }
                 System.out.println();
             }
         }

         /**
          * 비연결형 그래프의 경우, 모든 정점을 하나씩 방문하는 너비 우선 탐색
          * @param start
          */
         public void dfs() {
             boolean[] visit = new boolean[V];

             for(int i = 1 ; i < V ; i++) {
                 if(visit[i] == false) 
                     dfsUtil(i, visit);
                 System.out.println();
             }
         }

         /**
          * 주어진 노드를 시작한 너비 우선 탐색
          * @param start
          */
         public void dfs(int start) {
             boolean[] visit = new boolean[V];
             dfsUtil(start, visit);
             System.out.println();
         }

         /**
          * 인접한 노드 방문 하는 DFS 재귀함수
          * @param start
          * @param visit
          */
         private void dfsUtil(int start, boolean[] visit) {
             visit[start] = true; // 방문 체크
             System.out.print(start + "\t");

             Iterator<Integer> it = adjacent[start].listIterator(); // 인접한 노드 리스트
             while(it.hasNext()) {
                 int n = it.next();
                 if(!visit[n]) // 방문 확인 false인 경우, 노드 탐색
                     dfsUtil(n, visit);
             }
         }
     }

     public static void main(String[] args) {
         int initSize = 8;

         Graph graph = new Graph(initSize);

         graph.addEdge(graph, 1, 2);
         graph.addEdge(graph, 1, 3);
         graph.addEdge(graph, 2, 3);
         graph.addEdge(graph, 2, 4);
         graph.addEdge(graph, 2, 5);
         graph.addEdge(graph, 3, 6);
         graph.addEdge(graph, 3, 7);
         graph.addEdge(graph, 4, 5);
         graph.addEdge(graph, 6, 7);

         graph.printGraph(graph);

         graph.dfs(4); /* 주어진 노드를 기준으로 BFS 탐색 */
         graph.dfs();
     }
    }
  • Graph의 각 노드에 연결된 노드 리스트 및 DFS 결과
    // 노드별 연결 리스트 확인
    Node [1]
    adjacent Node: [2]    [3]    
    Node [2]
    adjacent Node: [1]    [3]    [4]    [5]    
    Node [3]
    adjacent Node: [1]    [2]    [6]    [7]    
    Node [4]
    adjacent Node: [2]    [5]    
    Node [5]
    adjacent Node: [2]    [4]    
    Node [6]
    adjacent Node: [3]    [7]    
    Node [7]
    adjacent Node: [3]    [6]    
    // 주어진 노드를 기준으로 DFS 탐색
    4    2    1    3    6    7    5    
    // 모든 노드 탐색
    1    2    3    6    7    4    5    

분석

  • 깊이 우선 탐색(DFS)의 시간 복잡도
    • 인접 리스트로 표현된 그래프: O(N+E)
    • 인접 행렬로 표현된 그래프: O(N^2)

너비 우선 탐색

  • 루트노드(또는 다른 노드)에서 시작해서 인접한 노드를 먼저 탐색하는 방법

    • 시작 정점(노드)으로부터 가까운 정점을 방문하고 멀리 떨어져 있는 정점을 나중에 방문하는 순회 방법
  • 맹목적 탐색방법의 하나로 시작 정점을 방문한 후 시작 정점에 인접한 모든 정점들을 우선 방문하는 방법

    • 맹목적 탐색방법이란 이미 정해진 순서에 따라 상태 공간 그래프를 점차 형성해 가면서 해를 탐색하는 방법
  • 더 이상 방문하지 않은 정점이 없을 때까지 방문하지 않은 모든 정점들에 대해서도 너비 우선 검색을 적용

  • 너비 우선 탐색 알고리즘의 장점

    • 너비 우선 탐색은 출발노드에서 목표노드까지의 최단 길이 경로를 보장
  • 너비 우선 탐색 알고리즘의 단점

    • 경로가 매우 길 경우에는 탐색 가지가 급격히 증가함에 따라 보다 많은 기억 공간을 필요로 하게 된다.
      • 해가 존재하지 않는다면 유한 그래프(finite graph)의 경우에는 모든 그래프를 탐색한 후에 실패로 끝난다.
      • 무한 그래프(infinite graph)의 경우에는 해를 찾지도 못하고, 끝내지도 못한다.

코드 설계

  • 기준이 되는 노드의 상위 노드 검색, 없을 경우 인접 노드 검색
  • 같은 레벨의 노드 검색 후, 해당 레벨 + 1의 인접 노드 검색
  • 리스트를 확인할 수 있는 Queue, 방문 확인을 위한 visit 배열 생성
  • 사용자가 입력한 기준 값으로 queue 넣고 해당 queue의 node 값에 인접한 adjacent node 리스트를 검색하여 방문 표시

코드 구현

  • 인접행렬을 기반으로 한 너비 우선 탐색
public class GraphArr {

    private int[][] MATRIX; // 그래프 배열 선언

    public GraphArr() {}

    public GraphArr(int initSize) {
        this.MATRIX = new int[initSize + 1][initSize + 1];
    }

    public void put(int x, int y) {
        MATRIX[x][y] = MATRIX[y][x] = 1;
    }

    public void printGraph() {
        for (int i = 0; i < MATRIX.length; i++) {
            for (int j = 0; j < MATRIX[i].length; j++) {
                System.out.print(MATRIX[i][j] + " ");
            }
            System.out.println();
        }
    }

    /**
     * 너비 우선 탐색
     * @param V
     */
    private void bfs(int V) {

        Queue<Integer> queue = new LinkedList<>();
        boolean[] visit = new boolean[MATRIX.length]; // 간선 갯수

        queue.offer(V);
        visit[V] = true;

        while(!queue.isEmpty()) {
            int x = queue.poll(); // 방문한 노드를 큐에서 꺼내 인접한 모든 노드를 조회
            System.out.print(x + "\t");
            for(int i = 0 ; i < MATRIX.length ; i++) {

                if(!visit[i] && MATRIX[x][i] == 1) { // 간선확인, 노드 방문 여부 확인
                    visit[i] = true;
                    queue.offer(i);
                }
            }
        }
    }

    public static void main(String[] args) {

        int initSize = 7;

        GraphArr graph = new GraphArr(initSize);

        graph.put(1, 2);
        graph.put(1, 3);
        graph.put(2, 3);
        graph.put(2, 4);
        graph.put(2, 5);
        graph.put(3, 6);
        graph.put(3, 7);
        graph.put(4, 5);
        graph.put(6, 7);

        graph.printGraph();
        graph.bfs(1); /* 주어진 노드를 기준으로 BFS 탐색 */
    }
}
  • 인접 배열 출력 및 BFS 탐색 결과
// 배열 출력
0 0 0 0 0 0 0 0 
0 0 1 1 0 0 0 0 
0 1 0 1 1 1 0 0 
0 1 1 0 0 0 1 1 
0 0 1 0 0 1 0 0 
0 0 1 0 1 0 0 0 
0 0 0 1 0 0 0 1 
0 0 0 1 0 0 1 0 
// BFS 탐색
1    2    3    4    5    6    7    
  • 인접 리스트를 기반으로 한 너비 우선 탐색
public class GraphList {

    static class Graph {
        private int V;
        private LinkedList<Integer>[] adjacent;

        @SuppressWarnings("unchecked")
        public Graph(int V) {
            this.V = V;
            this.adjacent = new LinkedList[V];
            for (int i = 0; i < V; i++)  adjacent[i] = new LinkedList<>();
        }

        public void addEdge(Graph graph, int src, int dest) {
            graph.adjacent[src].add(dest);
            graph.adjacent[dest].add(src);
        }

        public void printGraph(Graph graph) {
            for (int v = 1; v < graph.V; v++) {
                System.out.println("Node [" + v + "]");
                System.out.print("adjacent Node: ");
                for (Integer node : graph.adjacent[v]) {
                    System.out.print("[" + node + "]\t");
                }
                System.out.println();
            }
        }

        /**
         * 너비 우선 탐색
         * @param start
         */
        public void bfs(int start) {
            boolean[] visit = new boolean[V];
            LinkedList<Integer> queue = new LinkedList<>();

            visit[start] = true;
            queue.offer(start);

            while(!queue.isEmpty()) {
                start = queue.poll();
                System.out.print(start + "\t");

                Iterator<Integer> i = adjacent[start].listIterator();
                while(i.hasNext()) {
                    int n = i.next();
                    if(!visit[n]) {
                        visit[n] = true;
                        queue.offer(n);
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        int initSize = 8;

        Graph graph = new Graph(initSize);

        graph.addEdge(graph, 1, 2);
        graph.addEdge(graph, 1, 3);
        graph.addEdge(graph, 2, 3);
        graph.addEdge(graph, 2, 4);
        graph.addEdge(graph, 2, 5);
        graph.addEdge(graph, 3, 6);
        graph.addEdge(graph, 3, 7);
        graph.addEdge(graph, 4, 5);
        graph.addEdge(graph, 6, 7);

        graph.printGraph(graph);

        graph.bfs(1); /* 주어진 노드를 기준으로 BFS 탐색 */
    }
}
  • 인접 리스트 출력 및 BFS 탐색 결과
// 기준 노드 및 인접 노드 출력
Node [1]
adjacent Node: [2]    [3]    
Node [2]
adjacent Node: [1]    [3]    [4]    [5]    
Node [3]
adjacent Node: [1]    [2]    [6]    [7]    
Node [4]
adjacent Node: [2]    [5]    
Node [5]
adjacent Node: [2]    [4]    
Node [6]
adjacent Node: [3]    [7]    
Node [7]
adjacent Node: [3]    [6]    
// BFS 탐색 1 기준
1    2    3    4    5    6    7

분석

  • 인접 리스트 시간 복잡도
    • O(N+E)
  • 인접 행렬 시간 복잡도
    • O(N^2)
  • 그래프 내에 적은 숫자의 간선만을 갖는 희소 그래프(Sparse Graph)의 경우 인접 행렬보다 인접 리스트를 사용하는 것이 유리

그래프

  • 개념

    • 단순히(node, N)와 그 노드를 연결하는 간선(edge, E)을 하나로 모아 놓은 자료구조
    • 연결되어 있는 객체 간의 관계를 표현할 수 있는 자료구조
  • 용어

    • 정점(vertex): 위치라는 개념. (node 라고도 부름)
    • 간선(edge): 위치 간의 관계. 즉, 노드를 연결하는 선 (link, branch 라고도 부름)
    • 인접 정점(adjacent vertex): 간선에 의 해 직접 연결된 정점
    • 정점의 차수(degree): 무방향 그래프에서 하나의 정점에 인접한 정점의 수
    • 무방향 그래프에 존재하는 정점의 모든 차수의 합 = 그래프의 간선 수의 2배
    • 진입 차수(in-degree): 방향 그래프에서 외부에서 오는 간선의 수 (내차수 라고도 부름)
    • 진출 차수(out-degree): 방향 그래픙에서 외부로 향하는 간선의 수 (외차수 라고도 부름)
    • 방향 그래프에 있는 정점의 진입 차수 또는 진출 차수의 합 = 방향 그래프의 간선의 수(내차수 + 외차수)
    • 경로 길이(path length): 경로를 구성하는 데 사용된 간선의 수
    • 단순 경로(simple path): 경로 중에서 반복되는 정점이 없는 경우
    • 사이클(cycle): 단순 경로의 시작 정점과 종료 정점이 동일한 경우
  • 특징

    • 그래프는 네트워크 모델이다.
    • 2개 이상의 경로가 가능하다.
    • 즉, 노드들 사이에 무방향/방향에서 양방향 경로를 가질 수 있다.
    • self-loop 뿐 아니라 loop/circuit 모두 가능하다.
    • 루트 노드라는 개념이 없다.
    • 부모-자식 관계라는 개념이 없다.
    • 순회는 DFS나 BFS로 이루어진다.
    • 그래프는 순환(Cyclic) 혹은 비순환(Acyclic)이다.
    • 그래프는 크게 방향 그래프와 무방향 그래프가 있다.
    • 간선의 유무는 그래프에 따라 다르다.
    • 해당 자료구조는 그래프 구조와 그래프 관리에 사용되는 알고리즘에 영향을 받는다.
    • 이론적으로 그래프는 리스트와 행렬 구조 중의 하나로 구별 가능하지만, 실제 적용에 있어서 최적의 자료구조는 두 구조의 조합된 형태를 띤다.
  • 종류

    • 무방향 그래프 & 방향 그래프
    • 가중치 그래프(Weighted Graph)
    • 연결 그래프 & 비연결 그래프
    • 사이클(Cycle) & 비순환 그래프(Acyclic Graph)
    • 완전 그래프(Complete Graph)
  • 구현방법 2가지

    1. 인접 리스트(Adjacency List)

      • 모든 정점(vertex) 및 노드(Node)를 리스트(ArrayList, LinkedList)에 저장. 즉, 각각의 노드에 인접한 노드들을 저장
      • 정점 index를 통해 각 노드에 인접한 노드 리스트를 확인할 수 있다.
      • 무방향 그래프에서는 간선이 인접한 노드에 모두 저장되기 때문에 두번 저장된다.
        ex) 정점의 수: N, 간선의 수:E인 무방향 그래프인 경우
        N개의 리스트, N개의 배열, 2E개의 노드가 필요
    2. 인접 배열(Adjacency Matrix)

      • 정점 혹은 노드의 개수가 N인 그래프를 인접 행렬로 표현
      • 간선의 수와 무관하게 항상 n^2개의 메모리 공간이 필요
      • 무방향 그래프를 인접 행렬로 표현한다면, 대칭 행렬이 된다.
      • 인접 리스트의 경우 인접한 노드의 리스트를 갖고 있기 때문에 인접한 노드를 쉽게 찾을 수 있지만, 인접 행렬에서는 인접한 노드를 찾기 위해서는 모든 노드를 전부 순회해야 한다.
  • 인접 리스트와 인접 행렬의 용도 차이

    1. 인접 리스트
      • 그래프 내에 적은 숫자의 간선만을 갖는 희소 그래프(Sparse Graph)인 경우
      • 어떤 노드에 인접한 노드를 찾는 경우
      • 단, 간선의 존재 여부와 정점의 차수: 정점 i의 리스트에 있는 도드의 수, 즉 정점 차수만큼의 시간이 필요
    2. 인접 행렬
      • 그래프에 간선이 많이 존재하는 밀집 그래프(Dense Graph)의 경우
      • 두 정점을 연결하는 간선의 존재여부(M[i][j])를 O(1)안에 즉시 알 수 있다.
      • 정점의 차수는 O(N)안에 알 수 있다. (인접 배열의 i번 째 행 또는 열을 모두 더한다.)
      • 단, 어떤 노드에 인접한 노드들을 찾기 위해서는 모든 노드를 전부 순회해야한다.
      • 그래프에 존재하는 모든 간선의 수는 O(n^2) 안에 알 수 있다. (인접 행렬 전체를 검색)

코드 설계

  1. 루트에서 시작한다.
  2. 자식 노드들을 [1]에 저장한다.
  3. [1]에 저장된 노드들을 차례로 방문한다. 또한 각각의 자식들을 [2]에 저장한다.
  4. [2]에 저장된 노드들을 차례로 방문한다. 또한 각각의 자식들을 [3]에 저장한다.
  5. 위의 과정을 반복한다.
  6. 모든 노드를 방문하면 탐색을 마친다.

코드 구현

  • 행렬로 구현한 그래프 (인접행렬)
public class Graph {
    private int[][] MATRIX; // 그래프 배열 선언

    public Graph() {}

    public Graph(int initSize) {
        this.MATRIX = new int[initSize + 1][initSize + 1];
    }

    public void put(int x, int y) {
        MATRIX[x][y] = MATRIX[y][x] = 1;
    }

    public void printGraph() {
        for (int i = 0; i < MATRIX.length; i++) {
            for (int j = 0; j < MATRIX[i].length; j++) {
                System.out.print(MATRIX[i][j] + " ");
            }
            System.out.println();
        }
    }
}
  • 리스트로 구현한 그래프 (인접 리스트)
public class Graph {
    private int V;
    private LinkedList<Integer>[] adjacent;

    public Graph(int V) {
        this.V = V;
        this.adjacent = new LinkedList[V];
        for (int i = 0; i < V; i++)  adjacent[i] = new LinkedList<>();
    }

    public void addEdge(Graph graph, int src, int dest) {
        graph.adjacent[src].add(dest);
        graph.adjacent[dest].add(src);
    }

    public void printGraph(Graph graph) {
        for (int v = 0; v < graph.V; v++) {
            System.out.println("Adjacency list of vertex " + v);
            System.out.print("head");
            for (Integer pCrawl : graph.adjacent[v]) {
                System.out.print(" -> " + pCrawl);
            }
            System.out.println("\n");
        }
    }
}

트리

  • 부모 노드 밑에 여러 자식 노드가 연결되고, 자식 노드 각각에 다시 자식 노드가 연결되는 재귀적 형태의 자료구조

  • 단, 자식 노드의 자식이 부모로 연결되는 경우는 보통 트리로 인정되지 않음

  • rooted tree에서는 여러가지 용어를 정의

    • 높이(height)는 루트의 높이를 0으로 하고, 자식의 높이를 원래 노드보다 1 큰 것으로 정의
    • 말단 노드(leaf node)의 정의는 자식이 없는 노드
  • unrooted tree에서는 차수가 1인 노드로 말단 노드를 정의

  • Tree 관련 용어

    • 노드(node): 트리를 구성하는 기본 원소
    • 루트 노드(root node/root): 트리에서 부모가 없는 최상위 노드, 트리의 시작점
    • 부모 노드(parent node): 루트 노드 방향으로 직접 연결된 노드
    • 자식 노드(child node): 루트 노드 반대방향으로 직접 연결된 노드
    • 형제 노드(siblings node): 같은 부모 노드를 갖는 노드들
    • 리프 노드(leaf node/leaf/terminal node): 자식이 없는 노드
    • 경로(path): 한 노드에서 다른 한 노드에 이르는 길 사이에 있는 노드들의 순서
    • 길이(length): 출발 노드에서 도착 노드까지 거치는 노드의 갯수
    • 깊이(depth): 루트 경로의 길이
    • 레벨(level): 루트 노드(1) 로 부터 노드까지 연결된 엣지의 수의 합
    • 높이(height): 가장 긴 루트 경로의 길이
    • 차수(degree): 각 노드의 자식의 갯수
    • 트리의 차수(degree of tree): 트리의 최대 차수
    • 크기(size): 노드의 개수
    • 너비(width): 가장 많은 노드를 갖고 있는 레벨의 크기
    • Tree의 응용
    • 이진트리(Binary Tree)
    • 이진탐색트리(Binary Search Tree, BST)
    • 스레드 이진 트리
    • 힙(Heap)
    • B-tree
    • 포레스트(Forest)
    • 트라이(Trie)

자료구조

  • Tree 인터페이스 작성
    • search 추상메서드와 insert 추상메서드 작성
    • Comparable 인터페이스 타입의 원소를 저장할 수 있도록 제네릭 생성
    • Node 클래스를 자식으로 갖는 또 다른 재귀 타입
public interface Tree<E extends Comparable> {
    boolean search(E toFind);
    void insert(E toInsert);
}
  • Node 클래스 작성

    • 데이터와 left, right Node 클래스를 재귀 타입으로 갖는 클래스
    • constructor, Tree 인터페이스로부터 search, insert 메서드를 구현
public class Node<E extends Comparable> implements Tree<E> {

    private E value;
    private Tree<E> left;
    private Tree<E> right;

    public Node(E value, Tree<E> left, Tree<E> right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    public E getValue() { return value; }
    public Tree<E> getLeft() { return left; }
    public Tree<E> getRight() { return right; }
    public void setLeft(Tree<E> left) { this.left = left; }
    public void setRight(Tree<E> right) { this.right = right; }

    @Override
    public boolean search(E toFind) {
        // right element 검색
        if (toFind.equals(value)) return true;

        /* 
         * toFind를 value와 비교하여 같으면 0, '>' 는 양수, '<'는 음수
         * 음수일 경우 left Node를 검색
         * 같거나 양수일 경우 right Node를 검색
         */
        if (toFind.compareTo(value) < 0) { return left.search(toFind); }

        // 위 조건문에서 양수일 경우 right Node를 검색
        return right.search(toFind);
    }

    @Override
    public void insert(E toInsert) {
        /* 
         * toInsert 값을 value값과 비교하여 같으면 0, '>' 는 양수, '<'는 음수
         * 음수일 경우 left Node에 삽입
         * 같거나 양수일 경우 right Node에 삽입
         */
        if (toInsert.compareTo(value) < 0) { left.insert(toInsert);
        } else { right.insert(toInsert); }

    }
}
  • Leaf 클래스
public class Leaf<E extends Comparable> implements Tree<E> {

    private final Node<E> parent;

    public Leaf(Node<E> parent) {
        this.parent = parent;
    }

    @Override
    public boolean search(E toFind) {
        return false;
    }

    @Override
    public void insert(E toInsert) {
        /*
         * toInsert 값을 상위노드 값과 비교, 같으면 0, '>' 는 양수, '<'는 음수
         * 상위노드의 left 노드에 새로 Node 삽입
         */
        if (toInsert.compareTo(parent.getValue()) < 0)
            parent.setLeft(new Node<>(toInsert, new Leaf<>(parent), new Leaf<>(parent)));
        else
            parent.setRight(new Node<>(toInsert, new Leaf<>(parent), new Leaf<>(parent)));
    }
}

이진트리 검색 알고리즘

  • 단순 트리 구조

    • search를 통해 값이 있는지 확인
    • insert를 통해 상위노드와 비교하여 값이 작으면 left 노드에 삽입, 값이 같거나 클 경우 right 노드에 삽입
public class SimpleTree<E extends Comparable> {
    private E value;
    private SimpleTree<E> left;
    private SimpleTree<E> right;

    public SimpleTree(E value, SimpleTree<E> left, SimpleTree<E> right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    public E getValue() { return value; }
    public SimpleTree<E> getLeft() { return left; }
    public SimpleTree<E> getRight() { return right; }

    public boolean search(final E toFind) {
        // 찾으려는 값이 Tree에 있는 것을 확인
        if (toFind.equals(value)) return true;

        /* 
         * toFind를 value와 비교하여 같으면 0, '>' 는 양수, '<'는 음수
         * left 노드가 존재하고, 음수일 경우 left Node를 검색
         * 같거나 양수일 경우 right Node를 검색
         */
        if (toFind.compareTo(value) < 0 && left != null) return left.search(toFind);

        return right != null && right.search(toFind);
    }

    public void insert(final E toInsert) {
        // 입력할 값을 비교, 같으면 0, '>' 는 양수, '<'는 음수
        if (toInsert.compareTo(value) < 0) {
            // 입력 값이 작고, left Node가 null일 경우 새로 생성
            if (left == null) left = new SimpleTree<>(toInsert, null, null);
            // 입력 값이 작고, left Node가 null이 아닐 경우 삽입
            else left.insert(toInsert);

        } else {
            // 입력 값이 같거나 크고, right Node 값이 null인 경우 새로 생성
            if (right == null) right = new SimpleTree<>(toInsert, null, null);
            // 입력 값이 같거나 크고, right Node 값이 null이 아닌 경우 새로 생성
            else right.insert(toInsert);

        }
    }
}
  • 단순트리 검색 테스트
    • 단순 트리 구조의 insert, search 테스트
public class SimpleTreeTest {

    private SimpleTree<Integer> root;

    @Before
    public void createSampleTree() {
        final SimpleTree<Integer> t1 = new SimpleTree<>(1, null, null);
        final SimpleTree<Integer> t5 = new SimpleTree<>(5, null, null);

        final SimpleTree<Integer> t3 = new SimpleTree<>(5, t1, t5);
        final SimpleTree<Integer> t9 = new SimpleTree<>(9, null, null);
        /*     7
         *   5   9
         * 1 5
         */
        root = new SimpleTree<>(7, t3, t9);

    }

    @Test
    public void search() {
        assertTrue(root.search(7));
        assertFalse(root.search(70));
    }

    @Test
    public void insert() {
        root.insert(10);
        assertTrue(root.search(10));
        /*     7
         *   5   9
         * 1 5     10
         */
        assertEquals(Integer.valueOf(10), root.getRight().getRight().getValue());
    }

    @Test
    public void createTree() {
        final SimpleTree<Integer> root = new SimpleTree<>(7, null, null);
        root.insert(3);
        root.insert(9);
        root.insert(10);
        assertTrue(root.search(10));
        /*     7
         *   3   9
         *            10
         */
        assertEquals(Integer.valueOf(10), root.getRight().getRight().getValue());
    }

    @Test
    public void linkedListTree() {
        final SimpleTree<Integer> root = new SimpleTree<>(1, null, null);
        root.insert(2);
        root.insert(3);
        root.insert(4);
        root.insert(5);

        /*     1
         *       2
         *            3
         *              4
         *                5
         */
        assertEquals(Integer.valueOf(1), root.getValue());
        assertEquals(Integer.valueOf(2), root.getRight().getValue());
        assertEquals(Integer.valueOf(3), root.getRight().getRight().getValue());
        assertEquals(Integer.valueOf(4), root.getRight().getRight().getRight().getValue());
        assertEquals(Integer.valueOf(5), root.getRight().getRight().getRight().getRight().getValue());
    }
}
  • Complex 트리 검색
public class ComplexTreeTest {

    @Test
    public void insert() {
        final Node<Integer> root = new Node<>(7, null, null);
        root.setLeft(new Leaf<>(root));
        root.setRight(new Leaf<>(root));

        root.insert(3);
        assertTrue(root.search(3));
        assertFalse(root.search(13));
    }
}

코드 참고 - 자바 프로그래밍 면접 이렇게 준비한다

  • 데이터가 들어오는 위치가 가장 뒤에 있고, 데이터가 나가는 위치가 가장 앞에 있는 자료구조
  • 먼저 들어온 데이터가 가장 먼저 삭제되는 선입선출(First In First Out, FIFO) 자료구조
  • 어떠한 작업 / 데이터를 순서대로 실행/사용하기 위해서 대기시킬 때 사용
  • 큐 응용
    • Queue, PriorityQueue, Deque

코드 설계

  • Queue의 인터페이스의 추상 메서드 확인
  • LinkedList에서 구현 코드 확인
public interface Queue<E> extends Collection<E> {
  boolean add(E paramE);

  boolean offer(E paramE);

  E remove();

  E poll();

  E element(); // 해당 큐의 맨 앞에 있는(제일 먼저 저장된) 요소를 반환

  E peek(); // 해당 큐의 맨 앞에 있는(제일 먼저 저장된) 요소를 반환
}

코드 구현

  • Queue 인터페이스의 핵심 메서드를 LinkedList에서 확인
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable {
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last; 
    private static final long serialVersionUID = 876323262645176354L;
    public LinkedList() {}

    // 새로운 Node를 만들어 기존의 Queue의 마지막 node의 next에 연결, Queue의 size + 1
    public boolean add(E paramE) {
        linkLast(paramE);
        return true;
    }
    // add와 같은 기능을 함
    public boolean offer(E paramE) { 
        return add(paramE); 
    }

    // Queue의 index확인 후에 link가 연결된 부분을 null을 대입하여 link 연결을 해제
    public E remove(int paramInt) {
        checkElementIndex(paramInt);
        return unlink(node(paramInt));
    }

    // Queue의 first값을 확인 후 link를 제거한 뒤에 해당 Node를 반환
    public E poll() {
        Node<E> node = this.first;
        return (node == null) ? null : unlinkFirst(node);
    }

    // Queue의 first값을 확인하여 값을 조회
    public E element() { 
        return getFirst(); 
    }

    // Queue의 first값을 확인하여 값을 조회
    public E peek() {
        Node<E> node = this.first;
        return (node == null) ? null : node.item;
    }

    public E getFirst() {
        Node<E> node = this.first;
        if (node == null)
            throw new NoSuchElementException(); 
        return node.item;
    }

    void linkLast(E paramE) {
        Node<E> node1 = this.last;
        Node<E> node2 = new Node<>(node1, paramE, null);
        this.last = node2;
        if (node1 == null) {
            this.first = node2;
        } else {
            node1.next = node2;
        } 
        this.size++;
        this.modCount++;
    }

    E unlink(Node<E> paramNode) {
        E e = paramNode.item;
        Node<E> node1 = paramNode.next;
        Node<E> node2 = paramNode.prev;
        if (node2 == null) {
            this.first = node1;
        } else {
            node2.next = node1;
            paramNode.prev = null;
        } 
        if (node1 == null) {
            this.last = node2;
        } else {
            node1.prev = node2;
            paramNode.next = null;
        } 
        paramNode.item = null;
        this.size--;
        this.modCount++;
        return e;
    }

    private E unlinkFirst(Node<E> paramNode) {
        E e = paramNode.item;
        Node<E> node = paramNode.next;
        paramNode.item = null;
        paramNode.next = null;
        this.first = node;
        if (node == null) {
            this.last = null;
        } else {
            node.prev = null;
        } 
        this.size--;
        this.modCount++;
        return e;
    }

    private void checkElementIndex(int paramInt) {
        if (!isElementIndex(paramInt))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(paramInt)); 
    }
    ...
}

스택(Stack)

  • 스택은 리스트의 한쪽 끝으로만 자료의 삽입, 삭제 작업이 이루어지는 자료구조
  • 스택은 가장 나중에 삽입된 자료가 가장 먼저 삭제되는 후입선출(LIFO)방식으로 자료를 처리

코드 설계

  • Stack 클래스의 핵심 메서드 확인
public E push(E paramE) // 해당 스택의 제일 상단에 전달된 요소를 삽입

public synchronized E pop() // 해당 스택의 제일 상단에 있는(제일 마지막으로 저장된) 요소를 반환, 해당 요소를 스택에서 제거

public synchronized E peek() // 해당 스택의 제일 상단에 있는(제일 마지막으로 저장된) 요소를 반환

public boolean empty() // 해당 스택이 비어 있으면 true를, 비어 있지 않으면 false를 반환

// 해당 스택에서 전달된 객체가 존재하는 위치의 인덱스를 반환
// 이때 인덱스는 제일 상단에 있는(제일 마지막으로 저장된) 요소의 위치부터 0이 아닌 1부터 시작
public synchronized int search(Object paramObject) 

코드 구현

  • Stack 클래스 확인
public class Stack<E> extends Vector<E> {
    private static final long serialVersionUID = 1224463164541339165L;

    public E push(E paramE) {
        addElement(paramE);
        return paramE;
    }

    public synchronized E pop() {
        int i = size();
        E e = peek();
        removeElementAt(i - 1);
        return e;
    }

    public synchronized E peek() {
        int i = size();
        if (i == 0)
            throw new EmptyStackException(); 
        return elementAt(i - 1);
    }

    public boolean empty() { return (size() == 0); }

    public synchronized int search(Object paramObject) {
        int i = lastIndexOf(paramObject);
        return (i >= 0) ? (size() - i) : -1;
    }
}
// solution(new int[] { 6, 9, 5, 7, 4 });         
3    7    5    9    6    
// solution(new int[] { 3, 9, 9, 3, 5, 7, 2 });   
3    7    5    3    9    9    3    
// solution(new int[] { 1, 5, 3, 6, 7, 6, 5, 2 });
3    5    6    7    6    3    5    1    

계수정렬

  • 각 숫자가 몇개 있는지 갯수를 세어서 다른 한 배열에 저장한 후에 정렬을 하는 방식
  • 카운팅 정렬은 가장 큰 데이터에 따라 효율이 좌지우지된다.
  • 특정 데이터의 개수를 데이터의 값에 대응하는 위치에 저장
  • 자신의 위치에서 앞에 있던 값을 모두 더한 배열을 생성
  • 거기서 데이터가 들어가야 할 위치를 찾아내는 정렬 알고리즘이다.

코드 설계

  1. 각 요소의 범위가 1 ~ 7까지인 배열(A)을 읽기
  2. 요소의 값의 빈도수(frequency)를 저장
  3. 빈도 수를 기준으로 빈도수가 큰 순으로 정렬

코드 구현

    // int[] A = new int[] {1, 5, 4, 6, 5, 7, 1, 4, 4, 2};

    public int[] solution(int[] A) {
        int[] answer = {};

        // key : value로 저장할 수 있는 HashMap 생성
        Map<Integer, Integer> countMap = new HashMap<>();

        // 입력 값 출력
        for(int a : A) {
            countMap.put(a, countMap.getOrDefault(a, 0) + 1);
        }

        // key 값 가져와서 sorting
        List<Integer> sorted = new ArrayList<>(countMap.keySet());
        // 내림차순 정렬 (빈도수가 높은 순으로 정렬)
        Collections.sort(sorted, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return countMap.get(o2).compareTo(countMap.get(o1));
            }
        });

        answer = new int[sorted.size()];
        for(int i = 0 ; i < sorted.size() ; i++) {
            answer[i] = sorted.get(i);
        }

        return answer;
    }
  • 과정 값 및 결과 값
// key : frequency
4[3]    1[2]    5[2]    2[1]    6[1]    7[1]
// answer
4    1    5    2    6    7

분석

  • 모든 데이터의 크기 범위를 메모리 상에 표현할 수 있다면 복잡도 O(N)

힙 정렬(Heap Sort)

  • 병합 정렬, 퀵 정렬만큼 빠른 정렬 알고리즘

  • 가장 큰 원소를 찾는 데 최적화된 형태의 이진 트리

  • 힙의 가장 중요한 원칙

    • 부모 노드가 가진 원소는 항상 자식 노드가 가진 원소의 크기보다 크다.
    • 트리에서 가장 큰 원소는 항상 트리의 루트에 존재한다.
  • 힙의 모양 규칙

    • 마지막 레벨을 제외한 모든 레벨에 노드가 꽉 차 있어야 한다.
    • 마지막 레벨에 노드가 있을 때는 항상 가장 왼쪽부터 순서대로 채워져 있어야 한다.
  • 힙 트리 구조(Heap Tree Structure)를 이용하는 정렬 방식

    • 크기가 n인 완전 이진트리
    • 힙(Heap) 이란 최소값이나 최대값을 빠르게 찾아내기 위해 완전 이진트리를 기반으로 하는 자료구조
    • 최댓값이란 부모 노드의 키 값이 자식 노드의 키 값보다 항상 큰 힙
    • 최솟값이란 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작은 힙
    • 키 값의 대소 관계는 오직 부모 노드와 자식 노드 간에만 성립하며, 형제 사이에는 대소 관계가 정해지지 않는다.
  • 힙을 사용할 경우 새 원소를 추가하는 연산과 가장 큰 원소를 꺼내는 연산을 모두 O(logN) 시간에 수행할 수 있다.

코드 설계

  • 힙 구조의 규칙
    • A[i]에 대응되는 노드의 왼쪽 자손은 A[2 * i + 1]에 대응된다.
    • A[i]에 대응되는 노드의 오른쪽 자손은 A[2 * i + 2]에 대응된다.
    • A[i]에 대응되는 노드의 부모는 A[(i-1)/2]에 대응된다.(나눗셈 결과는 내림한다.)
  • 정렬 과정
    • 마지막 노드의 부모 노드부터 시작하여 root 노드순으로 검색한다.
    • 정렬의 과정은 최댓값을 검증하여 정상적인 힙 구조인지 확인, 아닐 경우 child와 parent를 변경

코드 구현

    private void swap(int[] A, int pre, int post) {
        System.out.println("swap: \tA[" + pre + "]=" + A[pre] + " <> A[" + post + "]=" + A[post]);
        int tmp = A[pre];
        A[pre] = A[post];
        A[post] = tmp;
    }
    /**
     * 힙 정렬
     * @param A
     */
    private void heapSort(int[] A) {
        int aLength = A.length - 1; // 배열의 실 길이
        int parentIdx = aLength / 2;

        // 마지막 노드의 부모 노드부터 시작하여 root 노드 방향으로 거슬러 올라감
        for (int i = parentIdx; i >= 0; i--) maxHeapify(A, A.length, i);

        for(int i = aLength ; i > 0 ; i--) { // 원소가 하나 남을 때까지 반복
            swap(A, 0, i); // 배열의 마지막 요소 값을 루트로 변경
            maxHeapify(A, i, 0);
        }
    }

    /**
     * 배열 A를 힙으로 만드는 메서드
     * 루트에 있는 가장 큰 값을 빼내어 마지막 요소와 바꾸고,
     * 배열의 나머지 부분을 다시 힙으로 만드는 과정을 반복하여 정렬을 수행

     * @param A
     * @param length
     * @param i 
     */
    private void maxHeapify(int[] A, int length, int i) {
        int parent = i; // parent index
        int leftChild = (i * 2) + 1; // leftChild index
        int rightChild = (i * 2) + 2; // rightChild index


        // leftChild가 parent보다 클 경우
        if(leftChild < length && A[parent] < A[leftChild]) {
            parent = leftChild;
        }

        // rightChild가 parent보다 클 경우
        if(rightChild < length && A[parent] < A[rightChild]) {
            parent = rightChild;
        }

        if(i != parent) {
            swap(A, parent, i);
            maxHeapify(A, length, parent-1);
        }
    }
  • 과정
             01
            /\
     04              05
    /\            /\
 07      02      06      03
/            
08

A[7]=8 < swap > A[3]=7
             01
            /\
     04              05
    /\            /\
 08      02      06      03
/            
07

A[5]=6 < swap > A[2]=5
             01
            /\
     04              06
    /\            /\
 08      02      05      03
/            
07

A[3]=8 < swap > A[1]=4
             01
            /\
     08              06
    /\            /\
 04      02      05      03
/            
07

A[1]=8 < swap > A[0]=1
             08
            /\
     01              06
    /\            /\
 04      02      05      03
/            
07

A[0]=8 < swap > A[7]=7
             07
            /\
     01              06
    /\            /\
 04      02      05      03
/            
08

A[0]=7 < swap > A[6]=3
             03
            /\
     01              06
    /\            /\
 04      02      05      07
/            
08

A[2]=6 < swap > A[0]=3
             06
            /\
     01              03
    /\            /\
 04      02      05      07
/            
08

A[3]=4 < swap > A[1]=1
             06
            /\
     04              03
    /\            /\
 01      02      05      07
/            
08

A[5]=5 < swap > A[2]=3
             06
            /\
     04              05
    /\            /\
 01      02      03      07
/            
08

A[0]=6 < swap > A[5]=3
             03
            /\
     04              05
    /\            /\
 01      02      06      07
/            
08

A[2]=5 < swap > A[0]=3
             05
            /\
     04              03
    /\            /\
 01      02      06      07
/            
08

A[0]=5 < swap > A[4]=2
             02
            /\
     04              03
    /\            /\
 01      05      06      07
/            
08

A[1]=4 < swap > A[0]=2
             04
            /\
     02              03
    /\            /\
 01      05      06      07
/            
08

A[0]=4 < swap > A[3]=1
             01
            /\
     02              03
    /\            /\
 04      05      06      07
/            
08

A[2]=3 < swap > A[0]=1
             03
            /\
     02              01
    /\            /\
 04      05      06      07
/            
08

A[0]=3 < swap > A[2]=1
             01
            /\
     02              03
    /\            /\
 04      05      06      07
/            
08

A[1]=2 < swap > A[0]=1
             02
            /\
     01              03
    /\            /\
 04      05      06      07
/            
08

A[0]=2 < swap > A[1]=1
             01
            /\
     02              03
    /\            /\
 04      05      06      07
/            
08

분석

  • 힙 정렬은 선택 정렬을 응용한 알고리즘
  • 첫 요소를 꺼내는 것만으로 가장 큰 값이 구해지므로 첫 요소를 꺼낸 다음 나머지 요소를 다시 힙으로 만들어야 그 다음에 꺼낼 요소도 가장 큰 값을 유지한다.
  • 따라서 단순 선택 정렬에서 가장 큰 요소를 선택할 때의 시간 복잡도 O(n)의 값을 한 번에 선택할 수 있어 O(1)로 줄일 수 있다.
  • 복잡도 분석
    • 힙으로 만드는 작업의 시간 복잡도 O(logn)
    • 힙으로 만드는 작업을 요소의 개수 만큼 반복하므로 O(n logn)

+ Recent posts