본문 바로가기

Study/Algorithm Study

[Algorithm] 탐색 트리

+ 항공대학교 이인복 교수님의 알고리즘 해석 및 설계 과목 내용를 정리한 글입니다.

탐색 트리 문제의 정의

집합을 표현하는 자료 구조

- 문제를 풀기 위해서 수학의 집합의 개념을 이용할 수 있다.

(집합이란 어떤 답이 '있다', '없다'를 알 수 있어야 한다.)

- 집합을 어떻게 표현할 것인가?

 

집합에 대한 연산

1. membership query => O(n)

- 어떤 원소가 집합에 존재하는지를 판별할 수 있는 연산

 

2. insertion (삽입) => O(1)

- 배열 맨 뒤에 삽입

 

3. deletion (삭제) => O(n)


탐색 트리

레코드

= 개체에 대한 모든 정보를 묶은 저장 단위

ex) 주민등록번호, 이름

 

= 각 레코드를 대표할 수 있는 값

+ 정수로 가정한다. (같은 키를 같는 노드는 없다.)

 

탐색 트리

= 각 노드마다 하나의 키를 가지는 트리

+ 해당 레코드가 저장된 위치를 찾을 수 있다.

 

이진 탐색 트리

= 각 노드가 최대 두 개의 자식을 갖는 탐색 트리

+ 왼쪽 자식은 부모보다 키가 작고, 오른쪽 자식은 부모보다 키가 크다.

 

ex) {8, 3, 10, 1, 6, 14, 4, 7, 13}

이진 탐색 트리 예

 

서브 트리

= 특정 노드와 그 이하의 노드로만 이루어진 트리 (재귀적 구조)

위 예시의 서브 트리

 

이진 탐색 트리는 유일하지 않다.

 

1. 어떤 키 값이 자주 접근될지 모른다면, 균형잡힌 트리가 유리하다. (보통 유리)

=>  n개의 노드가 존재할 때, 가장 낮은 높이(log n)의 이진 탐색 트리

높이: 루트부터 가장 먼 노드까지 edge의 개수

 

2. 어떤 키 값이 자주 접근될지 알 수 있다면, 균형이 잡히지 않은 트리가 유리하다.

ex) {1, 2, 3, 4}

균형 잡힌 트리
균형 잡히지 않은 트리


이진 탐색 트리에서 검색

이진 탐색 트리에 원하는 값 k가 들어 있는가?

 

=> 재귀적으로 구현한다.

 

1. 트리가 비어 있다면 k는 트리에 있을 수 없다.

 

루트부터 아래 과정을 반복한다.

 

2 - 1. 만약 해당 값이 원하는 값 k라면, 해당 값을 반환한다.

2 - 2. 만약 해당 값이 k보다 크다면, 해당 값은 루트의 왼쪽 서브 트리에 존재한다.

2 - 3. 만약 해당 값이 k보다 작다면, 해당 값은 루트의 오른쪽 서브 트리에 존재한다.


이진 탐색 트리에서 삭제

삽입에 비해 삭제는 상대적으로 어렵다.

 

이진 탐색 트리에 한 키를 삽입하면 확실히 그 결과가 이진 탐색 트리이다.

but,  한 노드를 삭제하면, 그 결과가 이진 탐색 트리가 아닐 수도 있다.

 

< 이진 탐색 트리에서 가장 간단한 삭제 >

1. 이진 탐색 트리를 순회하면서 저장되어 있는 모든 값을 하나씩 읽는다.

2 - 1. 읽은 값이 삭제할 값이 아니라면, 새로운 이진 탐색 트리에 하나씩 삽입한다.

2 - 2. 읽은 값이 삭제할 값이라면, 건너뛴다.

 

=> 시간 복잡도, 공간 복잡도: O(n)

 

< 이진 탐색 트리에서 더 효율적인 삭제 >

삭제할 노드가 무엇인가에 따라서 세 가지 경우가 존재한다.

 

+ 최대한 트리의 모양을 바꾸지 않는 것이 좋다.

1. 삭제할 노드가 리프일 경우 (자식이 없다.)

=> 그냥 지우면 된다.

 

2. 삭제할 노드가 자식 하나인 경우

=>  노드를 지우고, 자식을 원래 노드의 위치로 옮긴다.

 

