트리 (Tree)


트리 (Tree)

  • 비선형 자료구조
  • 계층적 관계를 표현하는 자료구조
    (일상생활에서 사용하는 회사의 조직도 등)
  • 여러 노드가 하나의 노드를 가리킬 수 없는 구조 (아래 그림은 트리가 아님)

용어 정리

  • 노드 (node)
     - 트리의 구성요소로, 위 그림에서 A, B, C, D, E, F가 이에 해당됨
  • 간선 (edge)
     - 노드와 노드를 연결하는 선
  • 루트 노드 (root node)
     - 위 그림의 트리구조에서 최상위에 존재하는 A를 의미
  • 단말노드 (terminal node) 또는 잎사귀 노드 (leaf node)
     - 다른 노드가 연결되어있지 않은 노드로서 위 그림에서 E, F, C, D가 이에 해당됨
  • 내부노드 (internal node) 또는 비단말 노드 (nonterminal node)
     - 단말노드를 제외한 모든 노드로서, A, B가 이에 해당됨
  • 레벨
     - 각 층에 숫자를 매기는 것
  • 높이
     - 트리의 최고레벨을 가리킴

노드와의 관계

  • 노드 A는 B, C, D의 부모 노드이다. (parent)
  • B, C, D는 노드 A의 자식 노드이다. (child)
  • B, C, D는 부모가 같으므로 형제 노드이다. (sibling)

 

 

이진트리 (Binary Tree)와 서브트리 (Sub Tree)


서브트리 (Sub Tree)

  • 큰 트리에 속하는 작은 트리
  • 서브트리에 서브트리도 가능

 

이진트리 (Binary Tree)

  • 루트노드로부터 두 개의 서브트리가 존재해야 함
  • 노드가 있어야 할 곳에 노드가 존재하지 않으면 공집합 노드가 존재하는 것으로 간주함
    (노드가 하나여도 두 개의 공노드가 있다고 간주하기 때문에 노드가 하나여도 이진트리가 됨)

포화 이진트리 (Full Binary Tree)와 완전 이진트리 (Complete Binary Tree)

  • 포화 이진트리
     - 모든 레벨에 노드가 가득 찬 상태이며 노드를 더 추가하기 위해서는 레벨을 늘려야 함
  • 완전 이진트리
     - 노드가 트리의 왼쪽부터 순서대로 채워진 것
  • 아래 사진처럼 왼쪽부터 채워지지 않은 경우 이진트리이지만, 완전 이진트리가 아님

연결리스트 기반 이진트리

  • 추상자료형 (ADT)
    자료형 반환값 내용
    createBTreeNode() BTreeNode 이진트리 노드 생성
    getData(BTreeNode) Data 이진트리 노드의 데이터 반환
    setData(BTreeNode, Data) void 노드에 데이터 저장
    getLeftSubTree(BTreeNode) BTreeNode 왼쪽 서브트리를 반환
    getRightSubTree(BTreeNode) BTreeNode 오른쪽 서브트리를 반환
    createLeftSubTree(BTreeNode, BTreeNode) void 왼쪽에 서브트리를 연결
    createRightSubStree(BTreeNode, BTreeNode) void 오른쪽에 서브트리를 연결


