자료구조 with JAVA

[JAVA] 자료구조 - 트리 (Tree)

beginner-in-coding 2024. 12. 31. 17:59

(1) 트리(Tree)

  • 노드(Node)와 링크(Link)로 구성된 자료구조(Cycle X)
  • 계층적 구조를 나타낼 때 사용
  • ex) 폴더 구조(디렉토리, 서브 디렉토리), 조직도, 가계도 ...

(2) 트리 구조

  • 노드(Node): 트리 구조의 자료값을 담고 있는 단위
    • 루트 노드(Root): 부모 없는 노드, 가장 위의 노드
    • 잎새 노드(Leaf): 자식이 없는 노드(=단말)
    • 내부 노드(Internal): 잎새 노드를 제외한 모든 노드
  • 에지(Edge): 노드 간의 연결선(=Link, Branch)

  • 부모(Parent): 연결된 두 노드 중, 상위 노드
  • 자식(Child): 연결된 두 노드 중, 하위 노드
  • 형제(Sibling): 같은 부모를 가지는 노드

  • 깊이(Depth): 루트에서 어떤 노드까지의 간선의 수
  • 레벨(Level): 트리의 특정 깊이를 가지는 노드 집합
  • 높이(Height): 트리에서 가장 큰 레벨 값
  • 크기(Size): 자신을 포함한 자식 노드 개수

  • 차수(Degree): 각 노드가 가진 가지의 수
  • 트리의 차수: 트리의 최대 차수

트리 구조


(3) 트리 특징

  1. 하나의 노드에서 다른 노드로 이동하는 경로 유일
  2. 노드가 N개인 트리의 Edge 수: N-1개
  3. Acyclic (Cycle X)
  4. 모든 노드가 서로 연결
  5. 하나의 Edge를 끊으면 2개 Sub-Tree로 분리

(4) 이진 트리

  • 각 노드는 최대 2개의 자식을 가질 수 있음
  • 자식 노드는 좌우를 구분
    • 왼쪽 자식: 부모(기준)의 왼쪽
    • 오른쪽 자식: 부모(기준)의 오른쪽

(5) 이진 트리 종류

  1. 포화 이진 트리(Perfect Binary Tree): 모든 레벨에서 노드들이 꽉 차있는 트리
  2. 완전 이진 트리(Complete Binary Tree): 마지막 레벨을 제외하고 노드들이 꽉 채워져 있는 트리
  3. 정 이진 트리(Full Binary Tree): 모든 노드가 0개 또는 2개의 자식 노드를 갖는 트리
  4. 편향 트리(Skeqed Binary Tree): 한 쪽으로 기울어진 트리
  5. 균형 이진 트리(Balanced Binary Tree): 모든 노드의 좌우 서브 트리 높이가 1이상 차이나지 않는 트리(탐색속도 빠름)

트리 종류

(6) 이진 트리 특징

  • 포화 이진 트리의 높이가 h일 때, 노드의 수: 2^(h+1) - 1
  • 포화(or 완전) 이진 트리의 노드가 N개일 때, 높이: log N
  • 이진 트리의 노드가 N개일 때, 최대 가능 높이: N-1

(7) 이진 트리 순회(Traversal)

  • 모든 노드를 빠뜨리거나 중복하지 않고 방문하는 연산
  • 순회 종류 4가지: (1) 전위 순회, (2) 중위 순회, (3) 후위 순회, (4) 레벨 순회

(1) DFS - 전위 순회(Preorder Traversal)

  • 방문 순서: 현재 노드 → 왼쪽 노드 → 오른쪽 노드

DFS - 전위 순회


(2) DFS - 중위 순회(Inorder Traversal)

  • 방문 순서: 왼쪽 노드 → 현재 노드 → 오른쪽 노드

DFS - 중위 순회


(3) DFS - 후위 순회(Postorder Traversal)

  • 방문 순서: 왼쪽 노드 → 오른쪽 노드 → 현재 노드

DFS - 후위 순회


(4) BFS - 레벨 순회(Levelorder Traversal)

  • 방문 순서: 위쪽 레벨부터 왼쪽 노드 → 오른쪽 노드

BFS - 레벨 순회


(8) 이진 트리 구현

  • 배열로 구현하는 방법: 레벨 순회 순서로 배열에 구현
    • 왼쪽 노드의 인덱스 번호: (부모 인덱스)*2 + 1
    • 오른쪽 노드의 인덱스 번호: (부모 인덱스)*2 + 2
      •  Root 노드의 인덱스 번호==0 이므로
      • 루트 노드의 왼쪽 자식 노드의 인덱스 번호: 0*2 + 1 = 1 인덱스 자리
      • 루트 노드의 오른쪽 자식 노드의 인덱스 번호: 0*2 + 2 = 2 인덱스 자리

배열로 구현한 이진 트리

  • 그림과 다른 이유: 배열은 0부터 시작하기 때문에 구현할 때는 (왼:+0, 오:+1)이 아니라 (왼:+1, 오:+2)이 되야 함
  • 연결리스트: 값과 간선을 관리하기 위한 노드로 연결 리스트 구현

(9) JAVA로 구현해보기


