Computer Science/자료구조

자료구조 | binary trees, heap sort, threaded binary trees (python 구현) | Trees

토마토. 2022. 1. 8. 23:22
  • Binary tree
  • ex) huffman coding tree, tree traverse, evaluation tree, threaded binary trees
  • Heap
  • ex) priority queue, insert, delete, sort

Introduction

tree

tree는 계층적인 데이터 구조를 나타내기 위한 자료 구조다. 

tree는 root와 sub-trees로 이루어진다. 

 

huffman coding tree

huffman coding tree는 텍스트를 압축하기 위한 알고리즘으로, 

빈도가 높은 문자를 짧은 비트로 매칭되도록 트리를 구현한 것이다. 

 

import copy
class Node:
    def __init__(self, char, num):
        self.right = None
        self.left = None
        self.data = char
        self.freq = num
        self.huff = []
        
class Huffman:
    def __init__(self, char, num):
        self.root = None
        self.char = [char for _, char in sorted(zip(num, char))]
        self.num = [num for num, _ in sorted(zip(num, char))]
        self.nodes = []
    
    def selectTwo(self, num):
        minVal = num[0] + num[1]
        minFir = 0
        minSec = 1
        
        for i in range(len(num)-1):
            if num[i] + num[i+1] < minVal:
                minVal = num[i] + num[i+1]
                minFir = i
                minSec = i+1
        
        return minFir, minSec
    
    def createTree(self):
        print("Huffman coding tree")
        
        # step 0. sort char and num
        print(self.num)
        
        # step 1. create terminal node
        for i in range(len(self.char)):
            tmp = Node(self.char[i], self.num[i])
            self.nodes.append(tmp)
         
        # step 2. create parent node
        while True:
            try:
                if len(self.num) <= 1:
                    break
                
                # 인접한 두 개를 더하는 것 중에 제일 작은 노드를 찾는다. 
                first, second = self.selectTwo(self.num)
                # 단말 노드를 합해 부모 노드를 만든다
                tmp = Node('', self.nodes[first].freq+self.nodes[second].freq)
                tmp.left = self.nodes[first]
                tmp.right = self.nodes[second]
                 
                # node 리스트, num 리스트를 재조정해준다
                self.nodes.pop(first)
                self.nodes.pop(second-1)
                self.num.pop(first)
                self.num.pop(second-1)
                self.nodes.insert(first, tmp)
                self.num.insert(first, tmp.freq)
                
                # 현 상태 출력
                print(self.num)
            except:
                pass
        self.root = tmp
        
    def createCode(self, root, code):
        if root.left == None and root.right == None:
            root.huff = code
            print(root.data, root.huff)
            return
        else:
            root.huff = code
            left = copy.deepcopy(code)
            left.append('0')
            right = copy.deepcopy(code)
            right.append('1')
            self.createCode(root.left, left)
            self.createCode(root.right, right)
        # root 노드에서 시작한다.   
        # 탐색하면서 왼족에는 0, 오른족에는 1을 append 해준다.     
          
    def codes(self):
        print("Huffman codes")      
        self.createCode(self.root, [])  

# character
char = ['w', 'f', 'r', 'm', 'e', 'd', 'o', 't', 'i', 'a', 'n','\' \'']
# frequency
num =  [3, 3, 2, 2, 6, 3, 3, 2, 1, 1, 2, 1]

# character
char = ['a', 'b', 'c', 'd', 'e', 'f']
# frequency
num =  [6,5,4,3,2,1]

h = Huffman(char, num)
print()
h.createTree()
print()
h.codes()

 

Binary trees

binary trees

binary tree는 empty tree가 허용되고, child order를 좌/우로 구분한다.

 

아래는 이미 생성된 BinaryTree의 높이/깊이를 계산하는 프로그램이다. 

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