연결리스트 기반 이진트리 구현

  • 디렉토리 구조
  • BTreeNode.java
    package BinaryTree.LinkedList;
    
    public class BTreeNode {
        Object data;
        BTreeNode leftNode;
        BTreeNode rightNode;
    }​
  • BinaryTree.java
    package BinaryTree.LinkedList;
    
    public class BinaryTree {
        BTreeNode createBTreeNode() {
            BTreeNode newNode = new BTreeNode();
            newNode.leftNode = null;
            newNode.rightNode = null;
    
            return newNode;
        }
    
        Object getData(BTreeNode node) {
            return node.data;
        }
    
        void setData(BTreeNode node, Object data) {
            node.data = data;
        }
    
        BTreeNode getLeftSubTree(BTreeNode node) {
            return node.leftNode;
        }
    
        BTreeNode getRightSubTree(BTreeNode node) {
            return node.rightNode;
        }
    
        void createLeftSubTree(BTreeNode parent, BTreeNode child) {
            if (parent.leftNode != null){
                parent.leftNode = null;
            }
    
            parent.leftNode = child;
        }
    
        void createRightSubTree(BTreeNode parent, BTreeNode child) {
            if (parent.rightNode != null) {
                parent.rightNode = null;
            }
    
            parent.rightNode = child;
        }
    }​


  • BinaryTreeMain.java
    package BinaryTree.LinkedList;
    
    public class BinaryTreeMain {
        public static void main(String[] args) {
            BinaryTree binaryTree = new BinaryTree();
    
            BTreeNode node1 = binaryTree.createBTreeNode();
            BTreeNode node2 = binaryTree.createBTreeNode();
            BTreeNode node3 = binaryTree.createBTreeNode();
            BTreeNode node4 = binaryTree.createBTreeNode();
            BTreeNode node5 = binaryTree.createBTreeNode();
    
            binaryTree.setData(node1, 'A');
            binaryTree.setData(node2, 'B');
            binaryTree.setData(node3, 'C');
            binaryTree.setData(node4, 'D');
            binaryTree.setData(node5, 'E');
    
            binaryTree.createLeftSubTree(node1, node2); // B를 A의 왼쪽 서브트리로 등록
            binaryTree.createRightSubTree(node1, node3); // C를 A의 오른쪽 서브트리로 등록
            binaryTree.createLeftSubTree(node2, node4); // D를 B의 왼쪽 서브트리로 등록
            binaryTree.createRightSubTree(node2, node5); // E를 B의 오른쪽 서브트리로 등록
    
            BTreeNode tmpNode = null;
            System.out.println(binaryTree.getData(node1)); // 루트 노드 데이터 반환
    
            tmpNode = binaryTree.getLeftSubTree(node1);
            System.out.println(binaryTree.getData(tmpNode)); // A의 왼쪽 서브트리 데이터 반환
    
            tmpNode = binaryTree.getRightSubTree(node1);
            System.out.println(binaryTree.getData(tmpNode)); // A의 오른쪽 서브트리 데이터 반환
    
            tmpNode = binaryTree.getLeftSubTree(node2);
            System.out.println(binaryTree.getData(tmpNode)); // B의 왼쪽 서브트리 데이터 반환
    
            tmpNode = binaryTree.getRightSubTree(node2);
            System.out.println(binaryTree.getData(tmpNode)); // B의 오른쪽 서브트리 데이터 반환
        }
    }​


  • 결과

이진트리의 순회

  • 전위순회 (Preorder Traversal) : 루트 노드를 먼저 순회
  • 중위순회 (Inorder Traversal) : 루트 노드를 중간에 순회
  • 후위순회 (Postorder Traversal) : 루트 노드를 마지막에 순회


  • 중위순회 방법
     - 재귀적으로 순회 가능
    1) 왼쪽 서브트리 순회
    2) 루트 노드 방문
    3) 오른쪽 서브트리 순회

  • 중위순회 구현
    // 중위 순회
    void InorderTraverse(BTreeNode node) {
        if (node == null){
            return;
        }
    
        InorderTraverse(node.leftNode);
        System.out.println(node.data);
        InorderTraverse(node.rightNode);
    }​
  • 전위순회 및 후위순회
     - 중위순회의 방법 1, 2, 3의 순서를 변경
     - 중위순회
    // 전위 순회
    void PreorderTraverse(BTreeNode node) {
        if (node == null) {
            return;
        }
    
        System.out.println(node.data);
        PreorderTraverse(node.leftNode);
        PreorderTraverse(node.rightNode);
    }
     - 후위순회
    // 후위 순회
    void PostorderTraverse(BTreeNode node) {
        if (node == null) {
            return;
        }
    
        PostorderTraverse(node.leftNode);
        PostorderTraverse(node.rightNode);
        System.out.println(node.data);
    }​
  • 순회결과

 

이진트리를 이용한 수식 표현 (수식트리)


이진트리를 이용한 수식 표현 (수식트리)

  • 부모노드의 자식노드 두 개를 피연산자로 한 후 연산
    ex) 7+4*2-1

