반응형

* Binary Tree(이진 트리)

자식이 최대 2개만 존재하는 트리구조 

각 층별로 숫자를 매겨서 Level이라고 부르는데 레벨 값은 0부터 시작하므로 루트 노드의 레벨은 0이다.

모든 레벨이 꽉 찬 경우 이를 포화 이진 트리라고 부르며 i번째 노드의 자식은 i*2번째와 i*2+1번째 노드가 된다.

이진 트리는 1차원 배열로 구현할 수 있으며 다음 그림과 같이 구현이 가능하다.


* Binary Search Tree

이진 탐색 트리에서 효율적으로 탐색하는 방법은, 이진 트리에 데이터를 효과적으로 넣는 방법에서부터 시작이다.

이진 탐색 트리는 다음과 같은 규칙을 통해 데이터를 저장한다.

1. 이진 탐색 트리에서 노드에 저장된 키는 유일하다.

2. 루트 키의 왼쪽 서브 트리에 존재하는 모든 키는 루트 키보다 작아야 한다.

3. 루트 키의 오른쪽 서브 트리에 존재하는 모든 키는 루트 키보다 커야 한다.

이러한 조건을 만족시키며 값을 저장하기 때문에 저장과 삽입, 삭제시의 시간복잡도는 O(logN)이다.


하지만, 한쪽으로만 데이터가 저장되는 경우 배열보다 많은 메모리를 사용하면서 시간복잡도가 같은 비효율적인 상황이 발생한다.(편항 트리)

이러한 문제를 해결하기 위해 Rebalancing 기법이 등장하였는데, 이를 구현한 트리중 하나인 Red-Black Tree가 있다.


* Red-Black Tree

Red-Black Tree는 자가 균형 이진 탐색 트리로 편향 트리의 단점을 보완한 이진 탐색 트리이다.

즉, 동일한 노드의 수를 가질 때 Depth를 최소화 하여 시간 복잡도를 줄이는 것이 주 목적이다.

Red-Black Tree의 모든 노드는 Red or Black 의 색깔을 가지며 다음 조건을 만족한다.

  1. 루트 노드의 색깔은 Black이다.
  2. 모든 리프 노드의 색깔은 Black이다.
  3. Red 색깔 노드의 자식의 색깔은 Black이다.
  4. 모든 리프 노드에서 루트 노드까지 가는 경로에서 만나는 Black 노드의 수는 같다.