(1) Array를 이용한 이진 트리 구현, 순회 4가지

//Array를 이용한 이진 트리 구현, 순회
class BinaryTree{
    char[] arr;  //저장할 배열 생성

    BinaryTree(char[] data){
        this.arr = data.clone();  //입력받은 배열을 복사
    }

    //DFS-전위 순위: 현재 노드 -> 왼쪽 노드 -> 오른쪽 노드
    public void preOrder(int idx){
        //먼저 현재 노드 실행
        System.out.print(this.arr[idx]+ " ");
        int left = 2*idx+1;  //현재 노드의 왼쪽 노드
        int right = 2*idx+2;  //현재 노드의 오른쪽 노드

        //왼쪽 노드부터 실행
        if(left < this.arr.length){  //왼쪽 자식 노드 확인
            this.preOrder(left);  //재귀함수
        }
        //왼쪽 노드가 없으면 오른쪽 노드 실행
        if(right < this.arr.length){  //오른쪽 자식 노드 확인
            this.preOrder(right);
        }
    }

    //DFS-중위 순회: 왼쪽 노드 -> 현재 노드 -> 오른쪽 노드
    public void inOrder(int idx){
        int left = 2*idx+1;  //현재 노드의 왼쪽 노드
        int right = 2*idx+2;  //현재 노드의 오른쪽 노드

        //왼쪽 노드부터 실행
        if(left < this.arr.length){  //왼쪽 자식 노드 확인
            this.inOrder(left);
        }

        //왼쪽 노드가 없으면 현재 노드 실행
        System.out.print(this.arr[idx] + " ");

        //현재 노드 다음에 오른쪽 노드 실행
        if(right < this.arr.length){  //오른쪽 자식 노드 확인
            this.inOrder(right);
        }
    }

    //DFS-후위 순회: 왼쪽 노드 -> 오른쪽 노드 -> 현재 노드
    public void postOrder(int idx){
        int left = 2*idx+1;  //현재 노드의 왼쪽 노드
        int right = 2*idx+2;  //현재 노드의 오른쪽 노드

        // 왼쪽 노드부터 실행
        if(left < this.arr.length){
            this.postOrder(left);
        }

        //오른쪽 노드 실행
        if(right < this.arr.length){
            this.postOrder(right);
        }
        
        //현재 노드 실행
        System.out.print(this.arr[idx] + " ");
    }

    //BFS-레벨 순회: (위쪽 레벨부터) 왼쪽 노드 -> 오른쪽 노드
    public void levelOrder(int idx){
        // 배열 입력 그대로 출력하면 됨(이미 위쪽 레벨부터 왼,오 순서로 정렬되어 있음)
        for (int i = 0; i < this.arr.length; i++) {
            System.out.print(this.arr[i]+" ");
        }
        System.out.println();
    }
}
  • 재귀 함수를 이용해 찾고자 하는 노드를 입력 받는 것을 반복하면서 출력을 진행함

(2) 연결리스트를 이용한 이진 트리 구성, 순회

//연결 리스트를 이용한 이진 트리 구성, 순회
class Node{
    char data;  //저장할 데이터 변수 생성
    Node left;  //현재 노드의 왼쪽 노드 정보 변수 생성
    Node right;  //현재 노드의 오른쪽 노드 정보 변수 생성

    //TIP: alt+insert: 생성자 바로만들기
    public Node(char data, Node left, Node right) {
        this.data = data;
        this.left = left;
        this.right = right;
    }
}

class BinaryTree2{
    Node head;  //트리의 root 노드 정보 저장하는 변수 생성

    BinaryTree2(){}
    BinaryTree2(char[] arr){  //배열로 트리를 입력받음
        //노드 형태의 배열로 만듬
        Node[] nodes = new Node[arr.length];
        for (int i = 0; i < arr.length; i++) {  //입력 받은 배열을 노드 객체들로 만듦
            nodes[i] = new Node(arr[i], null, null);
        }

        //자식 연결
        for (int i = 0; i < arr.length; i++) {
            int left = 2*i+1;  //현재의 왼쪽 노드
            int right = 2*i+2;  //현재의 오른족 노드

            if(left < arr.length){
                nodes[i].left = nodes[left];  //현재 기준 노드의 왼쪽 노드 정보 입력에 노드 정보 추가
            }
            if(right < arr.length){
                nodes[i].right = nodes[right];  //현재 기준 노드의 오른쪽 노드 정보 입력에 노드 정보 추가
            }
        }

        //root 연결
        this.head = nodes[0];  //배열의 첫 번째 인덱는 root
    }

    //DFS-전위 순위: 현재 노드 -> 왼쪽 노드 -> 오른쪽 노드
    public void preOrder(Node node){
        if(node == null){  //왼쪽 노드나 오른쪽 노드가 없는 경우 반환을 통해 다음 라인으로 진행함
            return;
        }
        //현재 노드부터 시작
        System.out.print(node.data+" ");
        //현재 노드의 왼쪽으로 이동: 왼쪽 노드가 없는 경우 return을 통해 다음 라인(오른쪽)으로 이동함
        this.preOrder(node.left);
        //현재 노드의 오른쪽으로 이동
        this.preOrder(node.right);
    }

