# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        if not nums:
            return None
        
        mid = len(nums) // 2
        
        root = TreeNode(nums[mid])
        root.left = self.sortedArrayToBST(nums[:mid])
        root.right = self.sortedArrayToBST(nums[mid+1:])
        
        return root

 

배열이 먼저 정렬되어 있어야 한다.

 

이진 검색을 하는 것과 유사하게 구현한다. 숫자 리스트 중 중앙값보다 작은 값이 왼쪽, 중앙값보다 큰 값이 오른쪽으로 오게 하고, nums의 범위를 변경함으로써 재귀 탐색을 한다.

 

// 내림 연산자를 사용하여 중앙값을 탐색할 수 있게 한다.

 

 

'자료구조와 알고리즘 > leetcode' 카테고리의 다른 글

110. Balanced Binary Tree  (1) 2022.05.01
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        
        def check(node):
            if not node:
                return 0
            
            left = check(node.left)
            right = check(node.right)
            
            if left == -1 \
                or right == -1 \
                or abs(left-right) > 1:
                return -1
            
            return max(left, right) + 1
    
        return check(root) != -1

 

체크포인트

  • leaf 노드 탐색을 if not node (None인지 체크)로 수행
  • leaf 노드 도달 시 탐색를 종료하는 동시에 상태값을 0으로 저장
  • 현 노드 상태값은 max(left, right) + 1로 저장

1. 해시 테이블(Hash Table)이란

1.1 정의 및 특징

해시 테이블은 키(key)를 값(value)에 매핑할 수 있는 구조인, 연관 배열 추상 자료형을 구현하는 자료구조다.

key에 해시함수를 적용하여 계산한 값을 색인(index)으로 삼아 데이터를 저장한다.

create, read, update, delete 연산이 가능하다.

충돌의 문제가 없다면 1:1 매핑 구조를 띠므로 시간복잡도는 \(O(1)\)이다.

 

1.2 버킷(bucket)과 슬롯(slot)

해시 테이블에서 값은 버킷(bucket)과 슬롯(slot)으로 이루어진 형태의 테이블에 저장된다.

(이 테이블 자체를 버킷이라고 표현 하기도 함)

  • 버킷(bucket) : 여러개의 슬롯을 저장하는 레코드 형태의 자료 구조로, 버킷의 갯수는 고정적이다.
  • 슬롯(slot) : 해시와 매핑되는 값이 저장되어 있는 공간으로, 하나의 버킷 안에는 여러개의 슬롯이 존재할 수 있다. 슬롯은 하나 이상의 값을 저장할 수 있다.

 

2. 해시함수

해시테이블에서 중요한 것은 키마다 고유한 인덱스를 설정하는 것이다. 이때 해시함수가 중요한 역할을 한다.

해시함수는 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑해주는 함수인데 임의의 데이터마다 고유한 결과값을 계산한다.(이러한 해시함수의 특성을 이용하므로 1대1 매핑이 가능해진다.)

이때 서로 다른 입력값의 해시함수 결과값이 같을 경우 충돌(Collision)이 발생했다고 한다.

 

좋은 해시함수는 아래와 같은 조건을 가진다.

  • 해시함수 값 충돌의 최소화
  • 쉽고 빠른 연산
  • 해시테이블에 값이 고르게 분포
  • 사용할 키의 모든 정보를 사용
  • 해시테이블 사용 효율이 높을 것

 

3. 충돌과 해결 방안

해시 테이블 개념 자체는 이해하기 어렵지 않다. 충돌이 발생할 때 어떻게 해결하느냐가 중요한 문제가 된다.

아무리 우수한 해시함수라도 충돌은 필연적으로 일어난다. 충돌을 설명할 때 드는 가장 흔한 예시가 '생일 문제'이다. 모인 사람 중 생일이 같은 사람이 2명 이상 있을 확률이 23명이 모이면 50%를 넘고, 57명이 모이면 99%를 넘는다. 이처럼 충돌은 생각보다 흔한 일이다. 

또한 해시테이블이 가장 이상적으로 구성될 경우 탐색, 삽입, 수정, 삭제가 모두 \(O(1)\)에 가능하지만 충돌이 발생하여 만약 모든 데이터가 같은 버킷에 저장되었다고 한다면 최악의 경우 \(O(n)\)이 소요된다. 이와 같이 충돌은 해시테이블의 성능을 좌우하는 중요한 요소이다.

 