예를 들어가며 Red-Black Tree를 이해해보자. (참고 : https://zeddios.tistory.com/237)

추가되는 모든 노드의 색깔은 Red라고 가정하고 노드에 4,2,8,3을 추가하면 다음과 같다.

이 경우 3번 조건인 Red 색깔 노드의 자식은 무조건 Black 이여야 하는 조건을 위배한다.

이를 해결하는 전략으로 Restructuring(회전), Recoloring(색깔 변환)의 방법이 있다.

Double Red 문제를 해결하기 위해서는 현재 추가된 Node의 부모의 친척의 색깔에 따라 달라진다. 

아래 그림에서는 Z의 부모 친척 노드는 W가 된다.

## 부모 친척 노드가(W) Black인 경우는 Restructuring 과정을 거친다.

  1. 나(z)와 내 부모(v), 내 부모의 부모(Grand Parent)를 오름차순으로 정렬한다       
  2. 무조건 가운데 있는 값을 부모로 만들고 나머지 둘을 자식으로 만든다. 
  3. 올라간 가운데 있는 값을 검정(Black)으로 만들고 그 두자식들을 빨강(Red)로 만든다. 
위 과정을 거치면 4,6,7이 6을 루트 노드로 가지는 이진 트리가 되며, 여기에 2를 추가한다.

## 부모 친척 노드가(W) Red인 경우는 Recoloring 과정을 거친다.

  1. 현재 inset된 노드(z)의 부모와 그 형제(w)를 검정(Black)으로 하고 Grand Parent(내 부모의 부모)를 빨강(Red)로 한다. 
  2. Grand Parent(내 부모의 부모)가 Root node가 아니었을 시 Double Red가 다시 발생 할 수 있다. 

위 과정을 거치면  같은 트리가 구성될 것이다.

하지만, 4가 루트 노드가 아니라면 또다시 Dobule Red 문제가 발생할 수 있고, 이 경우 다시 Restructuring / Recoloring 과정을 거쳐야 한다.

이러한 과정을 거치기 때문에 Red-Black Tree의 삽입, Restructuring, Recoloring 모든 작업의 시간복잡도는 O(logN)가 된다.

반응형
반응형

#List

Class NameAddRemoveGetContains
ArrayListO(1)O(n)O(1)O(n)
LinkedListO(1)O(1)O(n)O(n)

#Set

Class NameAddContainsNext
HashSetO(1)O(1)O(h/n)
LinkedHashSetO(1)O(1)O(1)
EnumSetO(1)O(1)O(1)
TreeSetO(log n)O(log n)O(log n)

#Queue

Class NameOfferPeakPollSize
PriorityQueueO(log n)O(1)O(log n)O(1)
LinkedListO(1)O(1)O(1)O(1)
ArrayDequeueO(1)O(1)O(1)O(1)
DelayQueueO(log n)O(1)O(log n)O(1)

#Map

Class NameGetContainsKeyNext
HashMapO(1)O(1)O(h/n)
LinkedHashMapO(1)O(1)O(1)
WeakHashMapO(1)O(1)O(h/n)
EnumMapO(1)O(1)O(1)
TreeMapO(log n)O(log n)O(log n)


* 참고

http://kwseo.github.io/2015/09/24/time-complexity-about-collections/


반응형
반응형

* Graph (그래프)

그래프란 어떤 상태 혹은 객체 간의 관계를 나타내는 자료구조입니다. 그래프는 정점(Vertex)과 간선(Edge)으로 구성됩니다.
정점이란 어떠한 상태 혹은 객체를 나타냅니다. 간선은 그러한 정점 간의 관계, 그중에서도 연결성을 표현하는 요소입니다.

아래 그림은 그래프의 개념이 퍼지기 시작한 '쾨니히스베르크의 다리 건너기 문제'의 쾨니히스베르크의 다리를 도식화한 두 그림입니다.
A, B, C, D로 나눠진 4개의 구역과 각 구역을 잇는 a, b, c, d, e, f, g 7개의 다리가 있습니다. 왼쪽이 일반적으로 사용되는 그림이며, 이를 그래프 형태로 나타낸 것이 오른쪽의 그림입니다.
여기서 원으로 표현된 A, B, C, D가 정점이며 이 정점들을 연결하는 실선 a, b, c, d, e, f, g가 두 지역을 오고 갈 수 있음을 의미하는 간선이 됩니다.

<쾨니히스베르크의 다리 1>

 

<쾨니히스베르크의 다리 2>

이를 수학적으로 표현하면 아래와 같습니다.

전체그래프 G = (V, E)
정점 집합 V(G) = {A, B, C, D}
간선 집합 E(G) = {a, b, c, d, e, f, g}

이러한 정점과 간선의 관계를 표현하는 말로 인접(adjacent)과 부속(incident)이 있습니다. 정점 u, v가 있고 이 두 정점을 잇는 간선 e가 있다고 가정합시다. 이때 정점 u, v는 e로 인해 서로 인접합니다. 같은 상황에서 간선 e는 정점 u, v에 부속합니다.

그래프에는 크게 무방향 그래프(undirected graph, 무향 그래프)와 단방향 그래프(directed graph, 유향 그래프)가 있습니다.
무방향 그래프란 위의 쾨니히스베르크의 다리와 같이 모든 간선이 양방향으로 연결된 그래프입니다. 정점 u, v에 대해 무방향 그래프에서는 u->v라면 v->u이며, u->v가 불가능하다면 v->u 역시 불가능합니다.
반면 단방향 그래프는 각 간선이 방향을 가집니다. 따라서 정점 u에서 정점 v로 가는 간선 e가 있다면 이는 u->v를 의미할 뿐, v->u를 의미하는 것은 아닙니다.
단방향 그래프에서 u->v를 연결하는 간선이 있을 때 u는 v에 인접한다, v는 u로부터 인접하다 라고 표현할 수 있습니다.

<무방향 그래프>

 

<단방향 그래프>

간선은 따로 이름을 주지 않더라도 부속한 두 정점으로 표현할 수 있습니다. 무방향 그래프의 경우 u와 v를 연결하는 간선은 (u, v)와 같이 표현합니다. 단방향 그래프의 경우 u에서 나가 v로 들어오는 간선을 로 표현합니다.
따라서 위의 무방향 그래프 G1과 단방향 그래프 G2를 아래와 같이 표현할 수 있습니다.

G1 = (V, E)
V(G1) = {A, B, C}
E(G1) = {(A, B), (B, C)}

 

G2 = (V, E)
V(G2) = {A, B, C}
E(G2) = {<A, B>, <B, C>}

그래프에서 쓰이는 또 다른 개념으로는 차수(degree)가 있습니다. 차수란 각 정점에 부속된 간선의 수로, 무방향 그래프에서 어떤 정점 v의 차수란 v에 부속된 간선의 개수입니다. 단방향 그래프에서 정점의 차수는 입력차수(in-degree)와 출력차수(out-degree)로 나누어집니다. 정점 v의 입력차수는 v로 들어오는 간선의 수이며 출력차수는 v에서 나가는 간선의 수와 같습니다.

이러한 그래프를 코드로 표현하는 데에는 크게 두 가지 방법이 사용됩니다. 첫 번째는 인접 행렬(adjacency matrix)을 이용한 표현입니다. 인접 행렬이란 각 정점이 다른 정점과 연결된 상태에 관한 정보를 나타내는 정사각행렬입니다. 예를 들어, 정점 u에서 나가 정점 v로 들어오는 간선이 있다면 1, 아니면 0으로 인접 행렬을 채웁니다. 무방향 그래프의 경우, u->v간선이 v->u 간선과 동일하므로 대각선(diagonal)을 중심으로 대칭(symmetric)됩니다. 아래는 정점이 4개인 단방향 그래프의 예시입니다.

<인접 행렬을 이용한 단방향 그래프의 표현>

두 번째 방법은 인접 리스트(adjacency list)를 이용한 표현입니다. 인접 리스트란 한 정점과 인접한 정점들을 리스트로 표현하는 형태입니다.
아래는 정점이 4개인 단방향 그래프에서 정점 u에서 나가 v로 들어오는 간선들의 정보를 인접 리스트로 나타낸 예시입니다.

<인접 리스트를 이용한 단방향 그래프의 표현>

그렇다면 두 개의 표현법은 어떻게 다를까요?
2차원 인접 행렬의 이름을 adj_matrix, 인접 리스트의 이름을 adj_list라고 할 때 두 표현법은 아래와 같은 장단점을 가집니다. 
두 정점 u, v가 인접한지를 확인하는 연산을 생각해봅시다. 인접 행렬의 경우 이 연산은 adj_matrix[u][v]의 값을 참조하기만 하면 됩니다. 즉 O(1)의 시간복잡도를 가집니다. 
하지만 인접 리스트는 adj_list[u]에 연결된 원소들을 순회하며 v가 존재하는지 확인해야 합니다. 최악의 경우 이 연산은 O(|V|)의 시간복잡도를 가집니다. 
반면 정점 u에 연결된 모든 정점을 탐색하는 상황을 생각해봅시다. 인접 행렬의 경우 어떤 정점들이 연결되어있는지 알기 위해서 모든 adj_matrix[u][i]를 참조해야 합니다. 따라서 O(|V|)의 시간이 걸립니다. 
하지만 인접 리스트의 경우에는 adj_list[u]에 연결된 정점의 개수만큼만 걸립니다. 물론 최악의 경우 O(|V|)지만 이 탐색이 전체 그래프로 확장된다면 달라집니다. 
인접 행렬의 경우에는 O(|V|^2)을 모두 돌아야 하지만, 인접 리스트의 경우 O(|E| + |V|)가 됩니다. 이는 각 표현에 사용된 모든 값을 참조하기 때문인데, 인접 행렬의 공간복잡도는 O(|V|^2)이며 인접 리스트의 공간복잡도는 O(|E|)입니다. 
따라서 각 정점 간의 연결관계 참조가 잦다면 인접 행렬 표현이, 순회가 잦거나 |V|가 큰 그래프에서는 인접 리스트의 표현이 유리합니다.


* 출처 & 참고 링크

Samsung CodeGround

BFS & DFS

최소 신장 트리(MST, Minimum Spanning Tree)

반응형
반응형

* Tree (트리)

트리는 자식과 부모의 관계로 이루어진 계층적인 구조입니다. 필요에 따라 다양한 종류로 나뉘게 되는데 이번 문서에서는 제일 간단한 트리인 이진 트리에 대해서 설명하려고 합니다. 먼저 이진 트리에서 사용하는 용어들을 정리해보면 다음과 같습니다.

Root : 트리에서 가장 최상위에 존재하는 노드

Child : 어떠한 노드의 자식 노드

Parent : 어떠한 노드의 부모 노드

Siblings : 같은 부모를 갖는 형제 노드

Leaf/Terminal : 자식 노드를 갖지 않는 노드

Branch/Internal : 자식 노드를 적어도 1개 이상 갖는 노드

Degree : 노드가 가지고 있는 자식 노드의 개수

Height : 해당 노드부터 Leaf 노드까지의 가장 긴 거리

Level : 트리 각 층의 단계 (루트 노드의 경우 1)

위에서 트리에서 사용하는 용어들을 알아보았습니다. 여러분의 이해를 돕기 위해 아래의 그림을 통해서 용어들을 설명하도록 하겠습니다.

<그림 1> 이진 트리

1번 노드를 self 현재 노드라고 한다면, 0은 1번 노드의 Parent입니다.( 0은 트리의 최상단에 위치하기 때문에 root 노드입니다). 그리고 3번과 4번 노드는 1의 자식 노드들 입니다. 2번 노드는 1번 노드의 부모인 0번 노드의 자식 노드이므로 sibling입니다.(자매 노드). 또한 자식이 하나도 존재하지 않으면 Leaf/Terminal 노드입니다. 이러한 노드들은 그림에서 연두색으로 표시하였습니다. 만약 자식이 1명이라도 존재한다면 Branch/ Internal 노드입니다. 이러한 노드들은 그림에서 파랑색으로 표현했습니다. 1번 노드의 Degree는 자식 노드의 수인 2입니다. Height는 1번 노드보다 하위 노드들 중 가장 아래에 있는 노드 7번 노드와의 거리인 2입니다. 1번 노드의 level은 2입니다.(루트 노드는 Level이 1)

이제 위에서 정리했던 용어들의 설명이 다 끝났습니다.

다음으로 트리의 탐색 방법에 대해서 설명을 하도록 하겠습니다. 트리 노드들의 값들을 확인하기 위해선 어떤 방법을 사용할까요? 만약 트리가 선형구조로 되어 있다면 그냥 포인터를 하나 두고 한 칸 씩 뒤로 옮기면서 확인하면 되었겠지만 아쉽게도 트리는 비선형 구조입니다. 따라서 일반적인 선형탐색 방법으로는 모든 노드들을 확인할 수 없습니다. 트리에서는 이러한 탐색과정을 Traversal(순회)이라고 합니다. 순회의 방법에는 여러가지가 있지만 대표적으로 사용하는 방법은 3가지가 있습니다. 그 방법은 Pre-Order traversal(전위 순회), In-Order traversal(중위 순회), Post-Order traversal(후위 순회) 입니다. 이 3가지 순회의 차이점은 방문 시 수행할 기능을 어느 시점에 호출하는가 입니다.

먼저 Pre-Order의 수행 방법입니다. (방문 시 수행할 기능은 값 출력이라고 하겠습니다.)

Pre-Order(현재 트리 노드의 위치 cur){
  
  Print(cur’s value)
  
  If(cur’s left exist){
	Pre-Order(cur->left)
  }
  If(cur’s right exist){
	Pre-Order(cur->right)
  }
}
	

두 번째로 In-Order의 수행 방법입니다.

In-Order(현재 트리 노드의 위치 cur){
  If(cur’s left exist){
	In-Order(cur->left)
  }
  
  Print(cur’s value)
  
  If(cur’s right exist){
	In-Order(cur->right)
  }
}
	

마지막으로 Post-Order의 수행 방법입니다.

Post-Order(현재 트리 노드의 위치 cur){
  If(cur’s left exist){
	Post-Order(cur->left)
  }
  If(cur’s right exist){
	Post-Order(cur->right)
  }
  
  Print(cur’s value)
  
}
	

위의 3가지 트리 순회를 보면 Print, 즉 노드를 방문했을 때 수행할 기능의 순서만 변화하는 것을 알 수 있습니다. 그러면 위에서 사용했던 예시인 <그림 1>을 이용하겠습니다.

트리의 순회는 root에서부터 시작합니다.

먼저, Pre-Order(0 : root)을 실행했을 때 출력 결과에 대해서 말씀드리겠습니다. Pre-Order는 자기 자신-왼쪽-오른쪽 순서로 작업을 수행하는 방식입니다.

(1) 루트인 0번 노드에서 출발하므로 0을 출력합니다. 그리고 왼쪽 노드가 존재하므로 Pre-Order(1)을 호출합니다.

(2) 1번 노드에서 1을 출력하고 마찬가지로 왼쪽 노드가 존재하므로 Pre-Order(3)을 호출합니다.

(3) 3번 노드에서 3을 출력하고, 왼쪽 노드가 존재하므로 Pre-Order(7)을 호출합니다.

(4) 7번 노드에서 7을 출력하게 됩니다. 7번 노드는 왼쪽 노드가 없습니다. 따라서 오른쪽 노드를 확인합니다. 하지만 오른쪽 노드 역시 존재하지 않으므로 Pre-Order(7)은 종료됩니다.

(5) 다시 Pre-Order(3)으로 돌아갑니다.(재귀 함수) 방금 Pre-Order(3)에서 왼쪽 노드를 확인했기 때문에 오른쪽 노드를 확인합니다. 하지만 오른쪽 노드가 존재하지 않으므로 Pre-Order(3) 역시 종료됩니다.

(6) Pre-Order(1)로 돌아가게 됩니다. 마찬가지로 왼쪽 노드를 확인했으므로 오른쪽 노드를 확인하는데 4번 노드가 오른쪽 노드이므로, Pre-Order(4)를 호출합니다.

(7) 4번 노드에서 4가 출력됩니다. 4번 노드도 7번과 같이 Leaf/Terminal 노드이므로 그냥 종료됩니다.

(8) Pre-Order(1)로 돌아가고 Pre-Order(1)도 종료되어 Pre-Order(0) 으로 돌아갑니다. 이러한 방식으로 방문하게 되면 출력결과는 0-1-3-7-4-2-5-6으로 출력하게 됩니다.

이번에는 In-Order를 실행했을 때 출력 결과에 대해서 말씀드리겠습니다. In-Order는 왼쪽-자기 자신-오른쪽 순서로 작업을 수행하는 방식입니다.

(1) 우선 루트인 0번 노드에서 출발합니다. 하지만 Pre-Order와는 다르게 이때 0을 출력하지 않고 왼쪽 자식 노드의 유무를 확인합니다. 이때 왼쪽 노드가 존재하므로 In-Order(1)을 호출합니다.

(2) 1번 노드에서도 1을 바로 출력하지 않고, 왼쪽 노드가 존재하므로 In-Order(3)을 호출합니다.

(3) 3번 노드에서도 3에서도 3을 바로 출력하지 않고, 왼쪽 노드가 존재하므로 In-Order(7)을 호출하게 됩니다.

(4) 7번 노드는 Leaf/Terminal 노드이므로 왼쪽 노드가 없어 7을 출력하고 오른쪽 노드의 유무를 하지만 오른쪽 노드도 없으므로 종료됩니다.

(5) 다시 In-Order(3)으로 돌아가서 3을 출력하고 오른쪽 노드를 확인하지만 존재하지 않으므로 종료됩니다.

(6) 다시 In-Order(1)로 돌아가서 1을 출력하고 오른쪽 노드로 4가 존재하므로 In-Order(4)를 호출합니다.

(7) 4번 노드 역시 Leaf/Terminal 노드이므로 4를 출력하고 종료됩니다.

(8) 다시 In-Order(1)로 돌아가서 종료되고, In-Order(0)으로 돌아가서 0을 출력합니다.

이러한 방식으로 방문하게 되면 출력값은 7-3-1-4-0-5-2-6이 됩니다. 이것은 x좌표 순으로 정렬했을 때의 순서와 동일합니다. 따라서 자신보다 작은 값은 왼쪽 서브 트리, 큰 값은 오른쪽 서브 트리에 저장하는 이진 탐색 트리라면 In-Order Traversal 방식을 이용했을 때 정렬된 값이 나오게 됩니다.

마지막으로 Post-Order를 설명하겠습니다. 이 방식은 왼쪽-오른쪽-자기자신 순서로 수행하는 방식입니다.

(1) 우선 루트인 0번 노드에서 시작하고, 왼쪽 노드가 존재하므로 Post-Order(1)을 호출합니다.

(2) 1번 노드에서도 왼쪽 노드가 존재하므로 Post-Order(3)을 호출합니다.

(3) 3번 노드 역시 왼쪽 노드가 존재하므로 Post-Order(7)을 호출합니다.

(4) 7번 노드는 Leaf/Terminal 노드이므로 7을 출력하고 종료됩니다.

(4) 다시 Post-Order(3)으로 돌아가서 3번 노드는 오른쪽 노드가 없으므로 3을 출력하고 종료됩니다.

(5) 1번 노드는 오른쪽 노드가 존재하므로 Post-Order(1)로 돌아가서 오른쪽 노드인 Post-Order(4)을 호출합니다.

(4) 4번 노드는 Leaf/Terminal 노드이므로 4를 출력하고 종료됩니다.

(5) 다시 Post-Order(1)로 돌아가서 1을 출력하고 종료됩니다.

이와 같은 방식으로 방문하면 출력값은 7-3-4-1-5-6-2-0이 됩니다.

이러한 트리의 순회 방법들로 트리에서 원하는 값을 탐색할 수 있습니다. 그러면 다음으로는 트리의 삽입에 대해서 알아보도록 하겠습니다.

이진 트리로 설명을 하자면, 각 노드의 왼쪽 오른쪽 노드를 저장하기 위해 아래와 같은 2차원 배열을 만들어줍니다.

Child[Node_Num][LR] : Node_Num은 해당 노드의 번호, LR은 0일 경우 왼쪽 자식 노드 1일 경우 오른쪽 자식 노드를 의미합니다. 따라서 0번 노드의 왼쪽 노드가 1번 노드라는 것을 표현하고 싶다면 Child[0][0] = 1과 같이 쓰면 되고, 0번 노드의 오른쪽 노드가 3번 노드라는 것을 표현하고 싶다면 Child[0][1] = 3과 같이 쓰면 됩니다.

그리고 만약 트리 노드마다 값이 존재한다면 1차원 배열인 Value를 만들어주어 각 노드의 값을 표현해주면 됩니다.


* 출처 & 참고 링크

Samsung CodeGround

최소 신장 트리(MST, Minimum Spanning Tree)

반응형

'∙Data Structure' 카테고리의 다른 글

[Data Structure] 자료구조별 시간복잡도  (0) 2019.02.01
[Data Structure] Graph (그래프)  (0) 2019.01.17
[Data Structure] Hashing (해싱)  (0) 2019.01.17
[Data Structure] Heap (힙)  (0) 2019.01.17
반응형

* Hashing (해싱)

해싱은 임의의 길이의 데이터(키, Key)를 고정된 길이의 데이터(해시값, Hash value)로 변환해 작은 크기의 해시 테이블로 대응(Mapping)시켜 식별하는 하나의 기법입니다. 해시 테이블은 M개의 버킷으로 이루어져 있으며, 이 글에서 다루는 해시값은 해당 키가 저장될 버킷의 번호(해시 테이블의 인덱스)를 나타냅니다.

키에서 해시값을 추출하는 일련의 과정을 해시 함수(Hash function)라고 합니다. 해시 함수는 같은 키에 대해서는 동일한 해시값을, 다른 키에 대해서는 다른 해시값을 추출해야 합니다. 하지만 일반적으로 해싱에서 해시값의 범위(M)는 키의 범위보다 작기 때문에 어떤 이상적인 해시 함수라도 비둘기집의 원리에 의해 서로 다른 두 키가 같은 해시값을 가질 수 있습니다. 이런 경우를 충돌(Collision)이라고 합니다.

해싱이 사용되는 예로 문자열(Key)을 정숫값(Hash value)으로 치환하여 문제를 해결하는 방법이 있습니다. 이에 사용되는 가장 대표적인 해시 함수는 진법을 이용하는 것입니다. "SCPC"이라는 단어가 있다고 하면, 아래와 같이 진법을 이용한 해시 함수를 만들 수 있습니다.

f(key) = ((((key[0]) * 26 + key[1]) * 26 + key[2])) * 26 … key[l – 1])
f("SCPC") = 19 * 26^3 + 3 * 26^2 + 16 * 26^1 + 3 * 26^0 = 336391

