20210629 Algorithm 04 : Binary Search(이진 탐색), Sequential Search(순차 탐색), Graph의 이해, BFS(너비우선탐색), DFS(깊이우선탐색)
Algorithm 04
Binary Search (이진 탐색)
- 탐색할 자료를 둘로 나누어 해당 데이터가 있을만한 곳을 탐색하는 방법
- 정열이 되어 있다는 가정하에 순차적으로 탐색하지 않고, 중간을 탐색해서 어디에 있는지 판단하는 방식
분할 정복 알고리즘과 이진 탐색
- 분할 정복 알고리즘 : Divide(문제를 하나 또는 둘 이상으로 분할), Conquer(나눠진 문제가 충분히 작고, 해결가능하면 해결하고 그렇지 않으면 다시 분할함)
- 이진 탐색:
- Divide : 리스트를 두 개의 서브 리스트로 나눔
- Conquer :
- 검색할 숫자 > 중간값 -> 뒷부분의 서브 리스트에서 검색할 숫자를 찾음
- 검색할 숫자 < 중간값 -> 앞부분의 서브 리스트에서 검색할 숫자를 찾음
- 이진 탐색도 분할 정복 알고리즘에 해당하므로, 전형적으로 재귀함수를 활용하여 구현할 수 있음
알고리즘 고안
- 데이터가 일단, 정렬되어 있는 상태
- 데이터 리스트와, 검색할 데이터를 인자로 받아 해당 함수를 구현
알고리즘 구현
def binary_search(data, search):
if len(data) == 1 and search == data[0]: # 최종적으로 1개의 데이터인 경우 비교 데이터와 비교
return True
if len(data) == 1 and search != data[0]:
return False
if len(data) == 0:
return False
medium = len(data) // 2 # 몫을 구하면 중간 인덱스를 구할 수 있음
if search == data[medium]:
return True
else:
if search > data[medium]:
return binary_search(data[medium:], search)
else:
return binary_search(data[:medium], search)
# TEST
import random
data_list = random.sample(range(100), 10)
data_list.sort() # 정렬이 필요 하므로, list 내장 함수 sort()를 사용
# [10, 11, 18, 36, 42, 65, 68, 69, 71, 89]
binary_search(data_list, 66)
# [10, 11, 18, 36, 42, 65, 68, 69, 71, 89]
# [68, 69, 71, 89]
# [68, 69]
# [68]
# False
이진 탐색의 시간 복잡도
- 한번 처리하는 과정(1턴)에서, 전체의 데이터가 1/2씩 줄어 들고 데이터가 1개가 될 때 까지 이를 반복
- 1턴에 1번 밖에 처리 안함(안에 반복문은 없음)
- 데이터의 개수가 1이 될때 까지 반복 이니까, 턴의 개수는
log n
번- n _ (1/2) _ (1/2) _ (1/2) _ … = 1
- n * (1/2)^k = 1 (1일 될 때까지 k 번 반복)
- n = 2^k
- log n = k
O(log n)
Sequential Search (순차 탐색)
- 탐색은 여러 데이터 중에서 원하는 데이터를 찾아내는 것을 의미
- 데이터가 담겨있는 리스트를 앞에서 부터 하나씩 비교해서 원하는 데이터를 찾는 방법
프로그래밍 연습
- 임의 리스트 존재시 원하는 데이터의 위치를 return 하는 순차탐색 알고리즘 작성하기
from random import *
rand_data_list = list()
for num in range(10):
rand_data_list.append(randint(1, 100)) # 1 ~ 100 의 int를 10회 수행
print(rand_data_list)
# [71, 63, 75, 33, 6, 37, 81, 79, 3, 29] 중복된 숫자도 가능함
def sequential(data_list, search_data):
for i in range(len(data_list)):
if data_list[i] == search_data:
return i
return -1
# TEST
sequential(rand_data_list, 37) # 5
sequential(rand_data_list, 3) # -1
순차 탐색의 시간 복잡도
- 최악의 경우 list 길이가 n일 때, n번 비교해야함
O(n)
그래프(Graph) 이해
- 그래프는 실제 세계의 현상이나 사물을 정점(Vertex) 또는 노드(Nord) 와 간선(Edge)로 표현하기 위해 사용
- 예제) 집에서 회사로 가는 경로를 그래프로 표현
용어
노드 (Node)
: 위치를 말함, 정점(Vertex)라고도 함간선 (Edge)
: 위치 간의 관계를 표시한 선으로 노드를 연결한 선이라고 보면 됨 (link or branch라고도 함)인접 정점 (Adjacent Vertex)
: 간선으로 직접 연결된 정점(또는 노드), 인접노드- 참고 용어
무방향 그래프
: 간선이 방향이 없는 경우방향 그래프
: 간선이 방향이 있는 경우정점의 차수 (Degree)
: 무방향 그래프에서 하나의 정점에 인접한 정점의 수 (특정 노드에 무방향으로 연결된 노드의 수)진입 차수(In-Degree)
: 방향 그래프에서 외부에서 오는 간선의 수(자기 노드에서 몇개의 간선을 받느냐)진출 차수(Out-Degree)
: 방향 그래프에서 외부로 향하는 간선의 수(자기 노드에서 몇개의 간선을 보내냐)경로 길이 (Path Length)
: 경로를 구성하기 위해 사용된 간선의 수단순 경로 (Simple Path)
: 처음 정점과 끝 정점을 제외하고 중복된 정점이 없는 경로(경로 중에 중복된 정점을 가진 경로를 제외한 것, 즉, Start, End가 동일한 경우를 제외하고 중복된 정점을 가지지 않은 경로)사이클 (Cycle)
: 단순 경로의 시작 정점과 종료 정점이 동일한 경우- 단순 경로는 사이클 일 수 있다, 사이클은 단순 경로이다.
그래프의 종류
무방향 그래프 (Undirected Graph)
- 간선에 방향이 없는 그래프
- 간선을 통해, 노드는 양방향으로 갈 수 있음
- 보통 노드 A, B가 연결 되어 있을 경우,
(A, B)
,(B, A)
로 표기
방향 그래프 (Directed Graph)
- 간선에 방향이 있는 그래프
- 간선을 통해, 노드는 지정된 방향으로만 갈 수 있음
- 보통 노드 A, B가 연결 되어 있을 경우,
- A -> B:
<A, B>
로 표기 - B -> A:
<B, A>
로 표기 - 순서가 있음을 주의 하자
- A -> B:
가중치 그래프 (Weighted Graph) 또는 네트워크(Network)
- 간선에 비용 또는 가중치가 할당된 그래프
- 그림에서는 간선 위에 숫자를 적어 표현
- 거리가 될수도 있고, 그런 간선간의 차이를 표현할 수 있음
연결 그래프 (Connected Graph)와 비연결 그래프 (Disconnected Graph)
- 연결 그래프
- 일단 무방향 그래프 이어야 함
- 무방향 그래프에 있는 모든 노드에 대해 항상 경로가 존재하는 경우
- 모든 노드가 어느 하나라도 연결 되어 있어 접근이 가능한 경우를 말함, 하나에 모두 연결(완전 그래프)이 아님
- 비연결 그래프
- 무방향 그래프에서 특정 노드에 대해 경로가 존재하지 않는 경우
사이클(Cycle)과 비순환 그래프(Acyclic Graph)
- 사이클
- 단순 경로의 시작 노드와 종료 노드가 동일한 경우
- 비순환 그래프
- 사이클이 없는 그래프
완전 그래프 (Complete Graph)
- 그래프의 모든 노드가 서로 연결되어 있는 그래프
- 각각의 노드가 모든 노드로 바로 연결 되어 있음
그래프와 트리의 차이
- 트리는 그래프 중에 속한 특별한 종류임
그래프 ⊃ 트리
항목\종류 | 그래프 | 트리 |
---|---|---|
정의 | 노드와 노드를 연결하는 간선으로 표현되는 자료구조 | 그래프의 한종류, 방향성이 있는 비순환 그래프 |
방향성 | O (방향, 무방향 모두 가능) | O (방향 그래프만 가능) |
사이클 | O (순환, 비순환 모두 가능) | X (비순환만 가능) |
루트 노드 | X (루트 노드를 정의하고 있지 않지만, 스스로 추가는 가능) |
O (루트 노드를 정의하고 있음) |
부모/자식 관계 | X (부모자식 정의하고 있지 않지만, 스스로 추가는 가능) |
O (부모 자식 관계 존재) |
BFS (Breadth-First Search, 너비 우선 탐색)
BFS 와 DFS
- 대표적인 그래프 탐색 알고리즘
- 너비 우선 탐색(Breadth First Search, BFS): 정점들과 같은 레벨에 있는 노드들(형제 노드들)을 먼저 탐색하는 방식
- 깊이 우선방식(Depth First Search, DFS): 정점의 자식들을 먼저 탐색하는 방식
- 둘다, 약간 트리같은 사이클이 없는 그래프에서 탐색
BFS/DFS 방식 이해를 위한 예제
- BFS : 한 단계씩 내려가면서, 해당 노드와 같은 레벨있는 노드 먼저 순회
- 가로 방향의 지그재그
- DFS : 한 노드의 자식을 타고 끝까지 순회한 후, 다시 돌아와서 다른 형제들의 자식을 타고 내려가며 순회
- 세로 방향의 지그재그
Python으로 그래프를 표현하는 방법
- 파이썬에서 제공하는 딕셔너리와 리스트 자료구조 활용하여 그래프 표현
- 딕셔너리의 key - values 구조 item을 노드로 활용하여, key는 해당 DATA를 표현하고 values는 해당 노드에 연결된 노드를 가르키게 함
- values 는 리스트 형태로 여러 개의 노드를 가르키게 함
Key | Values | |||
---|---|---|---|---|
A | B | C | ||
B | A | D | ||
C | A | G | H | I |
D | B | E | F | |
E | D | |||
F | D | |||
G | C | |||
H | C | |||
I | C | J | ||
J | I |
graph = dict()
graph['A'] = ['B', 'C']
graph['B'] = ['A', 'D']
graph['C'] = ['A', 'G', 'H', 'I']
graph['D'] = ['B', 'E', 'F']
graph['E'] = ['D']
graph['F'] = ['D']
graph['G'] = ['C']
graph['H'] = ['C']
graph['I'] = ['C', 'J']
graph['J'] = ['I']
print(graph)
# {'A': ['B', 'C'],
# 'B': ['A', 'D'],
# 'C': ['A', 'G', 'H', 'I'],
# 'D': ['B', 'E', 'F'],
# 'E': ['D'],
# 'F': ['D'],
# 'G': ['C'],
# 'H': ['C'],
# 'I': ['C', 'J'],
# 'J': ['I']}
BFS 알고리즘 구현
- 자료구조 큐를 활용
- need_visit 큐와 visited 큐, 두 개의 큐를 생성
- visited 큐의 경우 다녀온 노드의 값을 저장하여 다녀온 노드를 구분하기 위함
- need_visit 큐의 경우에는 실질적으로 다음에 이동할 노드를 알려줌
- need_visit는 visited 큐와 비교하여 visited 큐에 존재하는 노드의 경우에는 아무작업도 하지 않음
- visited 큐에 이동할 노드가 존재하지 않는 경우 해당 노드를 집어 넣고, 해당 노드에서 이동 할 수 있는 노드를 순서대로 need_visit에 추가한다
패턴
- current에 있던 노드A -> visited (visited에 없는 경우)
- visited에 있는 경우 아무것도 하지 않고, 3번으로 가서, needvisit에서 하나를 빼서 current로 올림
- visited에 들어가진 노드 A의 이동 가능한 노드(B, C) -> need_visit
- need_visit에 있는 노드 B, C에서 하나를 -> current
- 반복
# {'A': ['B', 'C'],
# 'B': ['A', 'D'],
# 'C': ['A', 'G', 'H', 'I'],
# 'D': ['B', 'E', 'F'],
# 'E': ['D'],
# 'F': ['D'],
# 'G': ['C'],
# 'H': ['C'],
# 'I': ['C', 'J'],
# 'J': ['I']}
- 큐의 특징으로 FIFO 방식으로, 선입 선출 형태로 need_visit의 값이 넣고 빠짐
1 | |||||||||
---|---|---|---|---|---|---|---|---|---|
current | 초기 | A | A추가 | B | B추가 | C | C추가 | A | A추가X |
visited | [ ] | [ ] | [A] | [A] | [A, B] | [A, B] | [A, B, C] | [A, B, C] | [A, B, C] |
need_visit | [A] | [ ] | [B, C] | [C] | [C, A, D] | [A, D] | [A, D, A, G, H, I] | [D, A, G, H, I] | [D, A, G, H, I] |
2 | ||||||
---|---|---|---|---|---|---|
current | D | D추가 | A | A추가X | G | G추가 |
visited | [A, B, C] | [A, B, C, D] | [A, B, C, D] | [A, B, C, D] | [A, B, C, D] | [A, B, C, D, G] |
need_visit | [A, G, H, I] | [A, G, H, I, B, E, F] | [G, H, I, B, E, F] | [G, H, I, B, E, F] | [H, I, B, E, F] | [H, I, B, E, F, C] |
3 | ||||||
---|---|---|---|---|---|---|
current | H | H추가 | I | I추가 | B | B추가X |
visited | [A, B, C, D, G] | [A, B, C, D, G, H] | [A, B, C, D, G, H] | [A, B, C, D, G, H, I] | [A, B, C, D, G, H, I] | [A, B, C, D, G, H, I] |
need_visit | [I, B, E, F, C] | [I, B, E, F, C, C] | [B, E, F, C, C] | [B, E, F, C, C, C, J] | [E, F, C, C, C, J] | [E, F, C, C, C, J] |
4 | ||||
---|---|---|---|---|
current | E | E추가 | F | F추가 |
visited | [A, B, C, D, G, H, I] | [A, B, C, D, G, H, I, E] | [A, B, C, D, G, H, I, E] | [A, B, C, D, G, H, I, E, F] |
need_visit | [F, C, C, C, J] | [F, C, C, C, J, D] | [C, C, C, J, D] | [C, C, C, J, D, D] |
5 | ||||
---|---|---|---|---|
current | C | C추가X | C | C추가X |
visited | [A, B, C, D, G, H, I, E, F] | [A, B, C, D, G, H, I, E, F] | [A, B, C, D, G, H, I, E, F] | [A, B, C, D, G, H, I, E, F] |
need_visit | [C, C, J, D, D] | [C, C, J, D, D] | [C, J, D, D] | [C, J, D, D] |
6 | ||||
---|---|---|---|---|
current | C | C추가X | J | J추가 |
visited | [A, B, C, D, G, H, I, E, F] | [A, B, C, D, G, H, I, E, F] | [A, B, C, D, G, H, I, E, F] | [A, B, C, D, G, H, I, E, F, J] |
need_visit | [J, D, D] | [J, D, D] | [D, D] | [D, D, I] |
7 | ||||
---|---|---|---|---|
current | D | D추가X | D | D추가X |
visited | [A, B, C, D, G, H, I, E, F, J] | [A, B, C, D, G, H, I, E, F, J] | [A, B, C, D, G, H, I, E, F, J] | [A, B, C, D, G, H, I, E, F, J] |
need_visit | [D, I] | [D, I] | [I] | [I] |
8 | ||
---|---|---|
current | I | I추가X |
visited | [A, B, C, D, G, H, I, E, F, J] | [A, B, C, D, G, H, I, E, F, J] |
need_visit | [] | [] |
- 결국, A - B - C - D - G - H - I - E - F - J 순으로 탐색함
코드 구현
# graph =
# {'A': ['B', 'C'],
# 'B': ['A', 'D'],
# 'C': ['A', 'G', 'H', 'I'],
# 'D': ['B', 'E', 'F'],
# 'E': ['D'],
# 'F': ['D'],
# 'G': ['C'],
# 'H': ['C'],
# 'I': ['C', 'J'],
# 'J': ['I']}
def bfs(graph, start_node):
visited = list()
need_visit = list()
# 시작하는 초기 노드 넣기
need_visit.append(start_node)
# count = 0
while need_visit: # need_visit의 원소가 없을 때 까지
# count += 1
node = need_visit.pop(0)
if node not in visited:
visited.append(node)
need_visit.extend(graph[node]) # extend는 뒤에 리스트를 붙여줌
# print(count)
# need_visit 원소가 없을 때
return visited
# TEST
bfs(graph, 'A')
# [A, B, C, D, G, H, I, E, F, J]
# count를 넣었다면
# 19
BFS 시간 복잡도
- 사용자가 입력하여 지정할 수 있는 것이, 노드 수와 간선수 이기 때문에 시간복잡도에서 고려하게 됨
- 노드 수: V, 간선수: E
- while need_visit을 V+E만큼 실행
- 시간 복잡도 :
O(V + E)
DFS (Depth-First Search, 깊이 우선 탐색)
- 자기와 연결된 노드의 맨 밑의 레벨(Leaf node) 까지 탐색하고 다음으로 진행하는 형태
DFS 알고리즘 구현
- 자료구조 스택과 큐를 활용
- need_visit 스택과 visited큐, 두개의 자료 형태를 사용함
- 스택의 특징으로 LIFO 방식으로, 후입 선출 형태로 need_visit의 값이 넣고 빠짐
패턴
- BFS와 동일하지만, 다음 값으로 진행할 때, stack으로 후입선출 (LIFO)로 값을 빼서 진행함
# {'A': ['B', 'C'],
# 'B': ['A', 'D'],
# 'C': ['A', 'G', 'H', 'I'],
# 'D': ['B', 'E', 'F'],
# 'E': ['D'],
# 'F': ['D'],
# 'G': ['C'],
# 'H': ['C'],
# 'I': ['C', 'J'],
# 'J': ['I']}
1 | |||||||||
---|---|---|---|---|---|---|---|---|---|
current | 초기 | A | A추가 | C | C추가 | I | I추가 | J | J추가 |
visited | [ ] | [ ] | [ A ] | [A] | [A, C] | [A, C] | [A, C, I] | [A, C, I] | [A, C, I, J] |
need_visit | [A] | [ ] | [B, C] | [B] | [B, A, G, H, I] | [B, A, G, H] | [B, A, G, H, C, J] | [B, A, G, H, C] | [B, A, G, H, C, I] |
2 | ||||||
---|---|---|---|---|---|---|
current | I | I추가X | C | C추가X | H | H추가 |
visited | [A, C, I, J] | [A, C, I, J] | [A, C, I, J] | [A, C, I, J] | [A, C, I, J] | [A, C, I, J, H] |
need_visit | [B, A, G, H, C] | [B, A, G, H, C] | [B, A, G, H] | [B, A, G, H] | [B, A, G] | [B, A, G, C] |
3 | ||||||
---|---|---|---|---|---|---|
current | C | C추가X | G | G추가 | C | C추가X |
visited | [A, C, I, J, H] | [A, C, I, J, H] | [A, C, I, J, H] | [A, C, I, J, H, G] | [A, C, I, J, H, G] | [A, C, I, J, H, G] |
need_visit | [B, A, G] | [B, A, G] | [B, A] | [B, A, C] | [B, A] | [B, A] |
4 | ||||
---|---|---|---|---|
current | A | A추가X | B | B추가 |
visited | [A, C, I, J, H, G] | [A, C, I, J, H, G] | [A, C, I, J, H, G] | [A, C, I, J, H, G, B] |
need_visit | [B] | [B] | [] | [A, D] |
5 | ||||
---|---|---|---|---|
current | D | D추가 | F | F추가 |
visited | [A, C, I, J, H, G, B] | [A, C, I, J, H, G, B, D] | [A, C, I, J, H, G, B, D] | [A, C, I, J, H, G, B, D, F] |
need_visit | [A] | [A, B, E, F] | [A, B, E] | [A, B, E, D] |
6 | ||||
---|---|---|---|---|
current | D | D추가X | E | E추가 |
visited | [A, C, I, J, H, G, B, D, F] | [A, C, I, J, H, G, B, D, F] | [A, C, I, J, H, G, B, D, F] | [A, C, I, J, H, G, B, D, F, E] |
need_visit | [A, B, E] | [A, B, E] | [A, B] | [A, B, D] |
7 | ||||
---|---|---|---|---|
current | D | D추가X | B | B추가X |
visited | [A, C, I, J, H, G, B, D, F, E] | [A, C, I, J, H, G, B, D, F, E] | [A, C, I, J, H, G, B, D, F, E] | [A, C, I, J, H, G, B, D, F, E] |
need_visit | [A, B] | [A, B] | [A] | [] |
- 결국, A - C - I - J - H - G - B - D - F - E 순으로 탐색함
- 왼쪽, 오른쪽 방향은 상관 없이 똑같음
코드 작성하기
def dfs(graph, start_node):
visited, need_vist = list(), list()
need_visit.append(start_node)
while need_visit:
node = node_visit.pop()
if node not in visited:
visited.append(node)
need_visit.extend(graph[node])
return visited
# TEST
dfs(graph, 'A')
# ['A', 'C', 'I', 'J', 'H', 'G', 'B', 'D', 'F', 'E']
DFS의 사간 복잡도
- BFS와 마찬가지로
O(V + E)
이다- 노드수 + 간선 수