    //DFS-중위 순회: 왼쪽 노드 -> 현재 노드 -> 오른쪽 노드
    public void inOrder(Node node){
        if(node == null){  //왼쪽 노드나 오흔쪽 노드가 없는 경우 반환을 통해 다음 라인을 진행함
            return;
        }

        //왼쪽 노드부터 시작
        this.inOrder(node.left);
        //더이상 왼쪽 노드가 없는 경우 현재 노드를 출력
        System.out.print(node.data + " ");
        //오른쪽 노드로 이동
        this.inOrder(node.right);
    }

    //DFS-후위 순회: 왼쪽 노드 -> 오른쪽 노드 -> 현재 노드
    public void postOrder(Node node){
        if(node == null){
            return;
        }
        
        //왼쪽 부터 시작
        this.postOrder(node.left);
        //오른쪽으로 이동
        this.postOrder(node.right);
        //현재 노드 출력
        System.out.print(node.data+" ");
    }


    //BFS-레벨 순회: (위쪽 레벨부터) 왼쪽 노드 -> 오른쪽 노드
    public void levelOrder(Node node){
        Queue<Node> queue = new LinkedList<>();  //큐는 선입선출
        queue.add(node);  

        while(!queue.isEmpty()){  //큐가 비어있지 않을때 동안 반복
            Node cur = queue.poll();  //큐에서 현재 노드를 뽑음

            //현재 노드 출력
            System.out.print(cur.data+" ");
            if(cur.left != null){  //현재 노드의 왼쪽이 있다면
                queue.offer(cur.left);  //왼쪽 출력, 큐에 추가
            }
            if(cur.right != null){  //현재 노드의 오른쪽이 있다면
                queue.offer(cur.right);  //오른쪽 출력, 큐에 추가
            }
        }
    }
}

(3) 트리 구조 복습 / 구현

//트리 구조 복습/구현
class Node2{
    char data;  //노드에 저장할 데이터 변수 생성
    Node2 left;  //노드의 왼쪽 노드 정보를 저장할 변수 생성
    Node2 right;  //노드의 오른쪽 노드 정보를 저장할 변수 생성
    Node2 parent;  //노드의 부모 노드 정보를 저장할 변수 생성

    public Node2(char data, Node2 left, Node2 right, Node2 parent) {
        this.data = data;
        this.left = left;
        this.right = right;
        this.parent = parent;
    }
}

class BinaryTree3{
    Node2 head;  //트리의 Root 노드 정보를 저장할 변수 생성
    BinaryTree3(char[] arr){
        Node2[] nodes = new Node2[arr.length];  //입력의 길이만큼 노드 배열 생성

        for (int i = 0; i < arr.length; i++) {  //입력받은 배열 정보를 노드 객체들로 생성
            nodes[i] = new Node2(arr[i], null, null, null);
        }

        //초기 설정 - 노드들을 연결해주기
        for (int i = 0; i < arr.length; i++) {
            int left = 2 * i + 1;  //왼쪽 노드 인덱스
            int right = 2 * i + 2;  //오른쪽 노드 인덱스

            if (left < arr.length) {
                nodes[i].left = nodes[left];
                nodes[left].parent = nodes[i];
            }
            if (right < arr.length) {
                nodes[i].right = nodes[right];
                nodes[right].parent = nodes[i];
            }
        }
        this.head = nodes[0];  //root 노드 저장
    }

    //찾고자하는 data가 트리에 존재하는지, 존재하면 노드를 반환
     public Node2 searchNode(char data){
         Queue<Node2> queue = new LinkedList<>();
         queue.add(this.head);  //헤드부터 찾음
         while(!queue.isEmpty()){  //비어있지 않을 경우에만 반복
             Node2 cur = queue.poll();  //현재 큐의 노드를 하나 뽑음
             if(cur.data == data){  //뽑은 데이터가 찾는 데이터라면
                 return cur;
             }
             if(cur.left != null){  //뽑은 데이터의 왼쪽이 비어져 있지 않으면
                 queue.offer(cur.left);  //왼쪽 추가
             }
             if (cur.right != null) {  //뽑은 데이터의 오른쪽이 비어져있지 않으면
                 queue.offer(cur.right);  //오른쪽 추가
             }
         }
         return null;  //데이터가 존재하지 않으면 null반환
     }

     //크기 구하기: 트리의 사이즈를 data를 기준으로 자신을 포함한 자식 노드의 개수를 알아보는 함수
     public Integer checkSize(char data){
         Node2 node = this.searchNode(data);

         if(node == null){
             return null;
         }

         Queue<Node2> queue = new LinkedList<>();
         queue.add(node);
         int size = 0;
         while(!queue.isEmpty()){
             Node2 cur = queue.poll();

             if(cur.left != null){
                 queue.offer(cur.left);
                 size++;  //존재할 경우 사이즈 증가
             }
             if(cur.right != null){
                 queue.offer(cur.right);
                 size++;  //존재할 경우 사이즈 증가
             }
         }
         return size+1; //자기 자신 포함
     }
}