해시 함수의 시간 복잡도를 O(H)라고 할 때 해시값의 중복이 없는 이상적인 해싱에서 키의 검색, 삽입, 삭제에는 모두 O(H)의 시간이 걸립니다.
하지만 앞서 말했듯 해시값의 범위가 키의 범위보다 작을 때에는 충돌이 발생할 수밖에 없습니다.
예를 들어 위와 같은 진법 변환을 해시 함수로 사용할 때에는 문자열이 길어지면 해시값이 너무 커지므로 적당히 큰 수로 나머지 연산한 값, 이 글에서는 해시 테이블의 크기 M으로 나머지 연산한 값을 해시값으로 쓸 수 있습니다.
이때 해시값의 M에 대한 나머지가 같은 키끼리는 충돌이 발생합니다.

해시 함수와 마찬가지로 충돌을 제어하는 방법도 다양합니다. 여기에서는 대표적인 충돌 제어 방법 중 하나인 체이닝(Chaining)에 대해서 설명하겠습니다. 
체이닝은 충돌한 키들을 보존하기 위해 각 버킷을 리스트 형태로 구현합니다. 최초의 버킷은 모두 원소가 0개인 리스트의 헤더이며, 해당 버킷에 데이터가 추가될 때마다 노드를 추가합니다. 
이때 어떤 키가 해시 테이블에 존재하는지 검사하기 위해서는 해당 키의 해시값에 해당하는 버킷이 가진 노드를 모두 순회해야 합니다. 
이는 리스트에서 원소를 찾는 연산과 동일하며 이에 기반한 삭제 연산도 같은 선형 시간이 걸립니다. 
이때 몇 개의 버킷에만 데이터가 편중되는 최악의 경우 각 연산의 시간 복잡도는 O(H + N)이 됩니다.

