Tree 02. AVL Tree(Adel'son-Vel'skii & Landis, AVL 트리)

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.

figure 0

AVL 트리

Structure

BST와 같은 구조에서 Balanced를 유지하기 위해 각 노드의 heightkey와 함께 저장한다. 이때 height는 해당 노드에서 가장 먼 leaf에 도착하면서 거치는 노드의 수이며 max{left.height, right.height} + 1의 방식으로 계산할 수 있다. 또한 NULL 을 가리키는 경우엔, height = -1 으로 정하면 수식에 맞아 떨어진다.

figure 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

  1. BST와 같은 방법으로 값을 넣는다.
  2. 바뀐 구조를 AVL 구조로 잡아준다. (rotation 이용!)

일반적인 예시)

  • Node x가 AVL tree의 성질을 위반하는 가장 낮은 노드라고 가정

  • x.rightheight가 더 크다고 가정

  • 만약 x.right 가 right-heavy거나 balanced이면 left-rotate(x), 그렇지 않으면 right-rotate(x.right) and left-rotate(x) 를 통해 AVL로 만들 수 있다.

    • right-heavyfigure2

    • balanced

      figure3

    • left-heavy

      figure4

  • x.leftheight가 더 크다면, 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가 떴고, *avlavl 둘 다 조건을 넣어도 자식 노드에 접근하면서 비슷한 오류가 발생했다. NIL을 만들어서 해결하려면 모든 코드를 뜯어고쳐야 했기에 return 을 해서 해결하려 했으나, 재귀함수의 특성상 한번만 return 하면 될걸 여러번 해야 한다는 점이 너무 찝찝했다. 결국 NIL을 만들어서 해결하기로 했다.

NIL을 만드니 쉽게 문제가 해결되었다. 처음에 NIL을 피하려던 이유가 각 자식노드에 NIL을 만들어 주려면 parent가 다 다르니 여러개를 만들어야 해서 공간도 많이 차지하고 복잡해 진다고 생각했었다. 하지만 도식 상 빈 자식노드의 left, right 가 각각 NULL 을 가리키고 있었고, 실제 코드에선 하나의 포인터 NULL을 가리키고 있기 때문에 NULLNIL로 바꾸기만 하면 될 문제였고 따라서 공간도 하나만 더 차지하는 셈이었다.

rotate를 하면서 height를 업데이트 하다 보니 시간복잡도가 생각보다 커지는 문제가 발생하였다. rotate를 후위 순회를 통해 모든 노드를 탐색하면서 height가 불균형일 경우 rotate을 호출 했는데, rotate가 한번 이루어지면 다시 모든 height 를 업데이트를 하는 방식으로 하니 이중반복문 꼴이 되었다. 이렇게 되면 AVL을 쓰는 의미가 사라져서, rotateheight가 바뀌면 그 부모노드만 영향을 받는 점에서 착안하여 부모노드만 업데이트 하는 방향으로 하였다.

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