본문 바로가기

Data Structure & Algorithm

트리 (Tree) (3)

6.4. 이진 탐색 트리 삭제

이진 탐색 트리에서 특정 노드를 삭제하는 것은 굉장히 복잡하다. 경우의 수를 나눠서 이해하는 것이 좋다.

 

  1. Leaf Node 삭제하는 경우 (Branch가 없을 때)
  2. Child Node가 하나인 Node 삭제하는 경우 (Branch가 한 개일 때)
  3. Child Node가 두 개인 Node 삭제하는 경우 (Branch가 두 개일 때)

 

일단, 이전까지 만들었던 이진 탐색 트리에 데이터 넣는 메서드, 탐색하는 메서드가 정리된 NodeMgmt는 다음과 같다.

 

class NodeMgmt:
    def __init__(self, head):
        self.head = head

    def insert(self, value):
        self.current_node = self.head
        while True:
            if value < self.current_node.value:
                if self.current_node.left != None:
                    self.current_node = self.current_node.left
                else:
                    self.current_node.left = Node(value)
                    break
            else:
                if self.current_node.right != None:
                    self.current_node = self.current_node.right
                else:
                    self.current_node.right = Node(value)
                    break

    def search(self, value):
        self.current_node = self.head
        while self.current_node:
            if self.current_node.value == value:
                return True
            elif value < self.current_node.value:
                self.current_node = self.current_node.left
            else:
                self.current_node = self.current_node.right
        return False

 

위의 경우의 수 3가지를 나눠서 작성하려면, 일단 삭제할 노드를 탐색하는 코드가 필요하다.

 

삭제할 노드가 없는 경우도 처리해야 하므로, 이를 위해 삭제할 노드가 없는 경우는 False를 리턴하고, 함수를 종료시켜야 한다.

 

class NodeMgmt:
    def __init__(self, head):
        self.head = head
        
    def delete(self, value):	# 특정 value를 가진 노드를 삭제하려는 것
        searched = False	# 트리를 순회하면서 해당 노드를 찾았는지 못 찾았는지 표시하는 것
        # current_node는 삭제할 노드를 지칭하게끔 하고, parent는 삭제할 노드의 parent node를 지칭하게 할 것이다.
        self.current_node = self.head
        self.parent = self.head	# 삭제할 때, 해당 노드의 parent node에서도 작업을 해줘야 한다. 지칭하는 주소를 없애버린다든지..
        while self.current_node:
            if self.current_node.value == value:
                searched = True
                break
            elif value < self.current_node.value:	# 해당 노드가 삭제할 노드가 아니라면, 값의 크기를 비교해서 왼쪽, 오른쪽 어디에 있을지 봐야 한다.
                self.parent = self.current_node
                self.current_node = self.current_node.left
            else:	# 같거나 큰 경우
                self.parent = self.current_node
                self.current_node = self.current_node.right

        # while 구문을 지나가면 두 가지 경우가 있다.
        # 하나는 해당 노드를 찾았으니, searched에는 True가 들어가 있고, current_node는 삭제할 노드, parent는 삭제할 노드의 parent인 상황이다.
        # 다른 하나는 결국에는 self.current_node가 None이 돼서 넘어온 경우이고, 이때 searched는 False가 되어 있다. 찾는 노드가 이 트리에 없으니 그냥 종료하면 된다.

        if searched == False:	# self.current_node가 None이 돼서 넘어온 경우
            return False

        ### 이후부터 Case들을 분리해서 코드 작성

 

6.4.1. Leaf Node 삭제

첫 번째는 삭제할 노드가 맨 끝에 있는 Leaf Node(Terminal Node)인 경우이다. Leaf Node란 Child Node가 없는 Node이다. 삭제할 Node의 Parent Node가 삭제할 Node를 가리키지 않도록 하면 된다.

 

http://www.fun-coding.org/00_Images/tree_remove_leaf.png

 

위 그림에서, Leaf Node인 19를 삭제하기로 했다면, 19인 노드를 삭제하고, 15인 노드가 가리키는 Branch를 없애버리면 된다.

 