적재율(load factor)

해시함수를 다시 작성할지, 해시테이블의 크기를 조정해야 할지 결정할 때 적재율이라는 개념을 사용한다.

 

적재율은 다음과 같이 산출된다. 

\( load factor = \frac {n}{k} \)

여기서 n는 해시테이블에 저장된 데이터 개수, k는 버킷의 개수이다.

 

이 적재율은 해시함수가 키들을 잘 분산했는지 말하는 효율성 측정에도 사용된다.

적재율이 1이 넘으면 반드시 충돌이 발생한다.

 

개별 체이닝(seperate chaining)

충돌이 일어나면 연결리스트로 연결하는 방식이다.

John Smith와 Sandra Dee가 충돌이 발생했다. 이때 John smith를 저장하고, 그에 Sandra Dee를 연결리스트로 연결한다. 

 

1. 키의 해시 값을 계산한다.

2. 해시값을 이용해 배열의 인덱스를 구한다.

3. 같은 인덱스가 있다면 연결리스트로 연결한다.

 

잘 구현한 경우 \(O(1)\)이지만 최악의 경우 \(O(n)\)이다. 

 

오픈 어드레싱(open addressing)

오픈 어드레싱

그림출처

충돌이 발생하면 탐사(probing)을 통해 빈 공간을 찾아 저장하는 방식이다.

모든 값이 해시값과 일치하는 공간에 저장되지 않을 수 있는 것이다.

 

오픈어드레싱의 한 가지 방식 중 선형 탐사가 있다.  

충돌이 발생할 경우 해당 위치부터 순차적으로 탐사를 하나씩 진행한다. 

특정위치가 선점되어 있으면 바로 그 다음 위치를 확인하는 식이다. 구현이 간단하고 전체적 성능이 좋은 편이다. 

그러나 데이터가 고르게 분포되지 않아 전체적으로 해싱 효율을 떨어트릴 수 있다.

 

로드팩터를 넘어서게 되면 그로스 팩터의 비율에 따라 더 큰 크기의 버킷을 생성한 후 새롭게 복사하는 리해싱 작업이 일어난다.

 

언어별 해시테이블 구현

개별체이닝은 체이닝 시 malloc으로 메모리를 할당하여야 하므로 오버헤드가 높아지는 단점이 있다.

오픈 어드레싱은 그에 비해 일반적으로 체이닝에 비해 성능이 더 좋으나 슬롯이 80퍼센트 이상 차게 되면 성능 저하가 일어나고, 로드팩터 1이상은 저장할 수 없다. 최근 루비나 파이썬 같은 언어들은 오픈 어드레싱 방식을 택해 성능을 높이고, 로드 팩터를 작게 잡아 성능 저하 문제를 해결한다.

 

언어 방식
C++ 개별체이닝
자바 개별체이닝
개별체이닝
루비 오픈어드레싱
파이썬 오픈어드레싱

'자료구조와 알고리즘 > 자료구조' 카테고리의 다른 글

[자료구조] 연결 리스트(Linked List)  (0) 2022.04.04
[자료구조] 힙(Heap)  (0) 2022.04.02
[자료구조] 트리(Tree)  (0) 2022.04.02

1. 연결리스트(Linked List)란

각 노드가 데이터와 포인터를 가지고, 각 노드의 포인터가 다음 노드를 가리키게 하여 한 줄로 연결되도록 데이터를 저장하는 자료구조이다.

연결리스트 추상도

가장 첫 노드는 head, 마지막 노드느 tail이라고 한다.

각 노드의 앞쪽 노드를 전임 노드 (Predecessor Node), 뒤쪽 노드를 후임 노드 (Successor Node) 라고 한다

 