class BinaryTree:
    def __init__(self):
        self.root = None
        self.treeDepth = 0
        self.treeHeight = 0
        
        temp = Node('-')
        self.root = temp
        
        temp = Node('+')
        self.root.left = temp
        
        temp = Node('/')
        self.root.right = temp
        
        temp = Node('*')
        self.root.left.left = temp
        
        temp = Node('C')
        self.root.left.right = temp
        
        temp = Node('D')
        self.root.right.left = temp
        
        temp = Node('E')
        self.root.right.right = temp
        
        temp = Node('A')
        self.root.left.left.left = temp
        
        temp = Node('B')
        self.root.left.left.right = temp
        
        
    def printBT(self, tree):
        if tree:
            try:
                print("%d" % tree.data)
            except:
                print("%s" % tree.data)
            self.printBT(tree.left)
            self.printBT(tree.right)
    
    # 탐색
    def inorderSearch(self, node, item):
        if node:
            if node.data == item:
                return node
        return None
    
    def depth_node(self, node, item):
        if node:
            try:
                print("%d %d" % (node.data, item))
            except:
                print("%s %d" % (node.data, item))
            self.depth_node(node.left, item+1)
            self.depth_node(node.right, item+1)   
        else:
            return -1
             
    def nodeCalculate(self, node):
        if node.left or node.right:
            return max(self.nodeCalculate(node.left), self.nodeCalculate(node.right)) + 1
        else:
            return 0
    
    # 탐색
    def height_node(self, node, item):
        if node:
            try:
                print("%d" % (node.data), end=' ')
            except:
                print("%s" % (node.data), end =' ')   
            print("%d" % self.nodeCalculate(node)) 
            self.height_node(node.left, item)
            self.height_node(node.right, item) 
        else:
            return 0  

    def depth(self, root):
        self.finddepth(root, 0)
        print(f'tree depth : {self.treeDepth}')
        
    def finddepth(self, root, depth):
        # return 조건
        if not root:
            return 0
        else:
            if self.treeDepth < depth:
                self.treeDepth = depth
            self.finddepth(root.left, depth+1)
            self.finddepth(root.right, depth+1)
        
    def height(self, root):
        self.findheight(self.root, 0) # 결국 root의 depth를 찾는다
        print(f'tree height : {self.treeHeight}')
        return 0 
    
    def findheight(self, root, height):
        if root:
            if self.treeHeight < height:
                self.treeHeight = height
            self.findheight(root.left, height+1)
            self.findheight(root.right, height+1) 
        else:
            return 0  
            
c = BinaryTree()
c.printBT(c.root)
print('DDDDDDD')
c.depth_node(c.root, 0)
print('HHHHHH')
c.height_node(c.root, 0)
print("TDTDTDTD")
c.depth(c.root)
print("HHHHDDDD")
c.height(c.root)

 

 

tree traversal

postorder

 

    def postorder(self, node):
        if node:
            self.inorder(node.left)
            self.inorder(node.right)   
            print(node.data)

 

traverse
postorder
A
*
B
+
C
D
/
E
-

inorder

 

    def inorder(self, node):
        if node:
            self.inorder(node.left)
            print(node.data)
            self.inorder(node.right)

 

traverse
inorder
A
*
B
+
C
-
D
/
E

preorder

 

    def preorder(self, node):
        if node:
            print(node.data) 
            self.inorder(node.left)
            self.inorder(node.right)
traverse
preorder
-
A
*
B
+
C
D
/
E

level order

level by level로 출력한다. 

이는 Queue를 이용해서 (FIFO) 구현한다. 

 

알고리즘을 생각해보자.

- enqueue : root를 Q에 넣는다. 

- dequeue : dequeue하고 출력

- 다시 enqueue : root의 children을 Q에 enqueue

- queue가 empty 해질 때까지(종료 조건) 반복

 

    def level_order(self, node):
        queue = [] # FIFO
        queue.insert(0, node)
        while queue:
            tmp = queue.pop(0)
            print(tmp.data)
            if tmp.left:
                queue.insert(0, tmp.left)
            if tmp.right:
                queue.insert(0, tmp.right)

 

level_order
-
/
E
D
+
C
*
B
A

 

iterative inorder

재귀함수를 쓰지 않고 구현하려는 것이다. 잘 생각해보면 되는데, inorder traverse 방식은 왼쪽 -> root -> 오른쪽 순서대로 출력한다는 것을 고려해서 알고리즘을 구상해보자. 

 

<알고리즘>

- 왼쪽 방향 노드를 모두 스택에 push한다. 

- 한번 pop해주는데, 

만약 스택이 비어있으면 => 종료 조건

그렇지 않으면 => 오른쪽을 탐색

 

<시간 복잡도>

O(n)

 

구현

    def iterative_inorder(self, node):
        stack = []
        while True:
            while node:
                stack.append(node)
                node = node.left
            try:
                node = stack.pop()
            except:
                break
            print(node.data)
            node = node.right

 

iterative_inorder
A
*
B
+
C
-
D
/
E

 

copy tree

tree를 카피해주는 함수

 

    def copyTree(self, tree):
        if tree:
            node = Node(tree.data)
            node.left = self.copyTree(tree.left)
            node.right = self.copyTree(tree.right)
            return node

 

evaluation tree

수식 평가 트리를 만들어보자

 

스켈레톤 코드

class Node:
    def __init__(self, data):
        self.data = data
        self.value = None
        self.left = None
        self.right = None
class EvalTree:
    def __init__(self):
        pass
    def eval_tree(self, node):
        pass

c = EvalTree()
c.eval_tree()
print()

 

구현 완료 :)

class Node:
    def __init__(self, data):
        self.data = data
        self.value = None
        self.left = None
        self.right = None