http://www.fun-coding.org/00_Images/tree_remove_leaf_code.png

 

        # Case 1: 삭제할 Node가 Leaf Node인 경우
        if self.current_node.left == None and self.current_node.right == None:
            if value < self.parent.value:	# parent의 왼쪽에 붙어있냐, 오른쪽에 붙어있냐에 따라 달라질 수 있기에
                self.parent.left = None	# 작다면 parent의 왼쪽에 있는 branch가 None을 가리키게 하는 것
            else:
                self.parent.right = None
            del self.current_node

 

위 그림과 코드를 보면, 지우려고 하는 Leaf Node가 parent 노드의 왼쪽에 있는지, 오른쪽에 있는지도 고려해야 한다. 왜냐하면 parent가 가리키는 주소도 None으로 바꿔야 하기 때문이다.

 

6.4.2. Child Node가 하나인 Node 삭제

두 번째 경우는 Child Node가 하나인 Node를 삭제하는 경우이다. 여기서는 삭제할 노드의 Parent Node가 삭제할 노드의 Child Node를 가리키도록 한다.

 

http://www.fun-coding.org/00_Images/tree_remove_1child.png

 

예를 들어, 위 그림에서 Child Node를 한 개 가지고 있는 15를 삭제하려고 한다면, 15를 삭제하고 15의 Parent Node인 10이 삭제한 노드 15의 Child Node를 가리키게 해야 하는 것이다.

 

이 Case 2는 또 두 가지로 나눌 수 있다.

 

http://www.fun-coding.org/00_Images/tree_remove_1child_code.png

 

위 그림에서 삭제할 노드가 갈색으로 칠해진 노드라고 할 때,

 

  1. 삭제할 노드가 Child Node를 하나 갖고 있는데 왼쪽에 가지고 있을 때
  2. 삭제할 노드가 Child Node를 하나 갖고 있는데 오른쪽에 가지고 있을 때

우선 첫 번째 경우를 코드로 구현해보겠다.

 

        # Case 2: 삭제할 Node가 Child Node를 한 개 가지고 있을 경우
        # Case 2-1: 삭제할 노드가 Child Node를 왼쪽에 가지고 있을 때
        if self.current_node.left != None and self.current_node.right == None:
            if value < self.parent.value:	# 위 그림 기준으로, parent인 노드 10보다 작은 노드 5를 지우려는 상황
                self.parent.left = self.current_node.left	# parent인 10의 left를 지우려는 노드 5의 left인 3으로 연결하는 것
            else:	# 위 그림 기준으로, parent인 노드 10보다 큰 노드 15를 지우려는 상황
                self.parent.right = self.current_node.left	# parent인 10의 right를 지우려는 노드 15의 left인 12로 연결하는 것
        # Case 2-2: 삭제할 노드가 Child Node를 오른쪽에 가지고 있을 때
        elif self.current_node.left == None and self.current_node.right != None:
            if value < self.parent.value:
                self.parent.left = self.current_node.right
            else:
                self.parent.right = self.current_node.right

 

이렇게 된다.

 

이진 탐색 트리를 구현할 때에는 경우의 수를 직접 그림으로 그려보는 것이 중간에 헷갈리지 않고 차근차근 풀어나갈 수 있는 팁이라고 한다.

 

6.4.3. Child Node가 두 개인 Node 삭제

Child Node가 두 개인 경우, 두 가지 방법 중 하나를 사용하면 된다.

 

  1. 삭제할 Node의 오른쪽 자식 중, 가장 작은 값을 삭제할 Node의 Parent Node가 가리키도록 한다.
  2. 삭제할 Node의 왼쪽 자식 중, 가장 큰 값을 삭제할 Node의 Parent Node가 가리키도록 한다.

http://www.fun-coding.org/00_Images/tree_remove_2child.png

 