2. 연결리스트 특징(사용하는 이유)

  • 데이터를 연속적인 물리 메모리에 저장하지 않아도 된다.
  • 배열은 크기가 가변적이지 않기 때문에, 지정한 크기를 넘어가게 될 경우 메모리를 재할당하고, 배열 데이터를 복사하는 작업이 일어나게 되는데 연결리스트는 이러한 작업 없이 독립된 공간에 새로운 데이터를 할당하고 연결만 해주면 된다.
  • 탐색은 \(O(n)\), 시작 및 끝 지점에 데이터 삽입/추출 작업은 \(O(1)\)이 소요된다.
  • 탐색을 자주 해야 할 경우에는 배열, 데이터 삽입/추출을 자주 해야 할 경우에는 연결 리스트를 사용하는 것이 유리할 것이다.

 

3. 연결리스트 유형

1. 단일 연결 리스트(singly linked list)

후임 노드에 대한 포인터만을 갖는 가장 단순한 연결 리스트 형태이다.  큐를 구현할 때 사용하는 방식이다.

 

2. 이중 연결 리스트(doubly linked list)

단일 연결 리스트는 후임 노드에 대한 포인터만을 갖는 것에 비해, 이중 연결 리스트는 전임, 후임 양 방향에 대한 포인터를 갖고 있다.

 

3. 환형 연결리스트(circular linked list)

단일 연결리스트에서 마지막 노드(tail)가 첫 노드(head)를 가리키는 연결리스트를 말한다. 

 

4. 연결리스트 구현

class ListNode:
    def __init__(self, data, next=None) -> None:
        self.data = data
        self.next = next
    
class SinglyLinkedList:
    def __init__(self) -> None:
        self.head = ListNode(None)
        self.size = 0
        
    def size(self):
        return self.size
    
    def get_node(self, idx):
        if self.size == 0:
            print("It's empty Linked List")
            return
        elif idx + 1 > self.size:
            print("idx is bigger than size")
            return
        
        node = self.head
        for _ in range(idx):
            node = node.next
        return node
    
    def append(self, data):
        if self.size == 0:
            self.head = ListNode(data)
        else:
            node = self.get_node(self.size-1)
            node.next = ListNode(data)
        self.size += 1
    
    def appendleft(self, data):
        if self.size == 0:
            self.head = ListNode(data)
        else:
            node = ListNode(data)
            node.next = self.head
            self.head = node
        
        self.size += 1
    
    def insert(self, data, idx):
        if idx > self.size:
            print("out of range")
            return

        elif idx == 0:
            self.appendleft(data)
        
        else:
            node = self.get_node(idx-1)
            successor = node.next    
            node.next = ListNode(data)
            node.next.next = successor
            self.size += 1
        
    def pop(self):
        out = None
        if self.size == 0:
            print("It's empty Linked List")
            return
        
        elif self.size == 1:
            out = self.head.data
            self.head = ListNode(None)
            
        else:
            node = self.get_node(self.size-2)
            out = node.next.data
            node.next = None

        self.size -= 1
        return out        
            

    def popleft(self):
        if self.size == 0:
            print("It's empty Linked List")
            return
        
        elif self.size == 1:
            out = self.head.data
            self.head = ListNode(None)
        
        else:
            node = self.head
            out = node.data
            self.head = node.next

        self.size -= 1
        return out
        
    
    def delete(self, idx):
        if self.size == 0:
            print("It's empty Linked List")
            return
        
        elif idx >= self.size:
            print("out of range")
            return

        elif self.size == 1:
            self.popleft()
        
        else:
            node = self.get_node(idx-1)
            node.next = node.next.next
        
        self.size -= 1
        return
    
    def print(self):
        node = self.head
        while node:            
            if node.next is not None:
                print(f'{node.data}->', end="")
            else:
                print(f'{node.data}')
            node = node.next
        

linked_list = SinglyLinkedList()
linked_list.print()

linked_list.appendleft(2)
linked_list.print()
linked_list.append(1)
linked_list.print()
linked_list.popleft()
linked_list.print()
linked_list.insert(3,1)
linked_list.print()
linked_list.insert(2,1)
linked_list.print()
linked_list.popleft()
linked_list.print()
linked_list.pop()
linked_list.print()

 

'자료구조와 알고리즘 > 자료구조' 카테고리의 다른 글

[자료구조] 해시테이블  (0) 2022.04.07
[자료구조] 힙(Heap)  (0) 2022.04.02
[자료구조] 트리(Tree)  (0) 2022.04.02