이런 상황을 최소한으로 하기 위해서는 해시값의 추출 과정, 즉 해시 함수를 어떻게 구성할 것인지가 핵심이 됩니다.
좋은 해시 함수란 해시값을 추출하는 연산이 빠르되 충돌이 적고 해시 테이블의 영역을 고르게 사용할 수 있어야 합니다.
예를 들어 위에서 다룬 A * pow(B) mod M 형태의 진법 변환을 사용할 때 A의 값을 0이 아닌 1부터 시작하는 것이 좋습니다.
0을 사용하게 되면 "A"와 "AAA"의 해시값이 같아지기 때문입니다. 또한 B와 M이 서로소인 것이 충돌 확률이 낮아 더 좋은 해시 함수가 될 수 있습니다.

해시 함수의 구현과 충돌 제어 방법, 좋은 해시 함수의 조건에 관해서는 굉장히 많은 자료가 있으므로 보다 깊은 이해를 위해 직접 찾아보시기 바랍니다.

글의 첫 부분에서 말했듯 해싱의 핵심은 값의 식별입니다. 그렇기에 원소의 중복을 허용하지 않는 Set이나 Key:Value쌍에서 중복된 Key가 존재하면 안 되는 Map과 같은 자료구조를 구현하는 데에 사용되기도 합니다.