3. 삭제할 노드가 자식 둘인 경우

1. 노드의 키 값보다 바로 앞에 있거나, 바로 뒤에 있는 노드를 찾는다.

+ 바로 앞에 있는 노드: 삭제할 노드의 왼쪽 서브트리의 가장 오른쪽 노드

+ 바로 뒤에 있는 노드: 삭제할 노드의 오른쪽 서브트리의 가장 왼쪽 노드

 

2. 찾은 노드가 삭제할 노드의 위치로 오고, 다시 이 노드를 지우는 과정을 반복한다. (재귀: 1, 2번 경우)

+ 찾은 노드는 반드시 자식이 하나인 노드이다. 그렇지 않으면 삭제할 노드와 가장 가까운 노드가 아니다.


레드 블랙 트리 (Red Black Tree)

이진 탐색 트리의 경우, 한 쪽으로 노드들이 쏠릴 수 있다.

- 어떻게 하면 노드들이 자동으로 균형이 잡힐 것인가?

- 트리의 삽입, 삭제, 검색 연산은 모두 트리의 높이 h에 비례하는 시간이 걸린다.

=> h = O(log n)을 만족하도록 할 수 있는가?

 

< 레드 블랙 트리의 특성 >

=> 이진 탐색 트리에 추가적인 성질이 있다.

 

1. 각 노드는 Red 또는 Black 둘 중 한 가지 색을 갖는다.

 

2. 루트는 Black이다.

 

3. 노드의 색이 Red이면, 자식의 색이 Red일 수 없다

+ 나머지 3가지 경우는 모두 가능하다. 

 

4. 루트부터 NULL까지 모든 경로에서 만나는 Black 노드의 수는 항상 일정한다. (Black Height)


레드 블랙 트리에서 삽입

이진 탐색 트리에서 삽입과 동일하나, 색 때문에 추가적인 절차가 들어간다.

+ 검색은 색을 제외하고 이진 탐색 트리와 방법이 같다.

 

1. 일단, 새로 삽입된 노드는 Red이다. (default, 처음 삽입은 Black Node)

=> Black Height를 맞추는데 유리하다. (4번 조건을 위해)

 

2. 삽입된 노드의 부모의 색이 Red라면 2가지 경우로 나눠서 처리한다.

+ 삽입된 노드의 부모의 부모의 색은 무조건 Black이다.

 

2 - 1. 삽입된 노드의 부모의 부모의 두 자식 중 하나만 Red인 경우 (또는 자식이 하나인 경우)

=> 4가지 경우의 수 모두 1가지 경우의 수로 수정할 수 있다.

2 - 1 의 예

 

2 - 2. 삽입된 노드의 부모의 부모의 두 자식이 모두 Red인 경우

=> 색을 반전시키킨다.

2 - 2의 예

+ Special Case: 만약 C가 루트라면, C를 Black으로 바꾼다. (규칙을 어기지 않는다.)

+ C의 부모가 Red라면, 다시 2 - 1 or 2 - 2 방법으로 해결한다.

 

< 시간 분석 >

루트에서 리프까지 거리 (Black Height)는 가장 긴 경우에도 가장 짧은 경우의 2배 이하이다.

+ 가장 짧은 경우: 모두 Black Node인 경우+ 가장 긴 경우: Black Node와 Red Node가 번갈아 있을 경우

 

트리의 노드 개수가 n일 때, Black Height를 bh라고 하면,

=> 4^bh - 1 (Black Height가 가장 긴 경우)  ≥ n2^bh - 1 (Black Height가 가장 짧은 경우)

=> 계산하면,  log(n+1)  ≥ bh ≥ (1 / 2) × log(n + 1) 

트리의 높이 h는 bh ≤ h ≤ 2bh 이므로, 삽입, 탐색 모두 최대 2 × log(n + 1) 시간 내에 수행할 수 있다.


B - Tree

지금까지 데이터는 메인 메모리에 저장되어 O(1) 시간에 접근할 수 있다고 가정했다.

 

그러나, 실제 대용량 데이터는 HDD, SSD 등에 저장된다.

- SSD는 회전 문제는 없지만 상대적으로 메인 메모리에 비해 접근 속도가 느린 것은 동일하다.

 