1. 다익스트라(Dijkstra) 알고리즘이란

  • 음의 가중치가 없는 그래프의 한 정점에서 다른 정점까지의 최단 경로(거리)를 구하는 알고리즘이다.
  • 음의 가중치가 있을 때는 벨만-포드 알고리즘을 사용할 수 있다.
  • 가능한 모든 노드쌍의 최단 거리를 구할 때는 플로이드-워셜 알고리즘을 사용한다.
  • BFS를 기본으로 한다.

 

2. 다익스트라 알고리즘 동작 방식

  1. 출발 노드로부터 각 노드까지 최단 경로를 저장할 배열 distance[v]를 초기화하고, 출발 노드의 인덱스에는 0, 다른 노드에는 매우 큰 값을 저장한다(무한에 근접하도록 distance의 원소 타입의 최댓값으로 설정하면 된다).
  2. 현재 노드를 뜻하는 노드 A에 출발 노드 번호를 저장한다.
  3. A로부터 갈 수 있는 임의의 노드 B에 대해, A까지 최단거리(distance[A]) + A에서 B까지의 거리(graph[A][B])와 기존에 계산한 B까지의 최단거리(distance[B])를 비교한다. (min(distance[A]+graph[A][B], distance[B])) INF와 비교할 경우 무조건 전자가 작다.
  4. 만약 전자가 더 작다면, 즉 더 짧은 경로일 경우 distance[B]를 갱신한다.
  5. A의 모든 인접 노드에 대해 이 작업을 수행한다.
  6. 모든 인접 노드에 대해 위 작업을 마쳤으면, A를 '방문 완료'로 처리하여 중복 탐색을 방지한다.
  7. '미방문' 상태인 모든 노드들 중, 출발점으로부터 거리가 가장 짧은 노드를 하나 골라서 그 노드를 A에 저장한다.
  8. 도착 노드가 '방문 완료' 상태가 되거나, 혹은 더 이상 미방문 상태의 노드를 선택할 수 없을 때까지, 3~7을 반복한다.

이 작업을 마친 뒤, 도착 노드에 저장된 값(distance[end])이 바로 A로부터의 최단 거리이다. 만약 이 값이 INF라면, 중간에 길이 끊긴 것임을 의미한다.

 

 

3. 구현(파이썬)

3.1 단순한 방식

def dijkstra(start, n):
    infos = [[1, 2, 2], [1, 3, 1], [2, 4, 1], [2, 5, 2], [3, 4, 3], [4, 5, 1], [5, 6, 1]]
    INF = 10e9
    distance = [INF] * (n + 1)
    graph = [[] for i in range(n + 1)]
    visited = [False] * (n + 1)  
	
    for v, w, u in infos:
        graph[v].append((w, u))
        
    for v, w in graph[start]:
        distance[v] = w

    def get_smallest_node():
        smallest_value = INF
		index = 1
        for i in range(1, n+1):
            if distance[i] < smallest_value and not visited[i]:
                smallest_value = distance[i]
                index = 0
        return index
        
        
    for _ in range(n-1):
        now = get_smallest_node()
        visited[now] = True
        for child in graph[now]:
            c_node = child[0]
            cost = distance[now] + child[1]
            if cost < distance[c_node]:
                distance[c_node] = cost

이 경우 시간복잡도는 \(O(v^2)\)이다.

 

 3.2 우선순위큐 이용

from collections import defaultdict
import heapq

infos = [[1, 2, 2], [1, 3, 1], [2, 4, 1], [2, 5, 2], [3, 4, 3], [4, 5, 1], [5, 6, 1]]
INF = 10e9
distance = defaultdict(int)
graph = defaultdict(list)

Q = []

for u, v, w in infos:
    graph[u].append([v, w])
 
Q.append((0, start))

while Q:
    current_cost, current_node = heapq.heappop(Q)
    if  distance[current_node] < current_cost:
        continue
    for child in graph[current_node]:
        child_node = child[0]
        child_cost = child[1]
        alt_cost = current_cost + child_cost
        if alt_cost < distance[child_node]:
            heapq.heappush(Q, (alt_cost, child_node))

print(distance)

 

※ 우선순위 큐를 활용한 다익스트라 알고리즘의 시간복잡도

우선순위 큐를 사용할 경우 위 동작방식 7번에서 시간복잡도를 크게 줄일 수 있다.