class EvalTree: # inorder representation
    def __init__(self):
        self.root = None
        self.value = 0
        self.treeDepth = 0
        self.treeHeight = 0
        
        temp = Node('+')
        self.root = temp
        
        temp = Node('*')
        self.root.left = temp
        
        temp = Node('-')
        self.root.right = temp
        
        temp = Node('5')
        self.root.left.left = temp
        
        temp = Node('4')
        self.root.left.right = temp
        
        temp = Node('100')
        self.root.right.left = temp
        
        temp = Node('20')
        self.root.right.right = temp
                
    def eval_tree(self, node):
        if node:
            self.eval_tree(node.left)
            self.eval_tree(node.right)
            if node.data == '+':
                node.value = node.left.value + node.right.value
            elif node.data == '-':
                node.value = node.left.value - node.right.value
            elif node.data == '*':
                node.value = node.left.value * node.right.value
            elif node.data == '/':
                node.value = node.left.value / node.right.value
            else:
                node.value = int(node.data)
                print(node.value, end = ' ')
        

c = EvalTree()
c.eval_tree(c.root)
print(c.root.value)

threaded binary tree

threaded binary tree란, 말단 노드에 대해 next node로 연결해준 binary tree의 형태를 말한다. 

 

Binary trees에는 n+1개의 null 링크가 있다. 

이 null 링크를 편리하게 다음 노드를 검색할 수 있도록 변경해준 것이 threaded binary tree이다. 

 

특히 inorder-traversal 시퀀스로 탐색하는 경우, 그 전/다음 노드로 연결을 만들어주는 것이 가능하다. 

 

<단말 노드에게 thread pointer를 만들어주는 법>

 

왼쪽 null link는 previous node에 연결해준다. 오른쪽 null link는 다음 node에 연결해준다. (inorder 기준)

 

Node Field 구성은? 

한 노드의 thread field가 True이면, thread link를 이용하고, 

False이면, normal link로 다음 노드로 이동한다. 

 

Thread link를 쓰면, 탐색하다가 단말 노드를 구분해서 계산할 필요가 없다. 링크의 종류가 Normal link인지, Thread link인지만을 고려해주면 된다.

 

더 이상 탐색할 곳이 없는 '찐' 단말 노드의 경우, root(head)로 연결해준다. 

 

 

<구현>

by following right link from the root, it is possible to traverse the tree by inorder

Tree를 읽을 때 불필요한 방문을 줄여준다.

 

Iterative implementation

* tbt_inorder() => main function

노드를 순서대로 읽는다. 

특정 노드의 왼쪽 link가 비어있다면, Inorder predecessor로 연결해준다.

만약 오른쪽 link가 비어있다면, inorder successor로 연결해준다. 

predecessor or successor가 없다면, root 노드로 연결해준다.

 

* successor() => find a successor node

predecessor : 자신의 왼쪽 subtree 중 가장 오른쪽 노드

successor : 자신의 subtree 중 가장 왼쪽 노드

 

 

    def tbt_inorder(self, node):
        temp = node
        while True:
            # 다음 노드를 찾아 출력한다. 
            temp = self.successor(temp)
            if temp == node:
                break
            print("%3c " % temp.data)
            
    def successor(self, node):
        temp = node.right
        if not (temp.rightFlag or temp.leftFlag):
            while not temp.left_thread:
                temp = temp.left
        return temp

 

=> 시간 복잡도 : O(n)

 

 

heap

Heap이란?

우선순위 큐를 위해 만들어진 자료구조. 

 

우선순위 큐란, 우선순위의 개념을 큐에 도입한 자료 구조를 의미한다. 

데이터들은 우선순위를 가지고 있고, 우선순위가 높은 데이터가 먼저 나간다. 

우선순위 큐는 힙으로 구현하는 것이 가장 효율적이다 ( 삽입 O(logn), 삭제 O(logn) )

 

힙은 CBT Complete Binary Tree의 일종이다. (노드가 0개 혹은 2개를 가짐)

힙은 여러 개의 값 중에서 최댓값 / 최솟값을 빠르게 찾아내도록 만든 자료구조이다. 

힙은 일종의 [반정렬 상태]를 유지한다. 

이때 반정렬 상태는 부모 노드의 값 >= 자식 노드의 값인 정도로 정렬하는 것을 말한다. 

힙은 중복된 값을 허용한다. 

 

heap의 종류

<max heap>

부모 노드의 키 값 >= 자식 노드의 키 값인 CBT

 

<min heap>

max heap과 반대로, 

부모 노드의 키 값 <= 자식 노드의 키 값인 CBT

 

heap으로 구현하는 priority Queue

Queue는 FIFO 자료구조인데,우선순위 큐는 말 그대로 큐에 우선순위를 부여해서 우선순위가 높은 요소를 빨리 내보내는 자료구조를 말한다. 

 

