티스토리 뷰
Tree와 Graph
비선형 자료구조의 특별한 경우
📍 Tree
나무의 형태를 가진 단방향 그래프의 한 구조
하나의 뿌리로부터 가지가 사방으로 뻗은 형태
데이터가 바로 아래에 있는 하나 이상의 데이터에 한 개의 경로와 하나의 방향으로만 연결
→ 계층적 자료구조
계층적으로 표현이 되고, 아래로만 뻗어나감 → 사이클이 없다.
- 사이클 : 시작 노드에서 출발해 다른 노드를 거쳐 다시 시작 노드로 돌아오는 것
➡︎ 트리는 사이클이 없는 하나의 연결 그래프
🌳 Tree 구조와 특징
루트라는 하나의 꼭짓점 데이터를 시작으로 여러 개의 데이터를 간선으로 연결한다.
각 데이터를 노드(Node)라고 하며, 두 개의 노드가 상하 계층으로 연결되면 부모/자식 관계를 맺는다.
- A는 B와 C의 부모노드
- B와 C는 A의 자식노드
- 자식이 없는 노드는 리프 노드
Tree는 깊이와 높이, 레벨 등을 측정할 수 있다.
- 깊이 (Depth)
- 루트로부터 하위 계층의 특정 노드까지의 깊이를 표현
- 루트 노드는 깊이가 0(지면)
- 레벨 (Level)
- 같은 깊이를 가지고 있는 노드를 묶어 레벨로 표현
- 루트 노드의 레벨은 1
- 같은 레벨에 나란히 있는 노드를 형제 노드라고 함
- 높이 (Height)
- 리프 노드를 기준으로 루트까지의 높이를 표현
- 부모 노드의 높이는 자식 노드의 가장 높은 높이 값 +1
- 서브 트리 (Sub tree)
- 루트에서 뻗어 나오는 큰 트리 내부의 트리 구조를 갖춘 작은 트리
용어 정리
노드(Node) : 트리 구조를 이루는 모든 개별 데이터
루트(Root) : 트리 구조의 시작점이 되는 노드
부모 노드(Parent node) : 두 노드가 상하관계로 연결되어 있을 때 상대적으로 루트에서 가까운 노드
자식 노드(Child node) : 두 노드가 상하관계로 연결되어 있을 때 상대적으로 루트에서 먼 노드
리프(Leaf) : 트리 구조의 끝 지점이고, 자식 노드가 없는 노드
🌳 Tree 실사용 예제
컴퓨터의 디렉토리 구조
모든 폴더는 하나의 폴더(루트 폴더, /)에서 시작되어 가지를 뻗어나가는 모양새를 띈다.
- 하나의 폴더 안에 여러 개의 폴더가 존재
- 여러 개의 폴더 안에 또 다른 폴더나 파일이 존재
또 다른 예시
- 토너먼트 대진표, 가계도, 조직도 …
📍 Binary Search Tree
트리 구조는 가지고 있는 특징에 따라 여러 이름으로 불린다.
🌳 이진 트리 (Binary Tree)
자식 노드가 최대 두 개인 노드로 구성된 트리
자식 노드는 왼쪽 자식 노드와 오른쪽 자식 노드로 나눈다.
자료의 삽입, 삭제 방법에 따라
- 정 이진 트리(Full binary tree)
- 완전 이진 트리(Complete binary tree)
- 포화 이진 트리(Perfect binary tree)
로 나뉜다.
🌳 이진 트리 특징
- 정 이진 트리
- 각 노드가 0개 혹은 2개의 자식 노드를 가짐
- 포화 이진 트리
- 정 이진 트리이면서 완전 이진 트리인 경우
- 모든 리프 노드의 레벨이 동일하고, 모든 레벨이 가득 채워져있음
- 완전 이진 트리
- 마지막 레벨을 제외한 모든 노드가 가득 차있어야함
- 마지막 레벨의 노드는 전부 차 있지 않아도 됨. 단, 왼쪽은 채워져야함
➡︎ 이진 트리는 이진 탐색 트리와 이진 힙 구현에 사용되며, 효율적인 검색과 정렬을 위해 사용된다.
🌳 이진 탐색 트리 (Binary Search Tree)
이진 탐색 속성이 이진 트리에 적용된 형태의 이진 트리
이진 탐색 알고리즘 정렬된 데이터 중에서 특정한 값을 찾기 위한 탐색 알고리즘 중 하나로 트리의 루트 노드는 이진 탐색에서 리스트의 중간값이 된다.
루트 노드를 기준으로 이진 탐색 알고리즘에 기반하여
- 왼쪽 서브 트리의 값은 루트 노드의 값보다 작은 값들
- 오른쪽 서브 트리의 값은 루트 노드의 값보다 큰 값들
이 자리잡고 있어야 한다.
이진 탐색이란?
오름차순으로 정렬된 정수의 배열을 같은 크기의 두 부분 배열로 나눈후,
두 부분 중 탐색이 필요한 부분에서만 탐색하도록 탐색 범위를 제한하여 원하는 값을 찾는다.
📍 탐색 과정
1. 배열의 중간에 찾고자 하는 값이 있는지 확인
2. 중간값이 찾고자 하는 값이 아닐 경우, 오름차순으로 정렬된 배열에서 중간값보다 큰 값인지 작은 값인지 판단
3. 찾고자 하는 값이 중간값보다 작을 경우, 배열의 맨 앞부터 중간값 전까지의 범위를 탐색 범위로 잡고 탐색을 반복 수행
4. 찾고자 하는 값이 중간값보다 클 경우, 배열의 중간값 다음 값부터 맨 마지막까지를 탐색 범위로 잡고 탐색을 반복 수행
➡︎ 이진 탐색 트리는
- 각 노드에 중복되지 않는 키(Key)가 있다.
- 루트 노드의 왼쪽 서브 트리는 해당 노드의 키보다 작은 키를 갖는 노드들로 이루어진다.
- 루트 노드의 오른쪽 서브 트리는 해당 노드의 키보다 큰 키를 갖는 노드들로 이루어진다.
- 좌우 서브 트리도 모두 이진 탐색 트리여야한다.
⚠️ 이진 탐색 트리는 균형이 잡힌 트리가 아닐때, 입력되는 값의 순서에 따라 한쪽으로 노드들이 몰리게 될 수 있다. ⚠️
균형이 잡히지 않은 트리는 탐색하는 데 시간이 더 걸리는 경우가 있기 때문
→ 삽입과 삭제마다 트리의 구조를 재조정하는 과정을 거치는 알고리즘을 추가할 수 있다.
🌳 이진 탐색 트리 특징
기존 이진 트리보다 탐색이 빠르다.
- 이진 탐색 트리의 연산은 트리의 높이가 h라면 o(h)의 복잡도를 가지게 된다.
📍 효율적인 연산이 가능한 이유?
→ 탐색 과정을 보면 알 수 있음
📍 이진 탐색 트리의 탐색 과정
- 루트 노드의 키와 찾고자 하는 값을 비교
- 찾고자 하는 값과 같으면 탐색 종료
- 찾고자 하는 값이 루트 노드의 키보다 작다면 왼쪽 서브 트리로 탐색 진행
- 찾고자 하는 값이 루트 노드의 키보다 크다면 오른쪽 서브 트리로 탐색 진행
이러한 탐색 과정을 값을 찾을 때까지 반복하며, 최대 트리의 높이(h)만큼 탐색을 진행하게 된다. (값을 찾지 못하는 경우)
📍 Tree Traversal
트리 순회 특정 목적을 위해 트리의 모든 노드를 한 번씩 방문하는 것
모든 노드를 순회하는 방법으로
- 전위 순회
- 중위 순회
- 후위 순회
가 있다.
순회 방식들은 모두 노드를 순회할 때 왼쪽부터 오른쪽으로 조회한다.
🌀 전위 순회(preorder traverse)
주로 트리를 복사할 때 사용
- 루트를 먼저 방문
- 왼쪽 노드들을 순차적으로 탐색
- 왼쪽 노드 탐색이 끝나면 오른쪽 노드 탐색
→ 부모 노드가 제일 먼저 방문되는 순회 방식
🌀 중위 순회(inorder traverse)
이진 탐색 트리의 오름차순으로 값을 가져올 때 쓰임
루트를 가운데 두고 순회
- 제일 왼쪽 끝에 있는 노드부터 순회
- 왼쪽에 있는 노드의 순회가 끝나면 루트를 방문
- 루트를 거쳐 오른쪽에 있는 노드로 이동하여 탐색
→ 부모 노드가 서브 트리의 방문 중간에 방문되는 순회 방식
🌀 후위 순회(postorder traverse)
트리를 삭제할 때 사용
루트를 가장 마지막에 순회
- 제일 왼쪽 끝에 있는 노드부터 순회
- 루트를 거치지 않고 오른쪽으로 이동해 순회
- 마지막에 루트를 방문
→ 자식 노드가 먼저 삭제되어야 상위 노드를 삭제할 수 있음
🌀 레벨 순회(levelorder traverse)
트리의 레벨 기준으로 노드들을 방문하는 순회 방법
- 루트 노드에서 시작 (레벨 1)
- 아래로 뻗어나가며 노드들을 방문 (아래로 갈수록 레벨+1)
- 동일한 레벨에 여러 노드가 존재할 경우 왼쪽에서 오른쪽 순서로 방문
📍 순회 방식을 나누는 이유?
이진 트리 탐색의 경우는 간단한 편이지만 순회 방법은 복잡한 편이다.
모든 노드를 방문하기 위해서는 일정한 조건이 필요하고, 트리 구조를 유지보수하거나 특정 목적을 위해서도 순회 방법에 대한 정의는 필수적으로 필요하다.
🖇️ Graph
여러 개의 점이 서로 복잡하게 연결된 관계를 표현한 자료구조
📍 Graph 표현 방식
인접 행렬
두 정점을 바로 이어주는 간선이 있는 경우 두 정점은 인접하다.
서로 다른 정점들이 인접한 상태인지를 표시한 행렬 (2차원 배열의 형태)이다.
- 정점이 이어져 있다면 1 (true)
- 이어져 있지 않다면 0 (false)
- 가중치 그래프의 경우 1대신 관계에서 의미있는 값을 저장
- A의 진출차수 : 1개 (A→C)
- B의 진출차수 : 2개(B→A, B→C)
- C의 진출차수 : 1개(C→A)
인접 리스트
각 정점이 어떤 정점과 인접하는지를 리스트의 형태로 표현
각 정점마다 하나의 리스트를 가진다.
- 리스트는 자신과 인접한 다른 정점을 담고 있음
📍 그래프를 인접 리스트로 구현할 때, 정점 별로 살펴봐야 할 우선순위를 고려해 구현할 수 있다.
리스트에 담긴 정점들을 우선순위별로 정렬할 수 있지만, 우선 순위가 없다면 연결된 정점들을 단순하게 나열한 리스트가 된다.
우선순위를 다뤄야 한다면 더 적합한 자료구조(queue, heap)를 사용하는 것이 합리적이므로 보통 순서를 중요하지 않다. (단, 예외는 있음)
인접 행렬과 인접 리스트는 각각 언제 사용할까?
📍 인접 행렬
- 한 개의 큰 표와 같은 모습을 한 인접 행렬은 두 정점 사이에 관계가 있는지, 없는지 확인하기에 용이하다.
- A에서 B로 진출하는 간선이 있는지 파악하기 위해선 0번째 줄의 1번째 열에 어떤 값이 저장되어있는지 바로 확인가
- 최단 경로를 구하는 과정(BFS)에서는 그래프 탐색이 빈번하게 발생하는데, 이때 인접행렬이 인접리스트에 비해 조회 성능이 우수함가장 빠른 경로(shortest path)를 찾고자 할 때 주로 사용된다.
- 인접행렬의 경우 인덱스를 직접 접근하여 조회가 O(1)로 이루어지기 때문
➡︎ 인덱스를 통한 직접 접근이 가능한 인접행렬이 최단경로를 찾는 데 더 유리한 측면이 있다.
인접리스트의 경우 A 노드에서 B 노드로 이동하는 경우만 해도 O(N)의 시간이 소요되며, 더불어 최단 경로를 구하는 과정 자체에서도 시간이 많이 소요되기 때문이다.
반면, 인접리스트의 경우 각 row를 선형 조회해야 하므로 노드의 수가 N일 경우 O(N)의 시간이 소요된다.
📍 인접 리스트
- 메모리를 효율적으로 사용하고 싶을 때 인접 리스트를 사용한다.
- 인접 행렬은 연결 가능한 모든 경우의 수를 저장하기 때문에 상대적으로 메모리를 많이 차지
알아둬야 할 Graph 용어들
- 정점 (vertex): 노드(node)라고도 하며 데이터가 저장되는 그래프의 기본 원소
- 간선 (edge): 정점 간의 관계를 나타냄 (정점을 이어주는 선)
- 인접 정점 (adjacent vertex): 하나의 정점에서 간선에 의해 직접 연결된 정점
- 가중치 그래프 (weighted Graph): 연결의 강도(추가적인 정보, ex. 서울-부산으로 가는 거리 등)가 얼마나 되는지 적혀져 있는 그래프
- 비 가중치 그래프 (unweighted Graph): 연결의 강도가 적혀져 있지 않는 그래프
- 무(방)향 그래프 (undirected graph): 내비게이션 예제는 무(방)향 그래프.
- 서울에서 부산으로 갈 수 있듯, 반대로 부산에서 서울로 가는 것도 가능. 하지만 단방향(directed) 그래프로 구현된다면 서울에서 부산으로 갈 수 있지만, 부산에서 서울로 가는 것은 불가능(혹은 그 반대). 만약 두 지점이 일방통행 도로로 이어져 있다면 단방향인 간선으로 표현할 수 있음
- 진입차수 (in-degree) / 진출차수 (out-degree): 한 정점에 진입(들어오는 간선)하고 진출(나가는 간선)하는 간선이 몇 개인지를 나타냄
- 인접 (adjacency): 두 정점 간에 간선이 직접 이어져 있다면 이 두 정점은 인접한 정점
- 자기 루프 (self loop): 정점에서 진출하는 간선이 곧바로 자기 자신에게 진입하는 경우 자기 루프를 가졌다 라고 표현. 다른 정점을 거치지 않는다는 것이 특징
- 사이클 (cycle): 한 정점에서 출발하여 다시 해당 정점으로 돌아갈 수 있다면 사이클이 있다고 표현. 내비게이션 그래프는 서울 —> 대전 —> 부산 —> 서울로 이동이 가능하므로, 사이클이 존재하는 그래프
📍 Graph 실사용 예제
포털 사이트의 검색 엔진, SNS에서 사람들과의 관계, 내비게이션 등에서 사용
- 서울에서 대전에 들린 다음 부산으로 가는 경우
- 정점 : 서울, 대전, 부산
- 간선 : 서울-대전, 대전-부산, 부산-서울
- 간선은 내비게이션에서 이동할 수 있음을 나타낸다.
➰ 만약 여기에 토론토를 정점으로 추가한다면?
자동차로는 토론토에서의 이동이 불가능하므로 다른 정점과의 간선을 추가할 수 없다. 이러한 경우를 관계가 없다고 표현
→ 하나라도 정점이 연결되어 있지 않은 그래프를 비연결 그래프라고 함
간선으로는 정점 사이의 관계가 있다는 것만 알 수 있다.
- 얼마나 떨어져 있는지는 알 수 없음
→ 비 가중치 그래프
- 추가적인 정보를 파악할 수 없는 가중치(연결의 강도가 얼마인지)가 적혀있지 않은 그래프
let isConnected = {
seoul: {
busan: true,
daejeon: true
},
daejeon: {
seoul: true,
busan: true
},
busan: {
seoul: true,
daejeon: true
}
}
console.log(isConnected.seoul.daejeon) // true
console.log(isConnected.daejeon.busan) // true
- 내비게이션의 경우, 각 도시 간의 거리를 표시해야하므로 가중치 그래프로 바꿔야한다.
- 정점: 서울, 대전, 부산
- 간선: 서울—140km—대전, 대전—200km—부산, 부산—325km—서울
🖇️ BFS와 DFS
그래프의 모든 정점 탐색 방법
BFS(Breadth-First Search)
너비 우선 탐색 너비를 먼저 탐색하는 방법
- 주로 두 정점 사이의 최단 경로를 찾을 때 사용한다.
- 경로를 하나씩 전부 방문한다면, 최악의 경우 모든 경로를 다 살펴봐야 한다.
📍 최단 경로를 찾을 때 BFS방식을 사용하는 이유?
BFS는 현재 있는 노드에서 가까운 곳부터 탐색하는 도중 가장 먼저 발견한 해답이 최단 거리라는 보장이 되기 때문
DFS(Depth-First Search)
깊이 우선 탐색 깊이를 먼저 탐색하는 방법
- 한 정점에서 시작해서 다음 경로로 넘어가기 전에 해당 경로를 완벽하게 탐색할 때 사용
- BFS보다 탐색 시간은 조금 오래 걸리지만 모든 노드를 완전히 탐색할 수 있다.
❓Question
다음의 질문에 대한 답을 고민해 보세요.
- DFS와 BFS의 장단점은 또 무엇이 있을까요?
- BFS 장점 : 노드 수가 적고 깊이가 얕을 때 유리. 답이 되는 경로가 여러 개인 경우에도 최단 경로를 보장.
- BFS 단점 : 노드의 수가 많을수록 필요없는 노드들까지 저장해야 하므로 더 큰 저장공간 필요.
- DFS 장점 : BFS에 비해 저장공간의 필요성이 적음.(백트래킹 해야하는 노드만 저장)
- DFS 단점 : 답이 아닌 경로가 매우 깊을 경우 그 경로에 깊이 빠질 우려가 있음. 찾은 경로가 최단 경로라는 보장이 없음
- 그래프가 매우 크다면 어떤 탐색 기법을 고려해야 할까요?
- DFS
- 반대로, 그래프의 규모가 작고, depth가 얕다면 어떤 탐색 기법을 고려해야 할까요?
- BFS
'코딩 > 코드스테이츠' 카테고리의 다른 글
[React] - 번들링과 웹팩 (0) | 2023.03.20 |
---|---|
[HTML / CSS] - 반응형 웹 (0) | 2023.03.17 |
[자료구조] - Stack , Queue (0) | 2023.03.15 |
[인증 / 보안] - OAuth (0) | 2023.03.09 |
[인증 / 보안] - Token (0) | 2023.03.08 |