github.com/donghyeon0725/algorithm_java

 

donghyeon0725/algorithm_java

Contribute to donghyeon0725/algorithm_java development by creating an account on GitHub.

github.com

제 깃허브입니다.

 

 

 

설명링크

github.com/donghyeon0725/algorithm_java/blob/master/src/com/dataType/tree/README.md

 

donghyeon0725/algorithm_java

Contribute to donghyeon0725/algorithm_java development by creating an account on GitHub.

github.com

사진자료와 함께 설명하고 있기 때문에 위 링크로 들어가셔서 들을 읽고 오시는 것을 강력 추천합니다.

 

 

 

트리란

 

  • 트리: Node와 Branch를 이용해서, 사이클을 이루지 않도록 구성한 데이터 구조 (Child 간의 연결이 없음)
  • 실제로 사용되는 곳 : 트리의 한 종류로, 이진 트리 (Binary Tree) 형태의 구조로, 탐색(검색) 알고리즘 구현을 위해 많이 사용됨

 

 

알아두어야할 용어
용어 설명
Node 트리에서 데이터를 저장하는 기본 요소(데이터와 다른 연결된 노드에 대한 Branch 정보를 포함)
Root Node 트리 맨 위에 있는 노드
Level 최상위 노드를 Level 0 으로 하였을 때, 하위 Branch로 연결된 노드의 깊이를 나타냄
Parent Node 어떤 노드의 이전 레벨에 연결된 노드
Chlid Node 어떤 노드의 상위 레벨(더 낮은 가지)에 연결된 노드
Leaf Node(Terminal Node) Child Node가 하나도 없는 노드
Sibling (Brother Node) 동일한 Parent Node를 가진 노드
Depth 트리에서 Mode가 가지고 있는 최대 Level
Branch 노드와 노드 사이의 연결

용어

 

구현

 

  • find => 노드를 따라 값의 대소를 비교해가며 찾기
  • insert => 노드를 따라 값의 대소를 비교해가며 좌측 또는 우측 노드에 값을 삽입한다.
  • delete
    1. Leaf Node 를 삭제하는 경우 => 그냥 삭제
    2. Child Node 가 하나인 경우 => 해당 노드를 삭제하고 parent 노드가 있던 자리에 Child 노드를 연결
    3. Child Node 가 두개인 경우 => 삭제할 노드 기준으로 좌측 노드의 자식 중 가장 큰 값이나, 우측 노드의 가장 작은 값을 삭제한 자리로 옮기면 된다. 다만, 그 값 또한 자식 노드가 있을 수 있는데, 그 노드는 옮겨질 노드의 부모 노드 아래쪽으로 옮겨주면 된다.

insert

 

 

시간복잡도

 

  • 트리의 높이(depth)를 h라고 표기한다면, O(h)
  • n개의 노드를 가진다면 h = 2logn 에 가까우므로 시간복잡도는 O(logn)
  • 한번 실행마다 50%의 탐색 수를 제거할 수 있음. => log 의 시간복잡도를 가지는 이유
  • 다만, 데이터가 순차적으로 쌓이는 경우(연속적인 데이터가 쌓일 때) 링크드리스트와 동일한 O(n)의 성능을 보여줌(ArrayList는 검색의 경우 O(n))

 

 

구현
package com.dataType.tree;

public abstract class Tree<T> {
    /**
     * 노드에 왼쪽 오른쪽이 있음을 주의 깊게 본다.
     * 연결 관계를 표시한 것이다.
     * */
    class Node<T> {
        // 실제 데이터
        private T data;
        // 좌측 노드 (주소)
        private Node left;
        // 우측 노드 (주소)
        private Node right;
        
        
        public Node(T value) {
            this.data = value;
        }
        
        public T getData() {
            return data;
        }

        public void setData(T data) {
            this.data = data;
        }

        public Node getLeft() {
            return left;
        }

        public void setLeft(Node left) {
            this.left = left;
        }

        public Node getRight() {
            return right;
        }

        public void setRight(Node right) {
            this.right = right;
        }
    }

    private Node root;

    public Tree() {
        this.root = null;
    }

    /**
     * 비교 기준을 외부에서 구현 받는다.
     * */
    public abstract int compareTo(T t1, T t2);