Seek Time: 헤드를 원하는 데이터가 있는 섹터로 이동하는데 걸리는 시간

- 운이 좋으면, 현재 헤드 밑에 원하는 섹터가 있는 경우이다.

- 운이 나쁘면, 현재 헤드 바로 앞에 원하는 섹터가 있는 경우이다. (한 바퀴를 돌려야 한다.) 

 

데이터를 블럭 단위로 저장한다.

+ 블럭 하나 당 100개의 키를 넣을 수 있다.

 

=> 한 번 데이터에 접근하는 시간이 많이 걸리고 어짜피 블럭 단위로 저장된다면, 노드에 많은 정보를 넣고, 트리의 높이를 줄이는 것이 좋다.

 

즉, HHD, SSD 등의 접근을 줄이는 균형 잡힌 다진 검색 트리B - Tree를 이용한다.

 

< B - Tree 특징 >

1. 한 노드에 하나 이상의 키 값이 들어올 수 있다.

-  t개의 키 값이 저장되어 있다면, 이 노드의 자식은 t + 1개이다.

ex) {1, 2, 3} 노드의 자식: {-∞ ~ 1}, {1 ~ 2}, {2 ~ 3}, {3 ~ }  

 

2. 루트를 제외한 모든 노드는 k / 2 ~ k개의 키를 갖는다.

 

3. 모든 리프 노드는 루트에서부터 거리가 같다.


B - Tree에서 탐색과 삽입

한 노드에 둘 이상의 키가 저장될 수 있기 때문에 복잡하게 진행된다.

 

< 탐색 >

루트부터 아래 과정을 반복한다.

 

1 - 1. 찾고자 하는 키가 현재 노드에 포함되어 있다면 현재 노드가 답이다.

 

1 - 2. 포함되어 있지 않다면, 범위를 보고 다음에 방문할 노드를 결정한다,

- 한 노드가 최대 k개의 키를 저장하기 때문에 여러 값과 비교한다.

ex) 33은 25와 34 사이에 존재하므로 4번째 노드에 방문한다.

33을 탐색하는 예제

 

키가 저장될 노드를 찾았을 때,

 

1. 찾은 노드에 키를 저장할 수 있다면 저장한다.

 

2. 이미 k개의 키를 저장하고 있기 때문에 불가능하다면, 이 노드의 형제 노드에게 키를 넘길 수 있는지 확인한다.

 

3. 만약 형제 노드에게 키를 넘길 수 없다면, 중간 값을 기준으로 키들을 두 그룹으로 나누고 중간 값은 부모에게 전달한다.

 

4. 부모에게도 재귀적으로 진행한다.

 

ex) 한 노드에 키 값을 최소 2개, 최대 5개까지 저장할 수 있는 B - Tree의 삽입 예시

 

1) 31을 삽입한다.

- 31의 범위에 해당하는 노드안의 키 개수가 4개이므로 1개 더 삽입할 수 있다.

31 삽입

 

2) 5를 삽입한다.

- 5의 범위에 해당하는 노드안의 키 개수가 5개이므로 1개 더 삽입할 수 없다.

- 가까운 형제 노드의 키 개수를 확인한다. 3개이므로 형제 노드에 키 1개를 더 추가할 수 있다.

- 6이 바로 형제 노드로 이동한다면 B - Tree의 성질이 깨지므로, 6을 부모 노드로 이동시키고, 7을 형제 노드로 이동시킨다.

- 5를 범위에 해당하는 노드에 삽입한다.

 

3) 39를 삽입한다.

- 39의 범위에 해당하는 노드안의 키 개수가 5개이므로 1개 더 삽입할 수 없다.

- 가까운 형제 노드의 키 개수를 확인한다. 5개이므로 형제 노드에 키 1개를 더 추가할 수 없다.

- 범위에 해당하는 키를 두 그룹으로 나누고 중간값인 40을 부모에게 전달한다.

 

4) 32를 삽입한다.

- 32의 범위에 해당하는 노드안의 키 개수가 5개이므로 1개 더 삽입할 수 없다.

- 가까운 두 형제 노드 모두 키가 이미 5개이므로 형제 노드에 키 1개를 더 추가할 수 없다.

- 범위에 해당하는 키를 두 그룹으로 나누고 중간값인 31을 부모 노드로 전달한다.

 