* 출처 & 참고 링크

Samsung CodeGround

반응형

'∙Data Structure' 카테고리의 다른 글

[Data Structure] Graph (그래프)  (0) 2019.01.17
[Data Structure] Tree (트리)  (0) 2019.01.17
[Data Structure] Heap (힙)  (0) 2019.01.17
[Data Structure] Stack (스텍)  (0) 2019.01.17
반응형

* Heap (힙)

힙(heap) 은 최댓값 또는 최솟값을 빠르게 찾아낼 수 있는 트리형 자료구조입니다.

힙은 완전이진트리 형식을 따르며 모든 부모 노드의 값이 자식 노드들의 값과 일정한 대소 관계를 가지게 되는 규칙을 가지고 있습니다.

또한 자식노드 사이의 상관관계는 없으므로 유의하여야 합니다.

부모 노드의 값이 자식 노드의 값보다 크다면 최대 힙(Max Heap), 부모 노드의 값이 자식 노드의 값보다 작다면 최소 힙(Min Heap) 으로 부릅니다.

힙의 규칙에 따라 트리의 가장 상단에는 최댓값 또는 최솟값이 있는 것이 자명하기 때문에, O(1) 만에 최댓값과 최솟값을 찾을 수 있습니다.

<최소 힙의 예>

힙의 삽입은 트리의 가장 마지막 부분에 데이터를 삽입한 후, 부모 노드와 삽입 부분 노드의 대소 관계를 확인하여 힙의 규칙에 맞도록 부모 노드를 탐색, 스왑하는 과정으로 이루어져 있습니다.