    /**
     * 값을 비교해서 작으면 왼쪽으로 크면 우측으로 자식 노드를 찾아 나간다.
     * 탐색시 있으면 true 반환
     * */
    public boolean find(T value) {
        Node<T> newNode = new Node<>(value);
        // 첫 노드를 기준으로
        Node<T> parent = this.root;
        while (parent != null) {
            // 부모 노드보다 작은 경우 좌측 노드를 기준으로 다시 찾는다.
            if (this.compareTo(parent.getData(), newNode.getData()) > 0) {
                parent = parent.getLeft();
            } else if (this.compareTo(parent.getData(), newNode.getData()) < 0) {
                // 부모 노드보다 큰 경우
                parent = parent.getRight();
            } else {
                // 비교값으로 객체를 찾고 equals 함수를 리턴
                return true;
            }
        }
        // Leaf Node 노드까지 탐색했는데도 없는 경우
        return false;
    }

    /**
     * 노드에서 데이터를 꺼내준다.
     * */
    public T get(T value) {
        Node<T> result = getNode(value);
        return result != null ? result.getData() : null;
    }

    /**
     * 노드를 찾아서 있으면 리턴 없으면
     * null 리턴
     * */
    private Node<T> getNode(T value) {
        Node<T> newNode = new Node<>(value);
        Node<T> parent = this.root;
        
        // 부모 노드가 있으면 반복 => leafNode를 찾는 과정
        while (parent != null) {
            if (this.compareTo(parent.getData(), newNode.getData()) > 0) {
                parent = parent.getLeft();
            } else if (this.compareTo(parent.getData(), newNode.getData()) < 0) {
                parent = parent.getRight();
            } else {
                return parent;
            }
        }
        return null;
    }

    /**
     * 부모 노드를 찾는다.
     * 비교값을 비교해서 노드를 찾으면 그 이전에 찾았던 노드를 반환 하고 없으면 null
     * */
    private Node<T> getParentNode(T value) {
        // 찾으려는 부모노드의 자식 노드
        Node<T> newNode = new Node<>(value);
        // 부모노드가 담길 공간
        Node<T> parent = this.root;
        // 부모노드를 잠시 저장해둘 공간
        Node<T> target = this.root;
        
        while (parent != null) {
            if (this.compareTo(parent.getData(), newNode.getData()) > 0) {
                // 부모 노드를 잠시 저장해둠
                target = parent;
                parent = parent.getLeft();
            } else if (this.compareTo(parent.getData(), newNode.getData()) < 0) {
                target = parent;
                parent = parent.getRight();
            } else {
                return target;
            }
        }
        return null;
    }

    /**
     * 값을 비교해서 왼쪽 또는 오른쪽에 노드를 연결한다.
     * */
    public void insert(T value) {
        // 받은 값을 통해서 노드를 생성
        Node<T> newNode = new Node(value);

        //노드가 비어있을 때
        if (this.root == null) {
            this.root = newNode;
            return;
        }

        Node<T> parent = this.root;
        
        while (parent != null) {
            // 부모 노드보다 작은 경우
            if (this.compareTo(parent.getData(), newNode.getData()) > 0) {
                if (parent.getLeft() == null) {
                    parent.setLeft(newNode);
                }
                parent = parent.getLeft();
            } else if (this.compareTo(parent.getData(), newNode.getData()) < 0) {
                // 부모 노드보다 큰 경우
                if (parent.getRight() == null) {
                    parent.setRight(newNode);
                }
                parent = parent.getRight();
            } else {
                // 같은 경우
                return;
            }
        }
    }

    /**
     * 노드를 삭제할 때 4가지 경우를 따져야 한다.
     *
     * 1. 없는 노드를 삭제하려는 경우
     *      => return
     * 2. 자식 노드가 둘다 없는 경우
     *      => 대상 노드의 부모 노드로 부터 대상 노드와의 연결을 끊으면 됨
     * 3. 하나의 자식 노드를 가진경우
     *      => 대상의 부모 노드로 부터 대상 노드가 있었던 위치(왼쪽, 오른쪽)에 대상 노드의 자식 노드만 연결해주면 됨
     * 4. 자식이 둘다 있는 경우
     *      => 한쪽을 정해서 연결 관계를 재정의 해야함. 우측을 기준으로 하면, 대상 노드의 우측엔 대상 노드보다 큰 값이고 좌측은 착은 값이다.
     *      => 고로 우측에서 가장 작은 값을 찾으면 좌측의 모든 노드 값보다 큰 값이므로, 대상의 왼쪽 자식을 우측 자식중 제일 작은 노드의 왼쪽 자식으로 넣는다.
     *      => 그리고 대상의 우측에 있던 노드를 부모 노드와 연결한다. (원래 대상이 있던 위치에)
     *
     * 어려우면 설명 MarkDown 파일을 참고하길 바란다. (README.md 파일)
     * */
    public void delete(T value) {
        Node<T> parent = getParentNode(value);
        Node<T> target = getNode(value);

        //노드를 찾지 못한 경우
        if (parent == null) {
            return;
        }

        boolean isRight = parent.getRight() == target;
        // 둘다 없는 경우
        if (target.getLeft() == null && target.getRight() == null) {
            if (isRight)    parent.setRight(null);
            else            parent.setLeft(null);
        }

        else if (target.getLeft() == null) {
            Node<T> child = target.getRight();
            if (isRight)    parent.setRight(child);
            else            parent.setLeft(child);

        }
        else if (target.getRight() == null) {
            Node<T> child = target.getLeft();
            
            if (isRight)    parent.setRight(child);
            else            parent.setLeft(child);
            
        // 좌우 둘다 빈값이 아닌 경우
        } else {
            //우측에서 가장 작은 값을 찾음
            Node<T> smallerNode = target.getRight();
            while(smallerNode.getLeft() != null) {
                smallerNode = smallerNode.getLeft();
            }

            smallerNode.setLeft(target.getLeft());
            
            if (isRight)    parent.setRight(target.getRight());
            else            parent.setLeft(target.getRight());
        }


    }