그리고 우선순위 큐는 heap으로 구현하는 것이 가장 효율적이다. heap은 어떤 노드를 삽입할 때마다 재정비가 필요한데, 이 시간은 logn 즉, tree의 높이에 비례한다. 

Data Structure Insert Delete
Unordered array O(1) O(n)
Unordered linked list O(1) O(n)
Sorted array O(n) O(1)
Sorted Linked list O(n) O(1)
Max heap O(logn) O(logn)
Min heap O(logn) : tree height O(logn) : tree height

 

heap 연산

<insertion to heap>

step 0. 노드를 힙의 마지막 노드에 이어서 삽입한다. 

step 1. 부모 노드와 교환하면서 힙의 성질(부모 <= 자식 계층적 관계, CBT 노드당 0/2개)을 만족할때까지 반복한다. 

class Heap:
    def __init__(self, size):
        self.size = size
        self.heap = [None] * size
        self.count = 0
    def isEmpty(self):
        return self.count == 0
    def isFull(self):
        return self.count == self.size
    def add_heap(self, item):
        if self.isFull():
            return
        
        i = self.count
        # empty heap이 아니고, 
        # item이 부모 노드보다 큰 경우에는, 
        while i != 0 and item > self.heap[i//2]:
            # temp 자리에 부모 노드의 값을 저장해두고
            self.heap[i] = self.heap[i//2]
            # 다시 부모 노드와 부부모 노드를 비교하는 것을 반복한다. 
            i//=2
        # 마침에 부모 노드 >= 자식 노드의 관계가 성립한다면, 
        # i번째 item으로, item을 저장한다. 
        self.heap[i] = item
        self.count += 1
        print("HI1")
        print("%2d " % item, end=' ')
        print(self.heap)

c = Heap(10)
c.heap = [20,15,14,10,2,None, None, None, None, None]
c.count = 5
c.add_heap(5)
print(c.heap)

=> 시간 복잡도는 O(log2n)

 

<deletion from heap>

> 목표 : 최댓값인 root 값을 힙에서 지우고, 힙을 재정비한다. 

> 알고리즘

step 0. return하는 값인 root를 저장해두고 노드를 삭제한다. 

step 1. 빈 root의 자리에 말단 노드를 임의로 저장해둔다. 

step 2. 임시 root부터 차례로 탐색하면서, 임시 노드가 자식 노드보다 작은 경우가 있다면, swap(부모, 자식)을 해준다. 

step 3. 임시 root가 자신의 자리를 찾았으면, 함수를 종료한다. 

> 구현

    def del_heap(self):
        if self.count == 0:
            print("Empty heap")
            return
        # root를 제거한다. 
        item = self.heap[0]
        temp = self.heap[self.count]
        self.heap[self.count] = None
        self.count -= 1
        
        parent = 1
        child = 2
        # rebuilding heap
        while child <= self.count:
            if child < self.count and \
                self.heap[child] < self.heap[child+1]:
                    # child < parent
                    child += 1
            try:
                if temp >= self.heap[child]:
                    # 재구성 완료
                    break
            except:
                break
            # swap 이 필요한 경우
            self.heap[parent] = self.heap[child]
            parent = child
            child *= 2
        if self.count != 0:
            # 마지막 탐색 위치에 temp 저장
            self.heap[parent] = temp
        return item
    
c = Heap(10)
c.heap = [None,20,15,14,10,2, None, None, None, None]
c.count = 5
c.add_heap(5)
c.del_heap()
print(c.heap)

 

 

=> 시간 복잡도는 O(log2n)

 

heap sort

<heap을 max heap으로 정렬하는 방법>

step 0. 현재 원소로 binary tree / max heap를 생성한다. 

step 2. max heap의 루트 노드(최댓값)과 말단 노드를 heap에 맞게 swap해준다.

step 3. 새 루트 노드에 대해 max heap을 rebuilding 해준다. 

 

    def heapSort(self):
        n = self.size - 1
        for i in range(n//2, 0, -1):
            self.makeheap(i, n)
        print(self.num)
        for i in range(n-1, 0, -1):
            self.swap(1, i+1)
            self.makeheap(1, i)
            print(self.num)
    def makeheap(self, root, n):
        temp = self.num[root]
        child = 2 * root
        while child <= n:
            if child < n and self.num[child] < self.num[child + 1]:
                child += 1
            if temp > self.num[child]:
                break
            else:
                self.num[child//2] = self.num[child]
                child * 2
        self.num[child//2] = temp

 

=> 시간 복잡도 : O(nlogn)

* O(n) : 최초로 max heap을 만드는 과정

* n * O(log2n) : 각 element에 대하여 heapify() 해주는 과정