우선순위 큐를 사용하면 한번 처리된 노드는 더이상 처리되지 않는다. 즉 노드를 하나씩 꺼내 검사하는 반복문(while문)은 노드의 개수 V 이상의 횟수로는 반복되지 않는다. 또한 V번 반복될 때마다 각각 자신과 연결된 간선들을 모두 확인한다. 따라서 현재 우선순위 큐에서 꺼낸 노드와 연결된 다른 노드들을 확인하는 총 횟수는 총 최대 간선의 개수 E만큼 연산이 수행될 수 있다. 일반적으로 힙연산의 시간복잡도는 \(O(logn)\)이고 \(O(logE)\)를 E개의 원소에 대하여 반복하는 것으로 볼 수 있으므로 시간복잡도는 \(O(ElogE)\)로 표현할 수 있다. 

이때 중복 간선을 포함하지 않는 경우, E는 항상 V의 제곱보다 작다. 왜나하면, 모든 노드끼리 서로 다 연결되어 있다고 했을 때 간선의 개수를 약 \(V^2\)으로 볼 수 있고 항상 E는 \(V^2\)이하이기 때문이다.

\(O(ElogE)\) <= \(O(ElogV^2)\)이고, \(O(ElogV^2)\)는 \(O(2*ElogV)\)이므로 \(O(ElogV)\)로 표현할 수 있다.

 

1. 힙이란

힙은 완전 이진트리를 기반으로 하는 자료구조로서, 여러 값 중에서 최댓값이나 최솟값을 빠르게 찾기 위하여 고안되었다.

우선순위 큐에 활용된다.

 

2. 힙의 특징

  • 힙은 최소 힙과 최대 힙으로 나눠지며 각각 아래의 특성을 만족한다.
    • 최소 힙(Min Heap)의 경우 부모가 자식보다 작거나 같다.
    • 최대 힙(Max Heap)의 경우 부모가 자식보다 크거나 같다.
  • 중복 값을 허용한다.
  • 정렬된 구조가 아니다. 최소 힙의 경우 부모가 자식보다 작거나 같다는 조건만 만족할 뿐 정렬되어 있지 않다. 예를 들어 오른쪽 자식 노드가 레벨 차이에도 불구하고 왼쪽 노드보다 더 작은 경우도 있을 수 있다. 
  • 자식이 둘인 힙은 특별히 이진 힙이라고 부르고 자주 사용된다.

 

3. 힙의 구현

  • 힙은 주로 배열로 구현한다.
  • 직관적이고 쉽게 구현하기 위하여 인덱스 0은 사용하지 않고 1부터 사용한다.
  • 부모 노드와 자식 노드의 관계
    • 왼쪽 자식의 인덱스 = 부모의 인덱스 * 2
    • 오른쪽 자식의 인덱스 = 부모의 인덱스 * 2 + 1
    • 부모의 인덱스 = 자식의 인덱스 // 2

3.1 힙의 삽입

  1. 요소를 가장 하위레벨 최대한 왼쪽으로 삽입(배열로 표현할 경우 가장 마지막에 삽입)
  2. 부모값과 비교해 값이 더 작은 경우 위치 변경
  3. 계속해서 부모 값과 비교해 위치 변경

※ 시간복잡도는 O(logn)

 

 

3.2 힙의 추출(삭제)

  1. 루트를 추출한다.
  2. 추출 이후 아래의 과정으로 다시 힙의 특성을 유지한다.
  3. 추출 이후 비어 있는 루트에 가장 마지막 요소가 올라간다.
  4. 자식 노드와 값을 비교해서 자식보다 크다면 내려가는 과정을 반복한다.

※ 시간복잡도는 단순히 추출은 O(1)이나, 힙 유지에 O(logn) 소요

 

3.3 파이썬으로 이진 힙 구현