    /**
     * 디버깅 용도로 사용
     * */
    public void display(T value) {
        Node<T> node = getNode(value);

        T r = node.getRight() != null ? (T)node.getRight().getData() : null;
        T l = node.getLeft() != null ? (T)node.getLeft().getData() : null;

        System.out.println("좌측 자식 : " + (l != null ? l.toString() : null));
        System.out.println("우측 자식 : " + (r != null ? r.toString() : null));
    }

}

 

 

사용하기에 앞서 

 

비교할 대상을 단순 숫자가 아닌, 객체(제네릭)로 만들었기 때문에 별도의 클래스가 필요합니다.

아래와 같은 클래스를 먼저 만들어줍니다.

 

 

Account
package com.dataType.speedTest;

public class Account {
    private String name;
    private int id;

    public Account(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public Account setName(String name) {
        this.name = name;
        return this;
    }

    public int getId() {
        return id;
    }

    public Account setId(int id) {
        this.id = id;
        return this;
    }

    @Override
    public String toString() {
        return "Account{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }
}

 

사용
// Account 계정의 순서를 비교해서 정렬 기준을 만듬 => id값이 높을 수록
Tree<Account> tree = new Tree<Account>() {

    @Override
    public int compareTo(Account t1, Account t2) {
        return t1.getId() - t2.getId();
    }

   /* @Override
    public boolean equals(Account t1, Account t2) {

        return this.compareTo(t1, t2) == 0;//t1.getId().equals(t2.getId()) && (t1.getOrder() == t2.getOrder());
    }*/
};

Account account1 = new Account("one", 1);
Account account2 = new Account("two", 2);
Account account3 = new Account("three", 3);
Account account4 = new Account("four", 4);
Account account5 = new Account("five", 5);
Account account6 = new Account("six", 6);
Account account7 = new Account("seven", 7);
Account account8 = new Account("eight", 8);
Account account9 = new Account("nine", 9);
Account account10 = new Account("ten", 10);
Account account11 = new Account("eleven", 11);
Account account12 = new Account("twelve", 12);
Account account13 = new Account("thirteen", 13);
Account account14 = new Account("fourteen", 14);
Account account15 = new Account("fifteen", 15);

tree.insert(account8);
tree.insert(account4);
tree.insert(account12);
tree.insert(account2);
tree.insert(account6);
tree.insert(account10);
tree.insert(account14);
tree.insert(account1);
tree.insert(account3);
tree.insert(account5);
tree.insert(account7);
tree.insert(account9);
tree.insert(account11);
tree.insert(account13);
tree.insert(account15);

// 노드 찾기 => 이때 오직 id 값으로 찾음
boolean result1 = tree.find(account3);
boolean result2 = tree.find(new Account("one", 16));
System.out.println(result1); // true
System.out.println(result2); // false

// 삭제하기
tree.delete(account4);

// 디버깅
tree.display(account8);
tree.display(account6);
tree.display(account5);

 

위 소스를 그림으로 보자면 아래와 같습니다.

삽입된 모습
삭제할 때
삭제된 이후 최종 모습

 

출력결과
true
false
좌측 자식 : Account{name='six', id=6}  // 8번
우측 자식 : Account{name='twelve', id=12}  // 8번
좌측 자식 : Account{name='five', id=5}  // 6번
우측 자식 : Account{name='seven', id=7}  // 6번
좌측 자식 : Account{name='two', id=2}  // 5번
우측 자식 : null  // 5번