방명록
- 큐, 덱, 이진 탐색 트리, 우선 순위 큐, 그래프2024년 02월 06일 07시 03분 50초에 업로드 된 글입니다.작성자: 재형이반응형
- 자료구조는 이틀만에 끝나네요
- 벌써 6일차입니다ㅎㅎ
- 그럼 오늘도 출발~!
1. 큐 (Queue)
- 큐(queue)는 먼저 삽입된 데이터가 먼저 추출되는 자료구조(data structure)다
예시) 게임 대기 큐는 먼저 대기한 사람이 먼저 게임에 매칭된다
1-1. 연결 리스트로 큐 구현하기
- 큐를 연결 리스트로 구현하면, 삽입과 삭제에 있어서 𝑂(1)을 보장할 수 있다
- 연결 리스트로 구현할 때는 머리(head)와 꼬리(tail) 두 개의 포인터를 가진다
- 머리(head) : 남아있는 원소 중 가장 먼저 들어 온 데이터를 가리키는 포인터
- 꼬리(tail) : 남아있는 원소 중 가장 마지막에 들어 온 데이터를 가리키는 포인터
1-2. 파이썬으로 큐 구현해보기
class Node: def __init__(self, data): self.data = data self.next = None class Queue: def __init__(self): self.head = None self.tail = None def enqueue(self, data): node = Node(data) if self.head == None: self.head = node self.tail = node # 꼬리(tail) 위치에 새로운 노드 삽입 else: self.tail.next = node self.tail = self.tail.next def dequeue(self): if self.head == None: return None # 머리(head) 위치에서 노드 꺼내기 data = self.head.data self.head = self.head.next return data def show(self): cur = self.head while cur: print(cur.data, end=" ") cur = cur.next queue = Queue() data_list = [3, 5, 9, 8, 5, 6, 1, 7] for data in data_list: queue.enqueue(data) print("\n전체 노드 출력:", end=" ") queue.show() print("\n[원소 삭제]") print(queue.dequeue()) print(queue.dequeue()) print(queue.dequeue()) print("[원소 삽입]") queue.enqueue(2) queue.enqueue(5) queue.enqueue(3) print("전체 노드 출력:", end=" ") queue.show()
- Python의 리스트 자료형을 사용하여 큐를 구현할 수도 있지만 비효율적이다
- 큐(queue)의 구현 방식에 따른 연산 속도를 비교
import time data_list = [i for i in range(100000)] start_time = time.time() queue = [] for data in data_list: queue.append(data) while queue: queue.pop(0) print(f"Elapsed time: {time.time() - start_time} seconds.") print(queue) start_time = time.time() queue = Queue() for data in data_list: queue.enqueue(data) while queue.head != None: queue.dequeue() print(f"Elapsed time: {time.time() - start_time} seconds.") queue.show()
- 4배 가량 속도 차이가 나는 것을 확인할 수 있다
2. 덱 (Deque)
- 덱은 스택(stack)과 큐(queue)의 기능을 모두 가지고 있다
- 포인터 변수가 더 많이 필요하기 때문에, 메모리는 상대적으로 더 많이 필요하다
- Python에서는 큐(queue)의 기능이 필요할 때 간단히 덱(deque)을 사용한다
- 데이터의 삭제와 삽입 모두에서 𝑂(1)의 시간 복잡도가 소요된다
- 덱에서는 좌측 또는 우측 양 끝에서 자유롭게 데이터를 삽입하고 삭제할 수 있다
2-1. 파이썬의 덱(Deque) 라이브러리
- Python에서는 덱(deque) 라이브러리를 사용할 수 있다
- 아래의 모든 메서드는 최악의 경우 시간 복잡도 𝑂(1)을 보장한다
- 우측 삽입: 𝑎𝑝𝑝𝑒𝑛𝑑()
- 좌측 삽입: 𝑎𝑝𝑝𝑒𝑛𝑑𝑙𝑒𝑓𝑡()
- 우측 추출: 𝑝𝑜𝑝()
- 좌측 추출: 𝑝𝑜𝑝𝑙𝑒𝑓𝑡()
2-2. 연결 리스트로 덱 구현하기
- 덱(deque)을 연결 리스트로 구현하면, 삽입과 삭제에 있어서 𝑂(1)을 보장할 수 있다
- 연결 리스트로 구현할 때는 앞(front)과 뒤(rear) 두 개의 포인터를 가진다
- 앞(front) : 가장 좌측에 있는 데이터를 가리키는 포인터
- 뒤(rear) : 가장 우측에 있는 데이터를 가리키는 포인터
2-3. Python에서 덱(Deque)을 사용하는 경우
- 기본적인 Python의 리스트 자료형은 큐(queue)의 기능을 제공하지 않는다
- 가능하다면 Python에서 제공하는 덱(deque) 라이브러리를 사용한다
- 큐(queue)의 기능이 필요할 때는 덱 라이브러리를 사용하는 것을 추천한다
- 삽입과 삭제에 대하여 모두 시간 복잡도 𝑂(1)이 요구된다
2-4. 파이썬으로 덱(Deque) 구현해보기
from collections import deque d = deque() arr = [5, 6, 7, 8] for x in arr: d.append(x) arr = [4, 3, 2, 1] for x in arr: d.appendleft(x) print(d) while d: print(d.popleft()) arr = [1, 2, 3, 4, 5, 6, 7, 8] for x in arr: d.appendleft(x) print(d) while True: print(d.pop()) if not d: break print(d.popleft()) if not d: break
- 연결 리스트를 이용한 덱 구현
class Node: def __init__(self, data): self.data = data self.prev = None self.next = None class Deque: def __init__(self): self.front = None self.rear = None self.size = 0 def appendleft(self, data): node = Node(data) if self.front == None: self.front = node self.rear = node else: node.next = self.front self.front.prev = node self.front = node self.size += 1 def append(self, data): node = Node(data) if self.rear == None: self.front = node self.rear = node else: node.prev = self.rear self.rear.next = node self.rear = node self.size += 1 def popleft(self): if self.size == 0: return None # 앞에서 노드 꺼내기 data = self.front.data self.front = self.front.next # 삭제로 인해 노드가 하나도 없는 경우 if self.front == None: self.rear = None else: self.front.prev = None self.size -= 1 return data def pop(self): if self.size == 0: return None # 뒤에서 노드 꺼내기 data = self.rear.data self.rear = self.rear.prev # 삭제로 인해 노드가 하나도 없는 경우 if self.rear == None: self.front = None else: self.rear.next = None self.size -= 1 return data def front(self): if self.size == 0: return None return self.front.data def rear(self): if self.size == 0: return None return self.rear.data # 앞에서부터 원소 출력 def show(self): cur = self.front while cur: print(cur.data, end=" ") cur = cur.next d = Deque() arr = [5, 6, 7, 8] for x in arr: d.append(x) arr = [4, 3, 2, 1] for x in arr: d.appendleft(x) d.show() print() while d.size != 0: print(d.popleft()) arr = [1, 2, 3, 4, 5, 6, 7, 8] for x in arr: d.appendleft(x) d.show() print() while True: print(d.pop()) if d.size == 0: break print(d.popleft()) if d.size == 0: break
3. 이진 탐색 트리
3-1. 트리 (Tree)
- 트리는 계층적인 구조를 표현할 때 사용할 수 있는 자료구조다
- 루트 노드(root node): 부모가 없는 최상위 노드
- 단말 노드(leaf node): 자식이 없는 노드
- 트리에서는 부모와 자식 관계라는 것이 있다
- 형제 관계란 같은 부모를 가지는 관계
- 트리에는 깊이라는 용어가 있다
- 깊이(depth): 루트 노드에서의 길이(length)
- 길이란 출발 노드에서 목적지 노드까지 거쳐야 하는 간선의 수를 의미한다
- 트리의 높이(height)은 루트 노드에서 가장 깊은 노드까지의 길이를 의미한다
3-2. 이진 탐색 트리 (Binary Search Tree)
- 이진 트리는 최대 2개의 자식을 가질 수 있는 트리를 말한다
- 다수의 데이터를 관리(조회, 저장, 삭제)하기 위한 가장 기본적인 자료구조 중 하나다
- 이진 탐색 트리의 특징
- 왼쪽 자식 노드 < 부모 노드 < 오른쪽 자식 노드
- 특정한 노드의 키(key) 값보다 그 왼쪽 자식 노드의 키(key) 값이 더 작다
- 특정한 노드의 키(key) 값보다 그 오른쪽 자식 노드의 키(key) 값이 더 크다
- 특정한 노드의 왼쪽 서브 트리, 오른쪽 서브 트리 모두 이진 탐색 트리다
3-2-1. 이진 탐색 트리 – 삽입 연산
- 루트 노드에서 출발하여 아래쪽으로 내려오면서, 삽입할 위치를 찾는다
- 삽입할 노드의 키(key)가 작으면 왼쪽으로, 삽입할 노드의 키(key)가 크면 오른쪽으로 삽입
3-2-2. 이진 탐색 트리 – 조회 연산
- 루트 노드에서 출발하여 아래쪽으로 내려오면서, 찾고자 하는 원소를 조회한다
- 삽입할 노드의 키(key)가 작으면 왼쪽으로, 삽입할 노드의 키(key)가 크면 오른쪽으로 조회
3-2-3. 이진 탐색 트리 – 삭제 연산
- 이진 탐색 트리에서의 삭제 연산은 세가지의 경우의 수를 가지고 실행한다
- 왼쪽 자식이 없는 경우 → 오른쪽 자식으로 대체
- 오른쪽 자식이 없는 경우 → 왼쪽 자식으로 대체
- 왼쪽, 오른쪽이 모두 있는 경우 → 오른쪽 서브 트리에서 가장 작은 노드로 대체
3-3. 트리의 순회 (traversal)
- 트리에 포함되어 있는 정보를 모두 출력하고자 할 때 순회(traversal)를 사용할 수 있다
- 트리의 모든 노드를 특정한 순서(조건)에 따라서 방문하는 방법을 순회(traversal)라고 한다
- 전위 순회(pre-order traverse): 루트 방문 → 왼쪽 자식 방문 → 오른쪽 자식 방문
- 중위 순회(in-order traverse): 왼쪽 자식 방문 → 루트 방문 → 오른쪽 자식 방문
- 후위 순회(post-order traverse): 왼쪽 자식 방문 → 오른쪽 자식 방문 → 루트 방문
- 레벨 순회(Level Order Traversal): 낮은 레벨(루트)부터 높은 레벨까지 순차적으로 방문한다, 너비 우선 탐색(BST)
3-4. 편향 이진 트리 (Skewed Binary Tree)
- 편향 이진 트리는 다음의 두 가지 속성을 가진다
- 같은 높이의 이진 트리 중 최소 개수의 노드 개수를 가진다
- 왼쪽 혹은 오른쪽으로 한 방향에 대한 서브 트리를 가진다
3-5. 이진 탐색 트리의 시간 복잡도
- 노드의 개수가 𝑁개일 때, 시간 복잡도는 다음과 같다
- 트리의 높이(height)을 𝐻라고 할 때, 엄밀한 시간 복잡도는 𝑂(𝐻)다
- 이상적인 경우 𝐻 = $log_{2}N$로 볼 수 있다
- 하지만 최악의 경우(편향된 경우) 𝐻 = 𝑁로 볼 수 있다
조회 삽입 삭제 수정 균형 잡힌 이진 탐색 트리 𝑂(𝑙𝑜𝑔𝑁) 𝑂(𝑙𝑜𝑔𝑁) 𝑂(𝑙𝑜𝑔𝑁) 𝑂(𝑙𝑜𝑔𝑁) 편향 이진 탐색 트리 𝑂(𝑁) 𝑂(𝑁) 𝑂(𝑁) 𝑂(𝑁) 3-6. 균형 잡힌 트리: AVL 트리 (Adelson, Velski & Landis Tree)
- 이진 탐색 트리는 편향 트리가 될 수 있으므로, 최악의 경우 𝑂(𝑁)을 요구한다
- 반면에 AVL 트리는 균형이 갖춰진 이진 트리다
- 간단한 구현 과정으로 완전 이진 트리에 가까운 형태를 유지하도록 한다
- AVL 트리는 다음 4가지를 통해 트리를 재편성한다
- Left rotation
- Right rotation
- Left-Right rotation
- Right-Left rotation
3-7. 파이썬으로 이진 탐색 트리 구현해보기
from collections import deque class Node: def __init__(self, key): self.key = key self.left = None self.right = None class BinarySearchTree: def __init__(self): self.root = None def search(self, node, key): return self._search(self.root, key) def _search(self, node, key): if node is None or node.key == key: return node # 현재 노드의 key보다 작은 경우 if node.key > key: return self._search(node.left, key) # 현재 노드의 key보다 큰 경우 elif node.key < key: return self._search(node.right, key) def insert(self, key): self.root = self._insert(self.root, key) def _insert(self, node, key): if node is None: return Node(key) # 현재 노드의 key보다 작은 경우 if node.key > key: node.left = self._insert(node.left, key) # 현재 노드의 key보다 큰 경우 elif node.key < key: node.right = self._insert(node.right, key) return node def delete(self, key): self.root = self._delete(self.root, key) def _delete(self, node, key): if node is None: return None # 현재 노드의 key보다 작은 경우 if node.key > key: node.left = self._delete(node.left, key) # 현재 노드의 key보다 큰 경우 elif node.key < key: node.right = self._delete(node.right, key) # 삭제할 노드를 찾은 경우 else: # 왼쪽 자식이 없는 경우 if node.left is None: return node.right # 오른쪽 자식이 없는 경우 elif node.right is None: return node.left # 왼쪽과 오른쪽 자식 모두 있는 경우 node.key = self._get_min(node.right) node.right = self._delete(node.right, node.key) return node def _get_min(self, node): key = node.key while node.left: key = node.left.key node = node.left return key def preorder(self): self._preorder(self.root) def _preorder(self, node): if node: print(node.key, end=' ') self._preorder(node.left) self._preorder(node.right) def inorder(self): self._inorder(self.root) def _inorder(self, node): if node: self._inorder(node.left) print(node.key, end=' ') self._inorder(node.right) def postorder(self): self._postorder(self.root) def _postorder(self, node): if node: self._postorder(node.left) self._postorder(node.right) print(node.key, end=' ') def levelorder(self): return self._levelorder(self.root) def _levelorder(self, node): if node is None: return result = [] queue = deque() queue.append((0, node)) # (level, node) while queue: level, node = queue.popleft() if node: result.append((level, node.key)) queue.append((level + 1, node.left)) queue.append((level + 1, node.right)) for level, key in result: print(f"level: {level}, key: {key}") def to_list(self): return self._to_list(self.root) def _to_list(self, node): if node is None: return [] return self._to_list(node.left) + [node.key] + self._to_list( node.right) arr = [7, 4, 5, 9, 6, 3, 2, 8] bst = BinarySearchTree() for x in arr: bst.insert(x) print('전위 순회:', end=' ') bst.preorder() print('\n중위 순회:', end=' ') bst.inorder() print('\n후위 순회:', end=' ') bst.postorder() print('\n[레벨 순회]') bst.levelorder() bst.delete(7) print('\n전위 순회:', end=' ') bst.preorder() print('\n중위 순회:', end=' ') bst.inorder() print('\n후위 순회:', end=' ') bst.postorder() print('\n[레벨 순회]') bst.levelorder() bst.delete(4) print('\n전위 순회:', end=' ') bst.preorder() print('\n중위 순회:', end=' ') bst.inorder() print('\n후위 순회:', end=' ') bst.postorder() print('\n[레벨 순회]') bst.levelorder() bst.delete(3) print('\n전위 순회:', end=' ') bst.preorder() print('\n중위 순회:', end=' ') bst.inorder() print('\n후위 순회:', end=' ') bst.postorder() print('\n[레벨 순회]') bst.levelorder() print(bst.to_list())
4. 우선순위 큐 (Priority Queue)
- 우선순위 큐는 우선순위에 따라서 데이터를 추출하는 자료구조이다
- 우선순위 큐는 일반적으로 힙(heap)을 이용해 구현한다
4-1. 힙(Heap)의 시간 복잡도
우선 순위 큐 구현 방식 삽입 시간 삭제 시간 리스트 자료형 𝑂(1) 𝑂(𝑁) 힙 (Heap) 𝑂(𝑙𝑜𝑔𝑁) 𝑂(𝑙𝑜𝑔𝑁) 4-2. 힙 (heap)
- 힙(Heap)은 원소들 중에서 최댓값 혹은 최솟값을 빠르게 찾아내는 자료구조다
- 최대 힙(Max Heap): 값이 큰 원소부터 추출한다
- 최소 힙(Min Heap): 값이 작은 원소부터 추출한다
- 힙은 원소의 삽입과 삭제를 위해 𝑂(𝑙𝑜𝑔𝑁)의 수행 시간을 요구한다
- 단순한 𝑁개의 데이터를 힙에 넣었다가 모두 꺼내는 작업은 정렬과 동일하다
- 이 경우 시간 복잡도는 𝑂(𝑁𝑙𝑜𝑔𝑁) 이다
- 힙은 완전 이진 트리 자료구조를 따른다
- 힙에서는 우선순위가 높은 노드가 루트(root)에 위치한다
4-3. 최대 힙 (Max Heap)
- 부모 노드의 키 값이 자식 노드의 키 값보다 항상 크다
- 루트 노드가 가장 크며, 값이 큰 데이터가 우선순위를 가진다
4-4. 최소 힙 (min heap)
- 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작다
- 루트 노드가 가장 작으며, 값이 작은 데이터가 우선순위를 가진다
4-5. 최소 힙 구성 함수: Heapify
- Heapify 함수를 통해 최소 힙이 되도록 재편성할 수 있다
- 재편성할 때는 부모로 거슬러 올라가며, 부모보다 자신이 더 작은 경우에 위치를 교체한다 (상향식)
- 이런식으로 하면 새로운 원소가 삽입되었을 때 𝑂(𝑙𝑜𝑔𝑁)의 시간 복잡도로 힙 성질을 유지하도록 할 수 있다
- 최소 힙에서 원소를 제거할 때는 가장 마지막 노드가 루트 노드의 위치에 오도록 한 후에 Heapify 함수로 재편성한다
→ 가장 마지막 노드가 가장 크기 때문에 바꿔주고 재편성, 이 때는 거슬러 올라가는 것이 아닌 하향식
→ 이런식으로 하면 새로운 원소가 삭제되었을 때 𝑂(𝑙𝑜𝑔𝑁)의 시간 복잡도로 힙 성질을 유지하도록 할 수 있다
4-6. 파이썬의 힙(Heap) 라이브러리
- 파이썬에서는 힙(Heap) 라이브러리를 제공한다
- ℎ𝑒𝑎𝑝𝑞 라이브러리의 삽입 및 삭제에 대한 시간 복잡도는 모두 𝑂(𝑙𝑜𝑔𝑁)이다
4-6-1. heapq 라이브러리 – 힙 초기화
import heapq heap = [] print(heap) # 실행 결과 : []
4-6-2. heapq 라이브러리 – 삽입(push)과 추출(pop)
- 원소의 삽입: ℎ𝑒𝑎𝑝𝑝𝑢𝑠ℎ() 메서드
- 원소의 추출: ℎ𝑒𝑎𝑝𝑝𝑜𝑝() 메서드
import heapq heap = [] heapq.heappush(heap, 7) heapq.heappush(heap, 4) heapq.heappush(heap, 5) heapq.heappush(heap, 8) while heap: element = heapq.heappop(heap) print(element, end=" ") # 실행 결과 : 4 5 7 8
4-6-3. heapq 라이브러리 – 최솟값 구하기
import heapq heap = [] heapq.heappush(heap, 7) heapq.heappush(heap, 4) heapq.heappush(heap, 5) heapq.heappush(heap, 8) print(heap[0]) #실행 결과 : 4
4-6-4. heapq 라이브러리 – 리스트를 힙으로 변환
- ℎ𝑒𝑎𝑝𝑝𝑢𝑠ℎ() 메서드를 이용해 하나씩 원소를 삽입하지 않을 수 있다
- ℎ𝑒𝑎𝑝𝑖𝑓𝑦() 메서드는 새로운 리스트를 반환하지 않고, 리스트 내부를 직접 수정한다
import heapq heap = [9, 1, 5, 4, 3, 8, 7] heapq.heapify(heap) while heap: element = heapq.heappop(heap) print(element, end=" ") # 실행 결과 : 1 3 4 5 7 8 9
4-6-5. heapq 라이브러리 – 최대 힙 (Max Heap)
- 파이썬의 heapq 라이브러리는 기본적으로 최소 힙 기능을 제공한다
- 최대 힙을 위해서는 삽입과 추출할 때 키(key)에 음수(-) 부호를 취한다
import heapq arr = [9, 1, 5, 4, 3, 8, 7] heap = [] for x in arr: heapq.heappush(heap, -x) while heap: x = -heapq.heappop(heap) print(x, end=" ") # 실행 결과 : 9 8 7 5 4 3 1
4-7. 힙 활용 예시
4-7-1. 힙 정렬 (Heap Sort)
- 단순히 힙에 원소를 넣었다가 꺼내는 것 만으로도 정렬을 수행할 수 있다
import heapq def heap_sort(arr): heap = [] for x in arr: heapq.heappush(heap, x) result = [] while heap: x = heapq.heappop(heap) result.append(x) return result print(heap_sort([9, 1, 5, 4, 3, 8, 7])) # 실행 결과 : [1, 3, 4, 5, 7, 8, 9]
4-7-2. N번째로 작은 값
- 최소 힙이나 최대 힙을 사용하여 𝑛번째로 작은 값이나 𝑛번째로 큰 값을 얻을 수 있다
- 힙(heap)을 만든 뒤에 추출(pop) 함수를 𝑛번 호출한다
import heapq def n_smallest(n, arr): heap = [] for x in arr: heapq.heappush(heap, x) result = None for _ in range(n): result = heapq.heappop(heap) return result arr = [9, 1, 5, 4, 3, 8, 7] print(n_smallest(3, arr)) # 실행 결과 : 4
- 파이썬에서는 𝑁번째로 작은 값을 구하는 메서드를 제공한다
- 𝑛𝑠𝑚𝑎𝑙𝑙𝑒𝑠𝑡() 메서드는 가장 작은 𝑛개의 값을 반환한다.
import heapq arr = [9, 1, 5, 4, 3, 8, 7] result = heapq.nsmallest(3, arr) print(result[-1]) # 실행 결과 : 4
4-7-3. N번째로 큰 값
- 파이썬에서는 𝑁번째로 큰 값을 구하는 메서드를 제공한다
- 𝑛𝑙𝑎𝑟𝑔𝑒𝑠𝑡() 메서드는 가장 큰 𝑛개의 값을 반환한다
import heapq arr = [9, 1, 5, 4, 3, 8, 7] result = heapq.nlargest(3, arr) print(result[-1]) # 실행 결과 : 7
4-8. 파이썬으로 힙 구현하기
class Heap(object): def __init__(self): # 첫번째 원소는 사용하지 않음 self.arr = [None] # 원소 삽입(push) def push(self, x): # 마지막 위치에 원소를 삽입 self.arr.append(x) # 첫 원소인 경우 종료 if len(self.arr) == 2: return # 값의 크기를 비교하며 부모를 타고 올라감 i = len(self.arr) - 1 while True: parent = i // 2 # 작은 값을 부모 쪽으로 계속 이동 if 1 <= parent and self.arr[parent] > self.arr[i]: self.arr[parent], self.arr[i] = self.arr[i], self.arr[parent] i = parent else: break # 원소 추출(pop) def pop(self): # 마지막 원소 i = len(self.arr) - 1 # 남은 원소가 없다면 종료 if i < 1: return None # 루트 원소와 마지막 원소를 교체하여, 마지막 원소 추출 self.arr[1], self.arr[i] = self.arr[i], self.arr[1] result = self.arr.pop() # 루트(root)에서부터 원소 정렬 self.heapify() return result # 루트(root)에서부터 자식 방향으로 내려가며 재정렬 def heapify(self): # 남은 원소가 1개 이하라면 종료 if len(self.arr) <= 2: return # 루트 원소 i = 1 while True: # 왼쪽 자식 child = 2 * i # 왼쪽 자식과 오른쪽 자식 중 더 작은 것을 선택 if child + 1 < len(self.arr): if self.arr[child] > self.arr[child + 1]: child += 1 # 더 이상 자식이 없거나, 적절한 위치를 찾은 경우 if child >= len(self.arr) or self.arr[child] > self.arr[i]: break # 원소를 교체하며, 자식 방향으로 내려가기 self.arr[i], self.arr[child] = self.arr[child], self.arr[i] i = child
arr = [9, 1, 5, 4, 3, 8, 7] heap = Heap() for x in arr: heap.push(x) while True: x = heap.pop() if x == None: break print(x, end=" ")
- 힙 정렬 속도 측정해보기
import random import time # N개의 무작위 데이터 생성 arr = [] n = 100000 for _ in range(n): arr.append(random.randint(0, 1000000)) # 시간 측정 시작 start_time = time.time() # 힙에 모든 원소 삽입 heap = Heap() for x in arr: heap.push(x) # 힙에서 모든 원소 추출 result = [] while True: x = heap.pop() if x == None: break result.append(x) # 시간 측정 종료 print(f"Elapsed time: {time.time() - start_time} seconds.") # 오름차순 정렬 여부 확인 ascending = True for i in range(n - 1): if result[i] > result[i + 1]: ascending = False print("Sorted:", ascending) # 가장 작은 5개 원소와 가장 큰 5개 원소 출력 print(result[:5]) print(result[-5:])
5. 그래프 (Graph)
- 그래프(graph)란 사물을 정점(vertex)과 간선(edge)으로 나타내기 위한 도구다
- 그래프는 두 가지 방식으로 구현할 수 있다
- 인접 행렬(adjacency matrix): 2차원 배열을 사용하는 방식
- 인접 리스트(adjacency list): 연결 리스트를 이용하는 방식
- 그래프 종류
- 무방향 무가중치 그래프 : 모든 간선이 방향성을 가지지 않는 그래프를 무방향 그래프
- 무가중치 그래프 : 모든 간선에 가중치가 없는 그래프
- 방향 그래프 : 모든 간선이 방향을 가지는 그래프를 방향 그래프
- 가중치 그래프 : 모든 간선에 가중치가 있는 그래프
5-1. 인접 행렬 (Adjacency Matrix)
- 인접 행렬(adjacency matrix)에서는 그래프를 2차원 배열로 표현한다
5-1-1. 인접 행렬 - 무방향 무가중치 그래프
graph = [ [0, 1, 1, 0], [1, 0, 1, 0], [1, 1, 0, 1], [0, 0, 1, 0] ]
5-1-2. 인접 행렬 - 방향 가중치 그래프
graph = [ [0, 0, 7, 0], [3, 0, 8, 0], [0, 8, 0, 0], [0, 0, 4, 0] ]
5-2. 인접 리스트 (Adjacency List)
- 인접 리스트(adjacency list)에서는 그래프를 리스트로 표현한다
5-2-1. 인접 리스트 - 무방향 무가중치 그래프
graph = [ [1, 2], [0, 2], [0, 1, 3], [2] ]
5-2-2. 인접 리스트 - 방향 가중치 그래프
graph = [ [(2, 7)], [(0, 3), (2, 8)], [(1, 8)], [(2, 4)] ]
5-3. 그래프의 시간 복잡도
- 인접 행렬: 모든 정점들의 연결 여부를 저장해 $O(V^{2})$의 공간을 요구한다
- 공간 효율성이 떨어지지만, 두 노드의 연결 여부를 𝑂(1)에 확인할 수 있다
- 인접 리스트: 연결된 간선의 정보만을 저장하여 𝑂(𝑉 + 𝐸)의 공간을 요구한다
- 공간 효율성이 우수하지만, 두 노드의 연결 여부를 확인하기 위해 𝑂(𝑉)의 시간이 필요하다
필요한 메모리 연결 여부 확인 인접 행렬 $O(V^{2})$ 𝑂(1) 인접 리스트 𝑂(𝑉 + 𝐸) 𝑂(𝑉) 5-4. 인접 행렬 vs. 인접 리스트
- 최단 경로 알고리즘을 구현할 때, 어떤 자료구조가 유용할까?
- 각각 근처의 노드와 연결되어 있는 경우가 많으므로, 간선 개수가 적어 인접 리스트가 유리하다
반응형'프로그래밍 언어 > Python' 카테고리의 다른 글
파일 입출력, 함수, 클래스, 예외 처리 (0) 2024.02.09 리스트, 튜플, 딕셔너리, 집합, 불, 조건문, 반복문 (2) 2024.02.08 개발환경, 기본 입출력, 정수 자료형, 실수 자료형, 문자열 자료형 (4) 2024.02.07 배열, 연결리스트, 파이썬 리스트 자료형,스택 (2) 2024.02.05 다음글이 없습니다.이전글이 없습니다.댓글