가령 첫 번째 방법을 사용한다고 했을 때, 삭제할 노드 5의 오른쪽 자식은 6이다. 6에게는 계속 Branch가 뻗어나가며 자식들이 있을 수 있다. 그러면 결과적으로 이 안에서 가장 작은 것은 노드 6에서 뻗어나가는 자식들 중 가장 왼쪽에 있는 노드이다. 그 가장 작은 노드를 선택하고, 그것을 삭제하는 노드 5의 위치로 올리면 되는 것이다. 위 그림의 오른쪽 트리에 잘 나타나 있다.

 

반대로 두 번째 방법을 사용한다고 했을 때, 삭제할 노드 5의 왼쪽 자식은 3이다. 3에게는 계속 Branch가 뻗어나가면서 자식들이 있을 수 있다. 이중에서 가장 큰 것은 노드 3에서 뻗어나가는 자식들 중 가장 오른쪽에 있는 노드이다. 그 가장 큰 노드를 선택하고, 그것을 삭제하는 노드 5의 위치로 올리면 되는 것이다.

 

첫 번째 방법을 사용할 경우 순서대로 정리해 보면,

  • 삭제할 Node의 오른쪽 자식 선택
  • 오른쪽 자식의 가장 왼쪽에 있는 Node를 선택
  • 해당 Node를 삭제할 Node의 Parent Node의 왼쪽 Branch가 가리키게 함
  • 해당 Node의 왼쪽 Branch가 삭제할 Node의 왼쪽 Child Node를 가리키게 함
  • 해당 Node의 오른쪽 Branch가 삭제할 Node의 오른쪽 Child Node를 가리키게 함
  • 만약 해당 Node가 오른쪽 Child Node를 가지고 있었을 경우에는, 해당 Node의 본래 Parent Node의 왼쪽 Branch가 해당 오른쪽 Child Node를 가리키게 함

 

삭제할 Node가 Child Node를 두 개 가지고 있는 경우도 두 가지 경우로 나눠서 접근해야 한다.

  1. 삭제할 Node가 Parent Node 왼쪽에 있을 때
  2. 삭제할 Node가 Parent Node 오른쪽에 있을 때

 

Case 3-1: 삭제할 Node가 Child Node를 두 개 가지고 있을 경우 (삭제할 Node가 Parent Node 왼쪽에 있을 때)

 

이번에는 두 가지 전략 중 첫 번째인, 삭제할 Node의 오른쪽 자식 중, 가장 작은 값을 삭제할 Node의 Parent Node가 가리키도록 하는 전략을 사용할 것이다.

 

여기서 경우의 수가 또 2 가지로 나뉘는데,

 

  • Case 3-1-1: 삭제할 Node가 Parent Node의 왼쪽에 있고, 삭제할 Node의 오른쪽 자식 중, 가장 작은 값을 가진 Node의 Child Node가 없을 때
  • Case 3-1-2: 삭제할 Node가 Parent Node의 왼쪽에 있고, 삭제할 Node의 오른쪽 자식 중, 가장 작은 값을 가진 Node의 Child Node가 있을 때 (가장 작은 값을 가진 Node의 Child Node가 왼쪽에 있을 경우는 없음. 왜냐하면 왼쪽 Node가 있다는 것은 해당 Node보다 더 작은 값을 가진 Node가 있다는 뜻이기 때문)

 

http://www.fun-coding.org/00_Images/tree_remove_2child_code_left.png

 

Case 3-1을 코드로 구현해보면 다음과 같다.

 

        if self.current_node.left != None and self.current_node.right != None:	# Case 3: 삭제할 Node가 Child Node를 두 개 가지고 있을 경우
            if value < self.parent.value:	# Case 3-1: 삭제할 Node가 Parent Node 왼쪽에 있을 때
                self.change_node = self.current_node.right
                self.change_node_parent = self.current_node.right	# change_node뿐만 아니라, change_node_parent까지 알고 있어야 한다. 왜냐하면 가리키는 것을 수정해야 하기 때문이다.
                while self.change_node.left != None:	# 가장 작은 값을 가지는 노드를 찾기 위해 순회
                    self.change_node_parent = self.change_node
                    self.change_node = self.change_node.left
                if self.change_node.right != None:	# Case 3-1-2: 가장 작은 값을 가지는 노드의 오른쪽에 Child Node가 있을 때
                    self.change_node_parent.left = self.change_node.right
                else:
                    self.change_node_parent.left = None	# 가장 작은 노드를 찾았고, 그 노드 오른쪽에 Child Node가 없다면 change_node_parent의 왼쪽 연결고리를 끊어야 한다.
                self.parent.left = self.change_node	# 위 그림 기준으로, 16을 parent인 31 왼쪽에 연결한다.
                self.change_node.right = self.current_node.right
                self.change_node.left = self.change_node.left

 

