MIT introduction to Algorithm (6.006) Lecture 6을 기반으로 정리한 내용입니다.
BST의 문제점
BST에서 Unbalanced tree가 만들어지는 경우, 시간복잡도가 너무 커져서 의미가 없었다. 하지만 Balanced를 유지한다면, 시간복잡도 $O(h)$ 는 안정적이게 $O(lgn)$ 가 될 것이다.(원소의 개수가 n개이고, 한번 내려갈 때마다 2번씩 나뉘면 $log_2n$ 번 내려갔을때 끝까지 내려가므로)
Balanced BST maintains $h = O(lgn) \Rightarrow$ all operations run in $O(lgn)$ time.
AVL 트리
Structure
BST와 같은 구조에서 Balanced를 유지하기 위해 각 노드의 height
를 key
와 함께 저장한다. 이때 height
는 해당 노드에서 가장 먼 leaf
에 도착하면서 거치는 노드의 수이며 max{left.height, right.height} + 1
의 방식으로 계산할 수 있다. 또한 NULL
을 가리키는 경우엔, height = -1
으로 정하면 수식에 맞아 떨어진다.
이렇게 정한 height
를 이용하여 Balanced를 유지할 수 있는데, 모든 노드의 height(left)
와 height(right)
가 최대 $\pm 1$ 만큼만 차이난다면, 즉 항상 $| h_l - h_r | \le 1$ 로 만들어 준다면 될 것이다.
간단한 증명)
최악의 경우는 모든 노드에서 오른쪽 서브트리의 높이가 왼쪽 서브트리의 높이 + 1 인 경우일 것이다.
이때 $N_h$ 를 height가 h인 AVL-tree의 최소한의 노드의 개수라고 하자.
$\Rightarrow N_h = N_{h-1} + N_{h-2} + 1 > 2N_{h-2}$
$\Rightarrow N_h>2^{h/2}$
$\Rightarrow h < 2lgN_h$ 가 되므로
$O(h) \approx O(lgn)$ 이 된다…!
insert
- BST와 같은 방법으로 값을 넣는다.
- 바뀐 구조를 AVL 구조로 잡아준다. (rotation 이용!)
일반적인 예시)
-
Node
x
가 AVL tree의 성질을 위반하는 가장 낮은 노드라고 가정 -
x.right
의height
가 더 크다고 가정 -
만약
x.right
가 right-heavy거나 balanced이면left-rotate(x)
, 그렇지 않으면right-rotate(x.right) and left-rotate(x)
를 통해 AVL로 만들 수 있다.-
right-heavy
-
balanced
-
left-heavy
-
-
x.left
의height
가 더 크다면, symmetric하게 생각하면 된다.
AVL 트리는 요즘에 잘 안쓰이고 Red-Black tree나 다른 종류의 트리가 쓰인다고 한다. 그래서 강의와 책 모두 비중있게 다루지 않고 이런 성질을 가지고 있다~ 라는 선에서 끝난다. 따라서 자세한 설명은 생략하고 다른 operation 역시 BST와 유사한 방법으로 실행한 뒤 AVL로 만들어주면 된다.
AVL을 이용한 정렬
- AVL에 n번 넣으면 잘 정렬이 되며 $\theta(nlgn)$ 의 시간복잡도를 가진다.
- 중위순회를 통하여 탐색을 할 수 있고 $\theta(n)$의 시간복잡도를 지닌다.
Code (c)
코드가 너무 길어져서 헤더파일과 2개의 파일로 나눠서 작성하였다.
헤더
간단한 매크로와 구조체를 정의한 파일. height
를 구조체 멤버로 같이 데려갔다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef AVL_H
# define AVL_H
#include <stdio.h>
#include <stdlib.h>
#define MAX(x,y) (((x) < (y)) ? (y):(x))
#define ABS(x) (((x) < 0) ? (-(x)) : (x))
typedef struct s_avl{
int key;
int height;
struct s_avl *parent;
struct s_avl *left;
struct s_avl *right;
} t_avl;
void addHeight(t_avl **avl, t_avl *nil);
t_avl *makeAVL(t_avl **avl, t_avl *NIL);
#endif
메인 소스
AVL트리의 틀을 잡아주는 코드이며 각 구조체의 초기화와 데이터삽입, 프린트와 메모리 헤제를 하였다. height
를 이용하고 segmentation fault를 방지하기 위해NIL
구조체를 정의하여서 NULL
대신 사용하였다. 이전에 공부했던 BST와 크게 다르지 않다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include "avl.h"
void initNode(t_avl *newnode,t_avl *nil, int key)
{
newnode->parent = nil;
newnode->left = nil;
newnode->right = nil;
newnode->key = key;
newnode->height = 0;
}
void inOrdertoPrint(t_avl *avl, t_avl *nil)
{
if (avl != nil)
{
inOrdertoPrint(avl->left, nil);
printf("height:%d, ", avl->height);
printf("key: %d\n", avl->key);
inOrdertoPrint(avl->right, nil);
}
}
void addHeight(t_avl **avl, t_avl *nil)
{
if (*avl != nil)
{
addHeight(&(*avl)->left, nil);
addHeight(&(*avl)->right, nil);
(*avl)->height = MAX((*avl)->left->height, (*avl)->right->height) + 1;
}
}
void insertNode(t_avl **root, t_avl *nil, int key)
{
t_avl *y;
t_avl *x;
t_avl *newnode;
newnode = (t_avl*)malloc(sizeof(t_avl));
initNode(newnode, nil, key);
y = nil;
x = *root;
while (x != nil)
{
y = x;
if (key < x->key)
x = x->left;
else x = x->right;
}
newnode->parent = y;
if (y == nil)
*root = newnode;
else if (newnode->key < y->key)
y->left = newnode;
else y->right = newnode;
}
void postOrdertoFree(t_avl *avl, t_avl *nil)
{
if (avl != nil)
{
postOrdertoFree(avl->left, nil);
postOrdertoFree(avl->right, nil);
free(avl);
}
}
t_avl *makeNil(t_avl *nil)
{
nil->parent = NULL;
nil->left = NULL;
nil->right = NULL;
nil->key = 0;
nil->height = -1;
return (nil);
}
int main()
{
t_avl **root;
t_avl *nil;
int i;
int j = 0;
nil = (t_avl*)malloc(sizeof(t_avl));
nil = makeNil(nil); //setting nil
root = (t_avl**)malloc(sizeof(t_avl*));
*root = nil;
srand(42);
while (j < 50) // 1 ~ 100 까지 수 중에서 50개를 트리에 삽입
{
i = rand() % 100 + 1;
printf("%d번째 입력 : %d, ",j, i);
insertNode(root, nil, i);
j++;
}
addHeight(root, nil);
*root = makeAVL(root, nil);
inOrdertoPrint(*root, nil);
postOrdertoFree(*root, nil); //free 하기 위한 후위 순회
free(root);
printf("\n");
}
AVL 구조를 만들어주는 코드
위 강의 내용과 교재를 참고하여 AVL구조를 만들었다. 구글링 해본 결과 rotate
을 LL, LR 등의 케이스로 나눠서 하는 방법이 대다수였고 height
를 그때그때 계산하는 방법도 있었다. LL, LR등을 사용하는 코드가 더 깔끔하지만, 새로운 방법으로 짜보고 싶어서 이러한 방법을 사용하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include "avl.h"
void leftRotate(t_avl **avl, t_avl *nil)
{
t_avl *y;
t_avl *x;
x = *avl;
y = x->right;
x->right = y->left;
if (y->left != nil) //nil이면 parent를 설정하지 않음
y->left->parent = x;
if (x->parent == nil) //x의 parent의 자식을 y로 바꾸는 과정
y->parent = nil;
else if (x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
y->parent = x->parent; // y의 parent을 x의 parent로 변경
y->left = x;
x->parent = y; // 서로 연결
x->height = MAX(x->left->height, x->right->height) + 1;
y->height = MAX(y->left->height, y->right->height) + 1;
}
void rightRotate(t_avl **avl, t_avl *nil)
{
t_avl *x;
t_avl *y;
x = *avl;
y = x->left;
x->left = y->right;
if (y->right != nil)
y->right->parent = x;
if (x->parent == nil)
y->parent == nil;
else if (x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
y->parent = x->parent;
y->right = x;
x->parent = y;
x->height = MAX(x->left->height, x->right->height) + 1;
y->height = MAX(y->left->height, y->right->height) + 1;
}
void updateHeight(t_avl **avl, t_avl *nil)
{
t_avl *x;
x = *avl;
while (x ->parent != nil)
{
x->parent->height = MAX(x->parent->left->height, x->parent->right->height) + 1;
x = x->parent;
}
}
void buildAVL(t_avl **avl, t_avl *nil)
{
if (*avl != nil)
{
buildAVL(&(*avl)->left, nil);
buildAVL(&(*avl)->right, nil);
if (ABS((*avl)->right->height - (*avl)->left->height) <= 1)
{
return ;
}
else if ((*avl)->right->height > (*avl)->left->height)
{
if ((*avl)->right->left->height > (*avl)->right->right->height)
{
rightRotate(&(*avl)->right, nil);
leftRotate(avl, nil);
}
else
leftRotate(avl, nil);
updateHeight(avl, nil);
}
else
{
if ((*avl)->left->right->height > (*avl)->left->left->height)
{
leftRotate(&(*avl)->left, nil);
rightRotate(avl, nil);
}
else
rightRotate(avl, nil);
updateHeight(avl, nil);
}
}
}
t_avl *makeAVL(t_avl **avl, t_avl *nil)
{
t_avl *root;
buildAVL(avl, nil);
root = *avl;
while (root->parent != nil)
root = root->parent;
return (root);
}
원본 코드는 깃허브 에 있으며 srcs폴더에 소스코드, includes 폴더에 헤더파일이 있다. (Linux환경 gcc 컴파일러 사용)
삽질🤦
AVL 역시 공부할때는 어렵지 않았으나, 코드로 구현하면서 난관에 봉착했다. height
를 넣는 것부터 쉽지 않았는데, height
는 밑에서부터 계산하므로 BST때와 비슷하게 후위 순회를 하려고 했다. 하지만 이번엔 값을 대입해야 하므로 구조체를 이중 포인터로 받아와서 연산을 진행하였는데, 탈출 조건부터 문제가 생겼다. 기존의 후위 순회 처럼 *avl!=NULL
을 조건으로 주니 *avl
의 자식 노드들에 접근하면서 avl
의 크기를 벗어나 Segmentation falut가 떴고, *avl
과 avl
둘 다 조건을 넣어도 자식 노드에 접근하면서 비슷한 오류가 발생했다. NIL
을 만들어서 해결하려면 모든 코드를 뜯어고쳐야 했기에 return
을 해서 해결하려 했으나, 재귀함수의 특성상 한번만 return
하면 될걸 여러번 해야 한다는 점이 너무 찝찝했다. 결국 NIL
을 만들어서 해결하기로 했다.
NIL
을 만드니 쉽게 문제가 해결되었다. 처음에 NIL
을 피하려던 이유가 각 자식노드에 NIL
을 만들어 주려면 parent
가 다 다르니 여러개를 만들어야 해서 공간도 많이 차지하고 복잡해 진다고 생각했었다. 하지만 도식 상 빈 자식노드의 left, right
가 각각 NULL
을 가리키고 있었고, 실제 코드에선 하나의 포인터 NULL
을 가리키고 있기 때문에 NULL
을 NIL
로 바꾸기만 하면 될 문제였고 따라서 공간도 하나만 더 차지하는 셈이었다.
rotate
를 하면서 height
를 업데이트 하다 보니 시간복잡도가 생각보다 커지는 문제가 발생하였다. rotate
를 후위 순회를 통해 모든 노드를 탐색하면서 height
가 불균형일 경우 rotate
을 호출 했는데, rotate
가 한번 이루어지면 다시 모든 height
를 업데이트를 하는 방식으로 하니 이중반복문 꼴이 되었다. 이렇게 되면 AVL을 쓰는 의미가 사라져서, rotate
후 height
가 바뀌면 그 부모노드만 영향을 받는 점에서 착안하여 부모노드만 업데이트 하는 방향으로 하였다.
ADT(Abstract Data Type, 추상자료형)
ADT는 배열이나 연결리스트 처럼 특정한 자료 구조가 아니라 좀 더 추상적으로 만들어진 자료구조이며 코드를 짜기 전 pseudo code를 구상해 보는 것과 비슷한 맥락으로 이해했다. 예를 들어 어떤 데이터를 저장, 탐색 그리고 삭제를 하고 싶다면 ADT는 이와 같은 operation을 하는 형태를 뜻하고 이를 배열이나 연결리스트 등 특정한 형태로 만든다면 자료구조가 되는 것이다. 따라서 하나의 ADT에는 여러개의 자료 구조가 올 수 있으며 ADT의 operations에 어떤 자료구조를 사용하는게 더 효율적인가 생각할때 사용하기도 한다. 몇가지 예를 보자.
Priority Queue ADT | heap | AVL tree |
---|---|---|
Q = new-empty-queue |
$\theta(1)$ | $\theta(1)$ |
Q.insert(x) |
$\theta(lgn)$ | $\theta(lgn)$ |
x = Q.deletemin() |
$\theta(lgn)$ | $\theta(lgn)$ |
x = Q.findmin() |
$\theta(1)$ | $\theta(lgn)$ |
경우에 따라 AVL tree에서 findmin의 경우 $\theta(1)$ 로 줄일 수 있음
Predecessor / Successor ADT | heap | AVL tree |
---|---|---|
S = new-empty() |
$\theta(1)$ | $\theta(1)$ |
S.insert(x) |
$\theta(lgn)$ | $\theta(lgn)$ |
S.delete(x) |
$\theta(lgn)$ | $\theta(lgn)$ |
y = S.predecessor(x) |
$\theta(n)$ | $\theta(lgn)$ |
y = S.successor(x) |
$\theta(n)$ | $\theta(lgn)$ |
이와 같이, 어떻게 자료를 저장할지 판단할때 도움이 될 수 있다.
참조
MIT OCW 6.006 Introducion to Algorithms 강의 자료 및 강의 내용
Introduce to Algorithms, 3rd Edition
modfied:18 Apr 2021
Comments