오늘도 더 나은 코드를 작성하였습니까?

이진탐색트리 BST(Binary Search Tree) Feat. kotlin 본문

자료구조

이진탐색트리 BST(Binary Search Tree) Feat. kotlin

hik14 2023. 10. 24. 20:32

참조한 자료

유튜브 - https://www.youtube.com/watch?v=i57ZGhOVPcI

정의

*이진 트리(binary tree)- 각각의 노드가 최대 두 개의 자식 노드를 가지는 트리 자료 구조

 

이진 탐색 트리(BST: binary search tree)

 

이진 트리중 아래와 같은 특징을 지니는 트리

- 모든 노드는 값(value)을 가진다.

- 값은 대소관계가 존재한다. 즉 비교 가능하다.

- 노드의 왼쪽 서브트리에는 그 노드의 값보다 작은 값들을 지닌 노드들로 이루어져 있다.

- 노드의 오른쪽 서브트리에는 그 노드의 값보다 큰 값들을 지닌 노드들로 이루어져 있다.

- 좌우. 서브 트리는 각각이 다시 이진 탐색 트리 이다.

 

search / insert / delete

*search 는 Insert 및 delete를 위해 필수적으로 발생하기 때문에 생략한다.

 

Insert (삽입)

- 트리를 검색한 후 키와 일치하는 노드가 없으면, 마지막 노드에서 키와 노드의 크기를 비교하여서 왼쪽이나 오른쪽에 새로운 노드를 삽입한다.

 

delete (삭제)

-트리를 검색한 후 키와 일치하는 노드가 있다면 삭제한다. 삭제시 자식노드의 숫자에 따라 다르게 작동한다.

 

- 자식노드가 0개 노드(리프 노드) 삭제 : 해당 노드를 단순히 삭제한다.

예시) delete(20) 

 

- 자식노드가 1개 노드 삭제 : 해당 노드를 삭제하고 그 위치에 해당 노드의 자식노드를 대입한다

예시) delete(30) 

- 자식노드가 2개 노드 삭제 : 삭제하고자 하는 노드의 값을 해당 노드의 왼쪽 서브트리에서 가장 큰값으로 변경하거나, 오른쪽 서브트리에서 가장 작은 값으로 변경한 뒤, 해당 노드(왼쪽서브트리에서 가장 큰 값을 가지는 노드 또는 오른쪽 서브트리에서 가장 작은 값을 가지는 노드)를 삭제한다.

 

예시) delete(50) 

오른쪽 서브트리에서 가장 작은 값으로 변경

구현 

fun main() {
    val bst = BinarySearchTree()

    bst.add(10)
    bst.add(50)
    bst.add(20)
    bst.add(7)
    bst.add(1000)

    bst.print()
    println(bst.getSize())

    bst.delete(10)
    bst.print()
    println(bst.getSize())
}

data class Node(
    var key: Int,
    var left: Node? = null,
    var right: Node? = null
)

class BinarySearchTree(private var root: Node? = null) {

    private var size: Int = if (root == null) 0 else 1

    fun getSize() = size

    /*
        search
        재귀함수를 통해 구현 가능하다.
     */
    fun search(node: Node? = root, value: Int): Node? =
        if (node == null || node.key == value)
            node
        else if (node.key > value) {
            search(node = node.left, value = value)
        } else {
            search(node = node.right, value = value)
        }

    private fun addNode(parent: Node?, value: Int) {

        if (parent == null || parent.key == value) {
            // 이미 존재하는 key 라면 추가 하지 않는다.
            return
        } else if (parent.key > value) {
            if (parent.left == null) {
                parent.left = Node(key = value)
                size += 1
            } else
                addNode(parent.left, value)
        } else {
            if (parent.right == null) {
                parent.right = Node(key = value)
                size += 1
            } else
                addNode(parent.right, value)
        }
    }

    fun add(value: Int) {
        if (root == null) {
            root = Node(key = value)
            size += 1
        } else {
            addNode(root, value)
        }
    }

    fun delete(value: Int): Boolean {

        // 1. 삭제할 노드와 그 노드의 부모 노드의 참조를 얻는다.
        var node = root
        var parentNode: Node? = null
        // 삭제할 노드가 부모의 왼쪽, 오른쪽 자식인지 확인.
        var isFromLeft = false

        while (true) {
            // root node가 null 이거나 모든 리프노드 까지 탐색을 했는데 키값을 못 찾은 경우
            if (node == null) {
                return false
            }

            if (node.key == value)
                break
            else if (node.key > value) {
                parentNode = node
                node = node.left
                isFromLeft = true
            } else {
                parentNode = node
                node = node.right
                isFromLeft = false
            }
        }

        // 2-1 삭제할 노드가 leaf 노드
        if (node?.left == null && node?.right == null) {

            // root 만 남아 있는 상황.
            if (parentNode == null) {
                root = null
                size = 0
                return true
            }

            // 삭제할 노드가 부모의 left, right 노드 인지 확인 후 삭제.
            if (isFromLeft) {
                parentNode.left = null
            } else {
                parentNode.right = null
            }
        }

        // 2-2 삭제할 노드가 자식을 1개인 경우

        //  삭제할 노드가 왼쪽 자식을 가진 경우
        else if (node.right == null) {
            if (parentNode == null)
                root = node.left
            else if (isFromLeft) {
                parentNode.left = node.left
            } else {
                parentNode.right = node.left
            }
        }
        //  삭제할 노드가 오른쪽 자식을 가진 경우
        else if (node.left == null) {
            if (parentNode == null)
                root = node.right
            else if (isFromLeft) {
                parentNode.left = node.right
            } else {
                parentNode.right = node.right
            }
        }
        // 2-3 삭제할 노드가 자식을 2개인 경우

        // 삭제할 노드의 predecessor(자신의 보다 작으면서 가장 큰것)
        parentNode = node
        var left = parentNode?.left
        isFromLeft = true

        // successor를 찾음.
        while (left?.right != null) {
            parentNode = left
            left = left.right
            isFromLeft = false
        }

        node?.key = left?.key!!

        // predecessor 니까 오른쪽 자식은 없다.
        if (isFromLeft){
            parentNode?.left = left.left
        }else
            parentNode?.right = left.left

        size -= 1
        return true
    }

    fun print() {
        printSubTree(root)
    }

    /*
        inOrder 순회로 이진탐색 트리의 오름 차순 순으로 출력 한다.
     */
    private fun printSubTree(node: Node?) {
        if (node != null) {
            printSubTree(node.left)
            println("node key = ${node.key}")
            printSubTree(node.right)
        }
    }
}

시간복잡도

root 노드에서 원하는 값을 찾는 다면 best case로 상수시간.

평균적으로는 대소 비교에 따라 탐색할 노드를 절반으로 줄이기 때문에 logN

worst case는 이진 탐색트리가 매우 한쪽으로 편향되어 있는 경우, 

'자료구조' 카테고리의 다른 글

Red-Black Tree (레드 블랙 트리)  (0) 2023.10.26
균형을 유지하는 AVL-Tree  (0) 2023.10.25
B Tree 데이터 삭제  (1) 2023.10.11
B Tree 정의와 데이터 삽입  (0) 2023.10.02
펜윅트리 (Binary Index Tree) feat. kotlin  (0) 2023.07.03