Case 3-2: 삭제할 Node가 Child Node를 두 개 가지고 있을 경우 (삭제할 Node가 Parent Node 오른쪽에 있을 때)

 

이번에도 두 가지 전략 중 첫 번째인, 삭제할 Node의 오른쪽 자식 중, 가장 작은 값을 삭제할 Node의 Parent Node가 가리키도록 하는 전략을 사용할 것이다.

 

여기서 또 경우의 수가 두 가지로 나뉜다.

 

  • Case 3-2-1: 삭제할 Node가 Parent Node의 오른쪽에 있고, 삭제할 Node의 오른쪽 자식 중, 가장 작은 값을 가진 Node의 Child Node가 없을 때
  • Case 3-2-2: 삭제할 Node가 Parent Node의 오른쪽에 있고, 삭제할 Node의 오른쪽 자식 중, 가장 작은 값을 가진 Node의 오른쪽에 Child Node가 있을 때 (가장 작은 값을 가진 Node의 Child Node가 왼쪽에 있을 경우는 없다)

 

http://www.fun-coding.org/00_Images/tree_remove_2child_code_right.png

 

Case 3-2를 코드로 구현해보면 다음과 같다.

 

            else:	# Case 3-2: 삭제할 Node가 Parent Node 오른쪽에 있을 때
                self.change_node = self.current_node.right
                self.change_node_parent = self.current_node.right
                while self.change_node.left != None:
                    self.change_node_parent = self.change_node
                    self.change_node = self.change_node.left
                if self.change_node.right != None:
                    self.change_node_parent.left = self.change_node.right
                else:
                    self.change_node_parent.left = None
                self.parent.right = self.change_node
                self.change_node.left = self.current_node.left
                self.change_node.right = self.current_node.right

 

6.5. 파이썬 전체 코드 구현

 