class BinaryHeap(object):
    def __init__(self):
        self.items = [None] #don't use index 0 
    
    def __len__(self):
        return len(self.items) - 1
    
    # when, number k is inserted, swap when it is smaller than parent
    def _percolate_up(self):
        i = len(self)
        parent = i // 2
        while parent > 0:
            if self.items[i] < self.items[parent]:
                self.items[parent], self.items[i] = self.items[i], self.items[parent]
            i = parent
            parent = i // 2
   
    # append number k and percolate_up
    def insert(self, k):
        self.items.append(k)
        self._percolate_up()
    
    
    # when extracting first item, move last item to first and swap with child when it is smaller than child
    def _percolate_down(self, idx):
        left = idx * 2
        right = idx * 2 + 1
        smallest = idx
        
        if left <= len(self) and self.items[left] < self.items[smallest]:
            smallest = left
        
        if right <= len(self) and self.items[right] < self.items[smallest]:
            smallest = right
        
        if smallest != idx:
            self.items[idx], self.items[smallest] = self.items[smallest], self.items[idx]
            self._percolate_down(smallest)
            
    def extract(self):
        extracted = self.items[1]
        self.items[1] = self.items[len(self)]
        self.items.pop()
        self._percolate_down(1)
        return extracted

 

 

 

 

 

1. 트리의 개념

트리란 노드가 나뭇가지처럼 연결된 비선형 계층적 자료구조다.

그래프의 일종으로, 순환 구조를 갖지 않는 그래프라는 점이 가장 큰 특징이다.

 

 

2. 트리의 구성요소

트리의 구성요소

1) 간선(edge, branch) : 노드를 잇는 선

2) 루트 노드(root node) : 시작 노드. 부모 노드가 없고 트리에서 유일하다.

3) 리프 노드(leaf node) : 자식이 없는 노드. 단말 노드, 말단 노드라고도 한다. 트리는 나무를 뒤집어 놓은 듯한 모양이기 때문에 뒤집은 나무에서 잎의 위치를 생각하면 이해하기 쉽다.

4) 내부 노드(internal node) : 리프 노드가 아닌 노드

5) 부모 노드(parent node) : 자식(child) 노드를 가진 노드

6) 형제(siblings) : 같은 부모 노드를 공유하는 노드

 

7) 노드의 크기(size) : 자신을 포함한 모든 자손 노드의 개수

8) 노드의 깊이(depth) : 루트에서 어떤 노드에 도달하기 위해 거쳐야 하는 간선의 개수

9) 노드의 레벨(level) : 특정 깊이를 가지는 노드의 집합. 0부터 시작한다.

10) 노드의 차수(degree) : 하위 트리 개수(간선의 개수)

11) 트리의 차수(degree of tree) : 트리의 최대 차수

12) 높이(height) : 루트 노드에서 가장 깊은 곳에 있는 노드의 깊이 

 

 

3. 트리의 특징

  • 하나의 루트 노드와 0개 이상의 하위 트리로 구성된다.
  • 데이터를 순차적으로 저장하지 않으므로 비선형 자료구조이다.
  • 트리내에 또 다른 트리가 있는 재귀적 자료구조이다.
  • 그래프의 일종으로, 순환(Loop)구조를 갖지 않고 연결된 무방향 그래프 구조이다.
  • 노드 간에 부모 자식 관계를 갖고 있는 계층형 자료구조이며 모든 자식 노드는 하나의 부모 노드만 갖는다.
  • 노드가 n개인 트리는 항상 n-1개의 간선(edge)을 가진다.
  • 루트에서 어떤 노드로 가는 경로는 유일하다.

 

 

4. 트리의 종류

트리에는 트리, 이진 트리, 이진 탐색 트리, 균형 트리, 비균형 트리, 정 이진 트리, 완전 이진 트리, 포화 이진 트리 등이 있다.

4.1 트리와 이진 트리 (Binary Tree)

트리의 각 노드가 m개 이하의 자식을 가지고 있으면 그 트리는 m-ary트리(다진 트리)라고 부른다.

m이 2일 경우, 즉 모든 노드의 차수가 2이하일 때를 특별히 이진 트리라고 한다.

트리의 한 종류이고 모든 트리가 이진 트리는 아니다.

 

4.2 이진 트리(Binary Tree)와 이진 탐색 트리(Binary Search Tree, BST)

이진 탐색 트리는 정렬된 이진 트리다.

'모든 왼쪽 자식 노드 <= n < 모든 오른쪽 자식 노드' 를 만족한다.

탐색 시간 복잡도가 O(logn)이다.

 

4.3 균형 트리와 비균형 트리