트리의 마지막에 추가

부모노드와 비교하여 스왑

힙의 조건을 만족함

힙의 삭제는 삭제할 원소를 삭제 한 후 트리의 가장 마지막 원소를 삭제된 원소의 위치에 넣어준 후 힙의 조건을 만족하는 적절한 위치에 이를 수 있도록 조정해주면 됩니다.

제일 마지막 원소를 삭제된 원소의 자리로 옮긴다.

힙의 규칙에 맞도록 부모와 자식노드를 바꾸어 준다.

삭제가 완료된 힙

힙의 삽입과 삭제의 시간 복잡도는 최악의 경우 최하단 노드로부터 최상단 노드까지 스왑이 필요하게 됩니다. 따라서 O(log N)의 시간 복잡도를 가집니다.

완전이진트리를 사용하기 때문에 최대 사이즈의 배열로 선언 후 쉽게 구현하실 수 있습니다.


* 출처 & 참고 링크

Samsung CodeGround

Stack & Queue & PriorityQueue

반응형

'∙Data Structure' 카테고리의 다른 글

[Data Structure] Tree (트리)  (0) 2019.01.17
[Data Structure] Hashing (해싱)  (0) 2019.01.17
[Data Structure] Stack (스텍)  (0) 2019.01.17
[Data Structure] Queue (큐)  (0) 2019.01.17
반응형