이진트리를 이용한 수식 구현

  1. 중위표기법을 후위표기법으로 변환
    중위 표기법을 바로 트리로 구현하기 어려움 (괄호 또는 연산자 우선순위 때문에)

  2. 후위표기법을 수식트리로 변환
    스택을 이용하여 구현
    1) 후위표기법의 앞에서부터 스캔
    2) 스캔한 문자가 숫자인 경우 스택에 저장
    3) 스캔한 문자가 연산자인 경우 스택에서 두 개를 꺼내 연결한 후 다시 스택에 저장
    public BTreeNode createExpTree(String exp){
        String postfixExp = PostfixNotation.convPostfix(exp);
        Stack<BTreeNode> stack = new Stack<>();
    
        char c = ' ';
        for(int i=0; i<postfixExp.length(); i++){
            node = createBTreeNode();
            c = postfixExp.charAt(i);
    
            // 숫자면
            if (Character.isDigit(c)){
                setData(node, Character.getNumericValue(c));
            }
            // 숫자가 아니면 (연산자면) 연결
            else {
                setData(node, c);
                createRightSubTree(node, stack.pop());
                createLeftSubTree(node, stack.pop());
            }
            stack.push(node);
        }
    
        return stack.pop();
    }​

    ex) 중위표기법 1+2+3
    후위표기법 스택 결과
    12+3+    
    +3+ 2  1  
    3+  
    3+
     
    + 3        
     
       
     
     
  3. 수식트리 계산
     - 재귀로 구현
    1) 단말노드까지 왼쪽으로 이동 (노드의 좌우가 null일 경우) 후 값 반환
    2) 단말노드까지 오른쪽으로 이동 (노드의 좌우가 null일 경우) 후 값 반환
    3) 부모노드 값(연산자) 반환
    4) 연산자에 맞게 계산 진행
    5) 1) ~ 4) 항목 반복
    public double calculate(BTreeNode node){
        double op1 = 0;
        double op2 = 0;
    
        // 단말노드인지 확인
        if (getLeftSubTree(node) == null && getRightSubTree(node) == null){
            // 단말노드일 경우 해당 노드의 값을 가져와 Double 형식으로 변환
            return Double.valueOf(getData(node).toString());
        }
    
        // 노드의 왼쪽 피연산자 값 반환
        op1 = calculate(getLeftSubTree(node));
        // 노드의 오른쪽 피연산자 값 반환
        op2 = calculate(getRightSubTree(node));
    
        // 연산자 확인 후 계산
        switch (String.valueOf(getData(node))){
            case "+":
                return op1+op2;
    
            case "-":
                return op1-op2;
    
            case "*":
                return op1*op2;
    
            case "/":
                return op1/op2;
        }
    
        return 0;
    }