- 그러나 부모 노드 역시 키가 이미 5개이므로 추가 과정이 필요하다.

- 루트 노드는 형제 노드가 없으므로, 형제 노드에 키를 추가 할 수 없다.

- 루트 노드의 키들을 두 그룹으로 나누고 중간 값인 31을 부모 노드로 전달한다.

(즉, 새로운 루트가 생성되고, 1개의 노드가 추가된다.)


B - Tree에서 삭제

삽입의 반대과정이다.

 

1. 지워질 키 값이 리프 노드로 이동한다.

 

1 - 1. 지워질 키가 속한 노드가 리프 노드라면 해당 노드에서 키 값을 제거한다.

 

1 - 2. 리프 노드가 아니라면, 지워질 키의 바로 다음 값이 저장된 리프 노드를 찾고 값을 서로 바꾼다.

+ 지워질 키 값이 리프 노드로 이동한다.

+ 지워질 키의 바로 다음 값: 지워질 키 값과 가장 가까운 값을 가진 키 값

 

2. 리프 노드에서 키 값을 제거한다.

 

2 - 1. 키 값을 제거했을 때, k / 2개 미만의 키가 남는다면, 키 값을 넘겨줄 수 있는 형제 노드를 찾는다.

 

2 - 2. 키 값을 넘겨줄 수 있는 형제 노드가 없다면, 형제 노드와 지워질 키 가 속한 리프 노드를 합병한다.

+ 노드를 합병하면 부모 노드의 키가 줄기 때문에 이 과정이 반복된다.

 

ex) 한 노드에 키 값을 최소 2개, 최대 5개까지 저장할 수 있는 B - Tree의 삭제 예시

 

1) 7을 삭제한다.

- 7이 해당한 노드가 리프 노드이고, 3개의 키 값을 갖고 있으므로 그냥 삭제해도 문제가 생기지 않는다.

 

2) 4를 삭제한다.

- 4가 있는 노드는 리프 노드가 아니므로 4를 삭제하기 위해 바로 다음 값인 5가 저장된 리프 노드를 찾고 5와 위치를 변경한다

 

- 위치를 변경한 리프 노드에서 4를 삭제한다.

 

- 각 노드는 최소 2개 이상의 키 값을 가져야 하므로 형제 노드에서 값을 하나 빌린다.

- 왼쪽에 있는 형제 노드가 3개의 키 값을 갖고 있으므로 그 중 가장 큰 키 값 3을 빌린다.

- 삽입과 마찬가지로, 3이 삭제한 노드로 이동한다면 B - Tree의 성질이 깨지므로, 3을 부모 노드로 이동시키고, 5를 삭제한 노드로 이동시킨다.

 

3) 9를 삭제한다.

- 각 노드는 최소 2개 이상의 키 값을 가져야 하므로 형제 노드에서 값을 하나 빌려야 한다.

- 그러나 두 형제 노드 역시 2개의 키 값을 갖고 있으므로 빌릴 수 없다.

 

- 삭제할 노드와 형제 노드 1개를 2개 이상의 키 값을 갖는 노드 하나로 병합한다.

- 그러나 이 과정에서 B - Tree의 조건을 만족하기 위해 부모 노드에 있는 키 값 한 개가 병합되어야 하고, 부모 노드의 키 값이 2개 미만이 된다.

 

- 다시 형제 노드, 부모 노드로 병합하여 4개의 키값을 갖는 1개의 노드를 생성한다.


Red - Black Tree 와 B - Tree의 관계

2-3-4 Tree: 한 노드에 최대 4개의 자식이 올 수 있는 B-Tree이다. 

+ 키는 1개 or 2개 or 3개, 자식은 2개, 3개, 4개

 

Red - Black Tree는 2-3-4 Tree를 이진 트리로 표현한 것이다.

 

B-Tree의 3가지 노드를 Red - Black Tree의 3가지 노드로 표현

 

위 그림의 노드로 Red-Black Tree를 구성하면 Red-Black Tree의 조건을 모두 만족할 수 있다.

+ Red Node는 Black Node의 자식이므로 Black Node가 B - Tree의 Node 하나라고 생각하면, B - Tree를 Red - Black - Tree로 변환하더라도 B - Tree였던 Node를 구분할 수 있다.