class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class NodeMgmt:
    def __init__(self, head):
        self.head = head

    def insert(self, value):
        self.current_node = self.head
        while True:
            if value < self.current_node.value:
                if self.current_node.left != None:
                    self.current_node = self.current_node.left
                else:
                    self.current_node.left = Node(value)
                    break
            else:
                if self.current_node.right != None:
                    self.current_node = self.current_node.right
                else:
                    self.current_node.right = Node(value)
                    break

    def search(self, value):
        self.current_node = self.head
        while self.current_node:
            if self.current_node.value == value:
                return True
            elif value < self.current_node.value:
                self.current_node = self.current_node.left
            else:
                self.current_node = self.current_node.right
        return False

    def delete(self, value):
        searched = False
        self.current_node = self.head
        self.parent = self.head
        while self.current_node:
            if self.current_node.value == value:
                searched = True
                break
            elif value < self.current_node.value:
                self.parent = self.current_node
                self.current_node = self.current_node.left
            else:
                self.parent = self.current_node
                self.current_node = self.current_node.right

        if searched == False:
            return False

        # Case 1: 삭제할 Node가 Leaf Node인 경우
        if self.current_node.left == None and self.current_node.right == None:
            if value < self.parent.value:
                self.parent.left = None
            else:
                self.parent.right = None
            del self.current_node

        # Case 2: 삭제할 Node가 Child Node를 한 개 가지고 있을 경우
        # Case 2-1: 삭제할 노드가 Child Node를 왼쪽에 가지고 있을 때
        if self.current_node.left != None and self.current_node.right == None:
            if value < self.parent.value:
                self.parent.left = self.current_node.left
            else:
                self.parent.right = self.current_node.left
        # Case 2-2: 삭제할 노드가 Child Node를 오른쪽에 가지고 있을 때
        elif self.current_node.left == None and self.current_node.right != None:
            if value < self.parent.value:
                self.parent.left = self.current_node.right
            else:
                self.parent.right = self.current_node.right

        # Case 3: 삭제할 Node가 Child Node를 두 개 가지고 있을 경우
        if self.current_node.left != None and self.current_node.right != None:
            # Case 3-1: 삭제할 Node가 Parent Node 왼쪽에 있을 때
            if value < self.parent.value:
                self.change_node = self.current_node.right
                self.change_node_parent = self.current_node.right
                while self.change_node.left != None:
                    self.change_node_parent = self.change_node
                    self.change_node = self.change_node.left
                if self.change_node.right != None:
                    self.change_node_parent.left = self.change_node.right
                else:
                    self.change_node_parent.left = None
                self.parent.left = self.change_node
                self.change_node.right = self.current_node.right
                self.change_node.left = self.change_node.left
            # Case 3-2: 삭제할 Node가 Parent Node 오른쪽에 있을 때
            else:
                self.change_node = self.current_node.right
                self.change_node_parent = self.current_node.right
                while self.change_node.left != None:
                    self.change_node_parent = self.change_node
                    self.change_node = self.change_node.left
                if self.change_node.right != None:
                    self.change_node_parent.left = self.change_node.right
                else:
                    self.change_node_parent.left = None
                self.parent.right = self.change_node
                self.change_node.left = self.current_node.left
                self.change_node.right = self.current_node.right

 

전체 코드는 이렇게 된다.

 

이 파이썬 코드를 테스트 해보겠다. 0~999 숫자 중에서 임의로 100개를 추출해서 이진 탐색 트리에 입력, 검색 및 삭제 작업을 수행할 것이다.

 

random 라이브러리를 활용할 것이다. random.randint(첫 번째 숫자, 마지막 숫자)를 사용하면, 첫 번째 숫자부터 마지막 숫자 사이에 있는 숫자를 랜덤으로 선택해서 리턴한다.

 

import random

# 0 ~ 999 중, 100개의 숫자 랜덤 선택
bst_nums = set()
while len(bst_nums) != 100:
    bst_nums.add(random.randint(0, 999))

# 선택된 100개의 숫자를 이진 탐색 트리에 입력, 임의로 루트노드는 500을 넣기로 함
head = Node(500)
binary_tree = NodeMgmt(head)
for num in bst_nums:
    binary_tree.insert(num)

# 입력한 100개의 숫자 검색 (검색 기능 확인)
for num in bst_nums:
    if binary_tree.search(num) == False:
        print('search failed', num)

# 입력한 100개의 숫자 중 10개의 숫자를 랜덤 선택
delete_nums = set()
bst_nums = list(bst_nums)
while len(delete_nums) != 10:
    delete_nums.add(bst_nums[random.randint(0, 99)])

# 선택한 10개의 숫자를 삭제 (삭제 기능 확인)
for del_num in delete_nums:
    if binary_tree.delete(del_num) == False:
        print('delete failed', del_num)

 

위 코드로 테스트를 했을 때, search failed, delete failed가 단 하나도 나타나지 않았다. 잘 작동하는 것이다.

 

 

 

 

 

[참고자료]

패스트캠퍼스 개발자 취업 합격 패스 With 코딩테스트, 기술면접 초격차 패키지 Online 강의자료

'Data Structure & Algorithm' 카테고리의 다른 글

힙 (Heap) (2)  (1) 2023.01.06
힙 (Heap)  (0) 2023.01.06
트리 (Tree) (2)  (0) 2023.01.03
트리 (Tree)  (0) 2023.01.02
해시 테이블 (Hash Table) (3)  (0) 2023.01.01