꼭 필요한 자료구조 기초

피치자몽·2021년 7월 20일
0
post-thumbnail

탐색이란

많은 양의 데이터 중에서 원하는 데이터를 찾는 과정을 의미한다.
프로그래밍에서는 그래프, 트리 등의 자료구조 안에서 탐색하는 문제를 다룬다
대표적인 탐색 알고리즘으로 DFS/BFS 를 제대로 이해하려면 기본 자료구조인 스택과 큐에 대한 이해가 전제 되어야 한다.

스택과 큐를 사용할 때는 삽입과 삭제 외에도 오버플로와 언더플로를 고민해야한다.
오버플로는 특정한 자료구조가 수용할 수 있는 데이터의 크기를 이미 찬 상태에서 삽입할 때 발생한다. 반대로 자료구조가 없을 때 삭제하려 하면 언더플로가 발생한다.

스택

스택은 FILO 또는 LIFO 즉 선입후출,후입선출 구조라고 한다.
stack.append() stack.pop() 과 같은 메소드를 구현 할 수 있다.

큐는 선입선출 FIFO 구조라고 한다.
from collections import deque #collections 모듈에서 deque 자료구조 활용
queue = deque()
queue.append() #맨 뒤에 갚 삽입
queue.popleft() #맨 앞쪽갚 삭제 및 return
queue.reverse() #역순으로

dequeue는 스택과 큐의 장점을 모두 채택한 것인데, 데이터의 삽입과 삭제 속도가 리스트자료형에 비해 효율적이고 간단하다.
대부분의 코딩테스트에서 collections 모듈과 같은 기본 라이브러리 사용을 허용한다.
dequeue 객체를 리스트 자료형으로 변경하고자 한다면 list 메서드를 이용하자. list(dequeue)

재귀함수

DFS 와 BFS를 구현하려면 재귀함수도 이해하고 있어야 한다.

def recursive_function:
	print('재귀함수를 호출합니다.')
    recursive_function()

recursive_function()

이 코드를 실행하면 '재귀 함수를 호출합니다' 라는 문자열을 무한히 출력한다.
물론 파이썬 인터프리터는 호출 횟수 제한이 있는데, 이 한계를 벗어나게 되면
recursive error가 뜬다.

재귀함수는 수학시간에 언급되는 프랙탈 구조와 흡사하다.

재귀함수의 종료조건

def recursive_function(i):
	if i == 100:
    	return 
    print(i,'번째 재귀 함수에서',i+1,'번째 재귀함수를 호출합니다.')
    recursive_function(i+1)
    print(i,'번째 재귀 함수를 종료합니다.')

recursive_function(1)

재귀함수의 수행은 스택 자료구조를 이용한다.
따라서 스택 자료구조를 활용해야하는 상당수 알고리즘은 재귀함수를 이용해서 간편하게 구현될수있다. DFS가 대표적인 예이다.

재귀함수를 이용하는 대표적 예제로는 팩토리얼 문제가 있다.
수학적으로 0!과 1!의 값은 1로 같다는 성질을 이용하여 팩토리얼 함수는 n이 1이하가 되었을 때 함수를 종료하는 재귀함수의 형태로 구현할 수 있다.

#반복적으로 구현한 n!
def factorial_iterative(n):
	result = 1
    for i in range(1,n+1):
    	result *= i
    return result
#재귀적으로 구현한 n!
def factorial_recursive(n):
	if n <= 1:
    	return 1
    return n * factorial_recursive(n-1)

실행결과는 같지만 재귀함수가 재귀식(점화식)을 그대로 소스코드에 옮겼기 때문에 간결하다.

탐색 알고리즘 DFS/BFS

DFS

DFS는 깊이 우선 탐색이라고도 부르며, 그래프에서 깊은 부분을 우선적으로탐색하는 알고리즘이다.
그래프는 노드와 간선으로 표현되며 이때 노드를 정점이라고도 말한다.
그래프 탐색이란 하나의 노드를 시작으로 다수의 노드를 방문하는 것을 말한다.

프로그래밍에서 그래프는 크게 2가지 방식으로 표현할 수 있는데 코딩테스트에서는 이 두 방식 모두 필요하다.

인접 행렬 : 2차원배열로 그래프의 연결 관계를 표현하는 방식
인접 리스트 : 리스트로 그래프의 연결 관계를 표현하는 방식

012
0075
170무한
25무한0

인접 행렬방식은
2차원 배열에 각 노드가 연결된 형태를 기록하는 방식이다.
위와 같이 연결된 그래프를 인접 행렬로 표현할 때 파이썬에서는 2차원 리스트로 구현할 수 있다.
연결이 되어있지 않은 노드끼리는 무한의 비용이라고 작성한다.
실제 코드에서는 논리적으로 정답이 될수 없는 큰값중에서 999999999, 987654321 등의 값으로 초기화하는 경우가 많다.
이렇게 그래프를 인접행렬 방식으로 처리할 때는 다음과 같이 데이터를 초기화한다.

INF = 999999999 #무한의 비용 선언
#2차원 리스트를 이용해 인접행렬 표현
graph= [
	[0,7,5],
    	[7,0,INF],
    	[5,INF,0],
    ]