* Stack (스텍)

스택은 선형 구조이며, 마지막으로 삽입된 값이 가장 먼저 나오는 LIFO(Last in First Out)으로 되어 있습니다. 이 때 삽입하는 과정을 push라고 하며 값을 빼내는 과정을 pop이라고 합니다. 예를 들면 a, b, c 순서로 push하고 스택에 있는 값들을 모두 pop하면 c, b, a 순서로 나오게 되는 것입니다. 또한 스택에는 size, top, empty라는 함수가 있으며 size는 스택에 들어있는 값들의 수를 반환하는 함수이고, top은 마지막으로 삽입된 값을 반환하는 함수, empty는 스택이 비어 있는지 여부를 반환하는 함수입니다.

아래의 두 그림은 stack의 push, pop 과정을 보여주는 그림입니다.

(A) stack push

<그림 1> stack push

(B) stack pop

<그림 2> stack pop

그리고 이러한 스택을 만드는 방법에는 2가지 방법이 있습니다. 하나는 배열을 이용한 방법이고, 다른 하나는 링크드 리스트를 이용한 방법입니다.

1) 배열을 이용한 방법에서는 삽입될 위치인 스택의 마지막 부분을 저장하는 변수 top이 필요합니다. 이 변수를 이용하여 스택의 크기(스택에 들어있는 요소의 수)를 빠르게 알 수 있습니다. 또한 스택의 크기가 0일 경우 스택이 비어 있는 것이므로 비어 있는지 여부도 쉽게 알 수 있습니다. 따라서 배열로 구현하기 위해서는 값들을 담을 배열과 스택의 마지막 위치(삽입될 위치)를 저장하는 변수가 필요합니다.