구현

  • 후위표기법으로 변환을 위한 클래스 생성
    package Stack;
    
    import Stack.Stack;
    import Stack.LinkedList.LinkedListStack;
    
    public class PostfixNotation {
        public static String convPostfix(String infix){
            char c = ' ';
            Stack opStack = new LinkedListStack();
            StringBuilder sb = new StringBuilder();
    
            for(int i=0; i<infix.length(); i++){
                c = infix.charAt(i);
    
                // 숫자이면 표현
                if (Character.isDigit(c)){
                    sb.append(c);
                }
                // 연산자 스택이 비어있을 경우 값 push
                else if (opStack.isEmpty()){
                    opStack.push(c);
                }
                // 숫자가 아니고 연산자 스택이 비어있지 않은 경우 (연산자가 하나라도 스택에 추가된 경우)
                else {
                    // 여는 괄호가 나오면 스택에 저장 후 다음 문자로
                    if (c == '('){
                        opStack.push(c);
                        continue;
                    }
                    // 닫는 괄호가 나올 경우
                    // 스택에 저장된 모든 연산자를 반환
                    else if (c == ')'){
                        char check = ' ';
                        while(true) {
                            check = (char) opStack.pop();
                            if (check == '(') {
                                break;
                            }
                            else {
                                sb.append(check);
                            }
                        }
                        continue;
                    }
    
                    // 현재 연산자의 우선순위가 더 높은 경우
                    // 스택에 연산자 저장
                    if (compareOp((char)opStack.peek(), c) > 0){
                        opStack.push(c);
                    }
                    // 현재 연산자의 우선순위가 더 낮거나 같은 경우
                    // 스택에 있는 우선순위가 높은 연산자를 빼서 표현
                    else {
                        while(!opStack.isEmpty()){
                            if (compareOp((char)opStack.peek(), c) <= 0){
                                sb.append(opStack.pop());
                            }
                            else {
                                break;
                            }
                        }
                        opStack.push(c);
                    }
                }
            }
    
            char check = ' ';
            while(!opStack.isEmpty()) {
                check = (char) opStack.pop();
                if (check != '(') {
                    sb.append(check);
                }
            }
    
            return sb.toString();
        }
    
        // 연산자 우선순위 반환
        public static int getOpPriority(char c){
            switch (c) {
                case '*':
                case '/':
                    return 3;
    
                case '+':
                case '-':
                    return 2;
    
                case '(':
                    return 1;
    
                default:
                    return -1;
            }
        }
    
        // 연산자 우선순위 비교
        public static int compareOp(char stackOp, char curOp){
            int stackOpPriority = getOpPriority(stackOp);
            int curOpPriority = getOpPriority(curOp);
    
            // 현재 우선순위가 더 높은 경우
            if (stackOpPriority < curOpPriority){
                return 1;
            }
            // 우선순위가 같은 경우
            else if (stackOpPriority == curOpPriority){
                return 0;
            }
            // 스택의 우선순위가 더 높은 경우
            else {
                return -1;
            }
        }
    }​
  • 수식트리 구현을 위한 클래스 (BinaryTree 상속)
    package BinaryTree.LinkedList;
    
    import java.util.Stack;
    import Stack.PostfixNotation;
    
    public class ExpressionTree extends BinaryTree {
        BTreeNode node;
    
        public BTreeNode createExpTree(String exp){
            String postfixExp = PostfixNotation.convPostfix(exp);
            Stack<BTreeNode> stack = new Stack<>();
    
            char c = ' ';
            for(int i=0; i<postfixExp.length(); i++){
                node = createBTreeNode();
                c = postfixExp.charAt(i);
    
                // 숫자면
                if (Character.isDigit(c)){
                    setData(node, Character.getNumericValue(c));
                }
                // 숫자가 아니면 (연산자면) 연결
                else {
                    setData(node, c);
                    createRightSubTree(node, stack.pop());
                    createLeftSubTree(node, stack.pop());
                }
                stack.push(node);
            }
    
            return stack.pop();
        }
    
        public double calculate(BTreeNode node){
            double op1 = 0;
            double op2 = 0;
    
            // 단말노드인지 확인
            if (getLeftSubTree(node) == null && getRightSubTree(node) == null){
                // 단말노드일 경우 해당 노드의 값을 가져와 Double 형식으로 변환
                return Double.valueOf(getData(node).toString());
            }
    
            // 노드의 왼쪽 피연산자 값 반환
            op1 = calculate(getLeftSubTree(node));
            // 노드의 오른쪽 피연산자 값 반환
            op2 = calculate(getRightSubTree(node));
    
            // 연산자 확인 후 계산
            switch (String.valueOf(getData(node))){
                case "+":
                    return op1+op2;
    
                case "-":
                    return op1-op2;
    
                case "*":
                    return op1*op2;
    
                case "/":
                    return op1/op2;
            }
    
            return 0;
        }
    }​


  • 수식트리 테스트 구현
    package BinaryTree.LinkedList;
    
    public class ExpressionTreeMain {
        public static void main(String[] args) {
            ExpressionTree expTree = new ExpressionTree();
    
            BTreeNode expNode = expTree.createExpTree("(1+2)*3");
            System.out.println(expTree.calculate(expNode));
        }
    }​


  • 결과

 

[참고] 열혈 자료구조

[전체코드] 

+ Recent posts