O(logn)에 탐색 및 삽입이 가능할 정도로 잘 균형이 잡혀있는 경우(균형이 깨지면 최악의 경우 O(n))

* 자가 균형 이진 탐색 트리: 삽입 삭제 시 자동으로 높이를 작게 유지하는 이진 탐색 트리로, AVL 트리, 레드-블랙 트리 등이 있다.

 

4.4 정이진트리(Full Binary Tree), 완전이진트리(Complete Binary Tree), 포화이진트리(Perfect Binary Tree)

  • 정이진트리(Full Binary Tree) : 모든 노드가 0개 또는 2개의 자식 노드를 갖는다.
  • 완전이진트리(Complete Binary Tree) : 마지막 레벨을 제외하고 모든 레벨이 완전히 채워져 있고, 마지막 레벨의 모든 노드는 가장 왼쪽부터 채워져 있다.
  • 포화이진트리(Perfect Binary Tree): 모든 노드가 2개의 자식 노드를 갖고 있고, 모든 리프 노드가 동일한 깊이 또는 레벨을 갖는다.

 

힙(Heap), 트라이(Trie) 등이 대표적인 트리 자료구조이다.

 

그래프 탐색 알고리즘 중 DFS에 이어 BFS에 대해 포스팅한다.

 

DFS(깊이 우선 탐색) 알고리즘

1. DFS 알고리즘이란 그래프 탐색 알고리즘의 한 종류 그래프 탐색이란 한 정점으로부터 시작하여 다른 정점을 차례대로 모두 한번씩 방문하는 것 DFS는 Depth First Search의 약자로서, 임의의 정점에

heero.tistory.com

 

1. BFS(너비 우선 탐색) 알고리즘이란?

  • 그래프 탐색 알고리즘의 한 종류
  • 그래프 탐색이란 한 정점으로부터 시작하여 다른 정점을 차례대로 모두 한번씩 방문하는 것
  • DFS가 임의의 정점에서 시작하여 다른 분기로 넘어가기 전에 해당 분기를 완전히 탐색하는 방법이라면
  • BFS는 임의의 정점에서 시작하여 다음 레벨로 진입하기 전 인접한 분기를 모두 탐색하는 방법

2. BFS의 특징

  • 깊게 탐색하기 전에 넓게 탐색
  • BFS는 재귀로 구현할 수 없음
  • 방문한 노드를 저장하고 차례대로 꺼내면서 탐색하므로 큐(선입선출)를 사용하여 구현함

3. BFS의 장점

  • 가장 먼저 구한 해가 최단 경로임을 보장
  • 최단 경로가 존재한다면 특정 경로의 길이가 무한이라도 최단 경로를 찾을 수 있음

4. BFS의 단점

  • 큐에 탐색할 노드를 저장하므로 저장공간이 많이 소요
  • 노드가 많을수록 효율이 떨어짐 

5. BFS를 써야 하는 문제

 1) 두 노드 사이의 최단 경로 구하기 또는 임의의 경로 구하기

 2) 검색 대상의 규모가 크지 않은 경우, 검색 대상이 시작 지점으로부터 멀지 않은 경우

 3) 대상을 검색해야 하는 경우(조건을 만족하는 순회에 초점을 둔 DFS보다 유리)

 

6. 구현

스택과 재귀 두가지 방법으로 구현할 수 있었던 DFS와 달리 BFS는 큐를 사용한 반복구조로 구현하여야만 한다.

import collections
queue = collections.deque()
def bfs(start_node):
    visited = [start_node]
    queue.append(start_node)
    while queue:
        parent = queue.popleft()
        for child in graph[parent]:
            if child not in visited:
                queue.append(child)
                visited.append(child)
	return visited

참고)

collections 모듈에 구현된 deque자료형을 사용하였다.

리스트로도 구현할 수 있으나 큐의 연산을 구현하기 위해서는 pop(0)를 사용하여야 한다.

리스트의 pop연산은 리스트의 요소를 전부 복사하여야 하므로 시간 복잡도가 O(n)이다.

따라서 파이썬에서 queue를 성능저하 없이 사용하기 위해서는 deque자료형을 사용하고, 이 경우 O(1)에 연산이 가능하다.

+ Recent posts