배열로 구현한 스택의 push 와 pop 과정

<그림 3> stack implemented as an array

2) 링크드 리스트를 이용한 방법에서는 값과 다음 노드의 위치를 저장하는 스택 노드들을 이용할 것이며 마지막으로 삽입된 노드의 위치를 저장하는 노드형 포인터 top과 노드들의 수를 저장하는 변수 size가 필요합니다. Head 초기화 시 처음에 널 값을 넣어주고, push할 경우 새로 삽입되는 노드의 next가 이전의 top이 되고, NewNode가 top이 됩니다.

링크드 리스트로 구현한 스택의 push 와 pop 과정

<그림 4> stack implemented as linked list



* 출처 & 참고 링크

Samsung CodeGround

Stack & Queue & PriorityQueue

반응형

'∙Data Structure' 카테고리의 다른 글

[Data Structure] Hashing (해싱)  (0) 2019.01.17
[Data Structure] Heap (힙)  (0) 2019.01.17
[Data Structure] Queue (큐)  (0) 2019.01.17
[Data Structure] LinkedList (연결리스트)  (0) 2019.01.17
반응형

* Queue (큐)

큐는 선형 구조이며, 삽입된 순서대로 값이 나오는 FIFO(First in First Out)로 되어 있습니다. 이때 삽입하는 과정을 enqueue라고 하며 값을 빼내는 과정을 dequeue라고 합니다.
예를 들면 a, b, c 순서로 enqueue하고 큐에 있는 값들을 모두 dequeue하면 a, b, c 순서로 나오게 됩니다.
또한 큐에는 size, front, empty라는 함수가 있으며 size는 큐에 들어있는 값들의 수를 반환하는 함수이고, front은 큐에서 가장 먼저 삽입된 값을 반환하는 함수, empty는 큐가 비어 있는지 여부를 반환하는 함수입니다.

<그림 1> enqueue

<그림 2> dequeue

그리고 이러한 큐를 만드는 방법에는 2가지 방법이 있습니다. 하나는 배열을 이용한 방법이고, 다른 하나는 링크드 리스트를 이용한 방법입니다.

1) 배열을 이용한 방법에서는 삽입될 위치인 큐의 마지막 부분을 저장하는 변수 rear와 큐의 처음 부분을 저장하는 변수 front가 필요합니다. 또한 큐의 데이터 개수를 저장하는 변수 size를 이용하여 큐의 크기(큐에 들어있는 요소의 수)를 빠르게 알 수 있습니다. 또한 큐의 크기가 0일 경우 큐가 비어 있는 것이므로 비어 있는지 여부도 쉽게 알 수 있습니다. 따라서 배열로 구현하기 위해서는 값들을 담을 배열과 큐의 처음 위치와 마지막 위치(삽입될 위치)를 저장하는 변수가 필요합니다. 또한 enqueue과정과 dequeue과정에서 각각 rear index, front index가 점점 뒤로 밀려나기 때문에 이를 효율적으로 구성하려면 환형 배열로 구현해주어야 합니다.

<그림 3> 환형 배열로 구현한 큐의 dequeue

<그림 4> 환형 배열로 구현한 큐의 enqueue

2) 링크드 리스트를 이용한 방법에서는 값과 다음 노드의 위치를 저장하는 큐 노드들을 이용할 것이며 마지막으로 삽입된 노드의 위치를 저장하는 노드형 포인터 rear와 큐의 가장 앞 노드의 위치를 저장하는 노드형 포인터 front, 노드들의 수를 저장하는 변수 size가 필요합니다. front와 rear는 초기화시 처음에 널 값을 넣어주고, enqueue할 경우 이전에 rear의 next는 새로 삽입하는 노드가 되고, 새로 삽입되는 노드가 rear가 됩니다. dequeue할 경우 이전의 Head의 next가 Head가 되고, 이전의 head는 메모리 할당 해제 시킵니다.

<그림 5> 링크드 리스트로 구현한 queue



* 출처 & 참고 링크

Samsung CodeGround

Stack & Queue & PriorityQueue

반응형

'∙Data Structure' 카테고리의 다른 글

[Data Structure] Heap (힙)  (0) 2019.01.17
[Data Structure] Stack (스텍)  (0) 2019.01.17
[Data Structure] LinkedList (연결리스트)  (0) 2019.01.17
[Data Structure] ArrayList vs LinkedList  (0) 2018.12.27