print(graph)

[[0,7,5],[7,0,999999999],[5,999999999,0]]

인접리스트 방식에서는
'연결리스트'라는 자료구조를 이용해 구현한다.

#행(Row)이 3개인 2차원 리스트로 인접 리스트 표현
graph = [[] for _ in range(3)]
#노드 0에 연결된 노드정보 저장(노드,거리)
graph[0].append((1,7))
graph[0].append((2,5))
#노드 1에 연결된 노드정보저장(노드,거리)
graph[1].append((0,7))
graph[2].append((0,5))

print(graph)

[[(1,7),(2,5)],[(0,7)],[(0,5)]]

이 두 방식은 어떤 차이가 있을까?

메모리 측면에서 보자면 인접행렬방식은 모든 관계를 저장하므로 노드 개수가 많을수록 메모리가 불필요하게 낭비된다.

반면에 인접 리스트방식은 연결된 정보만을 저장하기 때문에 메모리를 효율적으로 사용한다.

하지만 이와 같은 속성 때문에 인접리스트 방식은 인접행렬방식에 비해 특정한 두 노드가 연결되어있는지에 대한 정보를 얻는 속도가 느리다.
인접 리스트 방식에서는 연결된 데이터를 하나씩 확인해야 하기 때문이다.
그러므로 특정한 노드와 연결된 모든 인접노드를 순회해야 하는 경우, 인접 리스트 방식이 인접 행렬방식에 배해 메모리공간의 낭비가 적다.

DFS & BFS

DFS는 스택자료구조를 이용하며 구체적인 동작 과정은 다음과 같다.
1. 탐색 시작 노드를 스택에 삽입하고 방문 처리를 한다.
2. 스택의 최상단 노드에 방문하지 않은 인접 노드가 있으면 그 인접 노드를 스택에 넣고 방문처리를 한다. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼낸다.
3. 2번의 과정을 더이상 수행할 수 없을 때까지 반복한다.

노드1을 시작노드로 설정하여 DFS를 이용해 탐색을 진행하면 어떻게 될 까?

일반적으로 노드중에서 방문하지 않은 노드가 여러개 있으면 번호가 낮은 순서부터 처리한다.
DFS의 기능을 생각하면 순서와 상관없이 처리해도 되지만, 코딩테스트에서는 번호가 낮은 순서부터 처리하도록 명시하는 경우가 종종있다.
따라서 관행적으로 번호가 낮은 순서부터 처리하도록 구현하는 편이다.

1.DFS

2.BFS

3. 비교

DFS 예제

def dfs(graph, v,visited):
#현재 노드를 방문 처리
    visited[v] = True
    print(v,end=' ')
    #현재 노드와 연결된 다른 노드를 재귀적으로 방문
    for i in graph[v]:
    	if not visited[i]:
           dfs(graph, i, visited)
#각 노드가 연결된 정보를 리스트 자료형으로 표현(2차원 리스트)
graph = [ 
	[], 
	[2,3,8],
    	[1,7],
    	[1,4,5],
    	[3,5],
    	[3,4],
   	 [7],
    	[2,6,8],
    	[1,7]
    ]
#각 노드가 방문된 정보를 리스트 자료형으로 표현(1차원 리스트)
visited = [False] * 9
#정의된 DFS 함수 호출
dfs(graph,1,visited)

BFS

BFS 알고리즘은 '너비 우선 탐색' 이라는 의미를 가진다. 가까운 노드부터 탐색하는 알고리즘이다. DFS 는 최대한 멀리 있는 노드를 우선으로 탐색하는 방식으로 동작한다고 했는데, BFS는 그 반대이다.
BFS 구현에서는 선입선출 방식인 큐 자료구조를 이용하는 것이 정석이다.
인접한 노드를 반복적으로 큐에 넣도록 알고리즘을 작성.

동작 방식

  1. 탐색 시작 노드를 큐에 삽입하고 방문처리를 한다.
  2. 큐에서 노드를 꺼내 해당 노드의 인접 노드 중에서 방문하지 않은 노드를 모두 큐에 삽입하고 방문 처리를 한다.
  3. 2번의 과정을 더 이상 수행할수 없을 때까지 반복한다.
from collections import deque
def bfs(graph, start, visited):
    queue = deque([start])
    visited[start] = True
    while queue:
    	v = queue.popleft()
        print(v,end=' ')
        for i in graph[v]:
            if not visited[i]:
            	queue.append(i)
                visited[i] = True
# 각 노드가 연결된 정보를 리스트 자료형으로 표현(2차원 리스트)
graph = [
	[], 
	[2,3,8],
    	[1,7],
    	[1,4,5],
    	[3,5],
    	[3,4],
   	 [7],
    	[2,6,8],
    	[1,7]
    ]
#각 노드가 방문된 정보를 리스트 자료형으로 표현(1차원 리스트)
visited = [False] * 9

#정의된 BFS 함수 호출
bfs(graph, 1, visited)

1 2 3 8 7 4 5 6

DFS BFS 의 구현 정리

DFSBFS
동작 원리스택
구현방법재귀함수 이용큐 자료구조 이용
profile
저는 웹개발 왕이 될거에요

0개의 댓글

관련 채용 정보