[완료]안녕하세요 AVL tree 구현 관련 질문입니다.
컴퓨터 학과 4학년인데 프로그래밍 관련 실력이 부족한것 같아
C++ 자료구조론(Ellis Horowitz, 이석호 역)을 기초로 다시 공부하고 있습니다.
해당 책에서 AVL tree를 구현하는데 이해가 안되는 부분이 있어서 글을 올렸습니다.
첫번째 질문입니다.
일단 아래 코드는 AVL tree insert 함수입니다.
template <class K, class E> void AVL<K, E>::insert(const K& k, const E& e) { if(!root){ root = malloc(sizeof(AvlNode<K,E>)); return; } AvlNode<K,E> *a = NULL, /*균형 인수 +-1을 가진 가장 최근의 노드*/ *pa = NULL, /*균형 인수 +-1을 가진 가장 최근의 노드 의 부모*/ *p = root, /*연산에 사용될 노드의 임의 저장소*/ *pp = NULL; /*위 노드의 부모*/ while(p){ /*삽입 위치 탐색*/ if(p->bf){a = p; pa = pp;}/*균형 인수가 +_ 1 이면 a, ap에 저장*/ if(k < p->key){pp = p; p = p->left_child;} //왼쪽 자식 으로 이동 else if( k > p->key){pp = p; p = p->right_child;} //오른쪽 자식으로 이동 else{p ->element= e; return;} } AvlNode<K,E> *y = malloc(sizeof(AvlNode<K,E>)); // 추가할 노드 생성 y->key = k; y->element = e; //원소를 적절한 위치에 추가한다. if(k < pp->key) pp->left_child = y; else pp->right_child = y; //노드 a에서 pp의 균형 인수를 조정. a의 정의에 의해, 이 경로 내에 //있는 모든 노드들은 균형 인수 0이 되어야 하며 후에 +_1로 변경 //d = +1은 x 가 a의 왼쪽 서브트리에 삽입된 것을 의미 //d = -1은 x 가 a의 오른쪽 서브 트리에 삽입된 것을 의미 int d;//원소 1개가 추가 되었으므로 1 또는 -1의 값을 갖는다. AvlNode<K,E> *b, //a의 자식 *c; //b의 자식 if(k > a ->key){b = p = a->right_child; d = -1;} //a의 오른쪽 서브트리에 삽입 else{b = p = a->left_child; d = 1;} //a의 왼쪽 서브트리에 삽입 //위에 근거해서 a의 오른쪽트리에 삽입 되었는지 왼쪽 트리에 삽입 되었는지 추정 while(p != y) //a 부터 시작하여 추가 노드까지 균형 인수 변경 { if(k > p->key){//오른쪽 높이가 1 만큼 증가 p->bf = -1; p = p->right_child; }else{ //왼쪽 높이가 1 만큼 증가 p -> bf = 1; p = p->left_child; } } //트리가 불균형 인가? if(!(a->bf) || !(a->bf +d)){//트리가 계속 균형을 유지 a->bf += d; return; } //아래 각각의 연산이 끝난 후에는 변수 b에 회전시킨 서브 트리의 root를 assign 해 놓는다. if(d == 1){//왼쪽 불균형 if(b->bf ==1){//LL 회전 a->left_child = b->right_child; b->right_child = a; a->bf = 0; b->bf = 0; }else{//LR 회전 c = b->right_child; b->right_child = c->left_child; a->left_child = c->right_child; c->left_child = b; c->right_child = a; switch(c->bf){ case 1: a->bf = -1; b->bf = 0; break; case -1: b->bf = 1; a->bf = 0; break; case 0: b->bf = 0; a->bf = 0; break; } c->bf = 0; b = c;//회전시킨 서브트리의 루트를 b에 assign } }else{//오른쪽 불균형- L 타입과 대칭 if(b->bf == -1){//RR 회전 a->right_child = b->left_child; b->left_child = a; a->bf = 0; b->bf = 0; }else{ c = b->left_child; a->right_child = c->left_child; b->left_child = c->right_child; c->left_child = a; c->right_child = b; switch(c->bf){ case 1: a->bf = 0; b->bf = -1; break; case -1: a->bf = 1; b->bf = 0; break; case 0: a->bf = 0; b->bf = 0; break; } c->bf = 0; b = c; } } //변경된 서브트리의 루트를 기존 트리에 assign한다. if(!pa)root = b; else if(a == pa->left_child)pa->left_child = b; else pa->right_child = b; return; }
코드가 길어서 죄송합니다.
일단 위 함수를 보고 전체 insert연산은 삽입 위치를 찾기 위해 O(logN) 이 걸리고
한번 회전연산이 일어나므로 O(1) 따라서 전체 O(logN)이라고 추측하였습니다.
하지만 제가 생각하기로는 이 연산이 external node에서 시작하므로 root까지 재구축(rebalance)
를 할 경우가 생겨 최악의 경우 회전 연산을 O(logN)정도 더 해야 할 것이라고 생각 하는데
제 해석이 맞는지 궁금합니다.
다시 말하자면 위 코드는 재구축을 1번밖에 안하는데 그것으로 충분할 것 같지 않습니다;;;
두번째 질문입니다.
delete 연산을 구현하는데 많은 어려움이 있습니다.
삭제된 노드를 해당 노드의 우측 트리에서 중위순회를 해 나오는 최초의 노드로 변경하고
balance factor는 변경된 노드에서 재귀적으로 높이를 재계산하여 결정하는 방법을 썼는데
그 이후에 root 까지 트리를 변경하는 방법을 잘 모르겠습니다.
변경된 노드에서 트리 루트까지 올라가면서 재구축 하는 방법은 위의 balance factor를 변경하는 방법과
결합하면 너무 많은 시간이 걸릴 것이라 생각합니다.
그래서 parent 변수를 추가하면 해결될것 같긴 하지만 이 변수를 추가하지 않고 위의 방법보다 더 효율적인
방법은 없을 까요?
일단http://www.stanford.edu/~blp/avl/libavl.html/index.html#toc_AVL-Trees 소스를 구해 놓긴 했는데
이걸로 다시 공부해보고 다시 짜 보는것이 나을 것 같기도 합니다.
차라리 위의 소스를 보고 다시 공부하는것이 나을까요?
답변 주시면 감사하겠습니다.
AVL tree 이후를 계속 공부하시면 답이 나올 듯...
생각해보니 저도 balanced tree 계열을 직접 구현해 본 적이 한번도 없네요.
AVL tree는 회전연산이 한번만 일어나는게 맞습니다. 그래서 치우침(skew) 현상이 발생하죠. 최악 깊이가 1.44 * log N 으로 알고 있습니다.
--------------------------------------------------------------------------------
확인해보니 AVL tree는 회전연산이 두번 발생할 수 있다네요.
AVL 트리는 BST(Binary
AVL 트리는 BST(Binary Search Tree)의 단점을 개선한 것인데,
BST를 완벽하게 이해하셨다면 AVL 트리는 좌우 균형을 맞추기 위한
Turn Left, Turn Right만 이해 하시면 될듯하네요.
(이 개념은 BTree 에서 좀더 발전합니다)
그런데 위의 C++코드는 너무 복잡해서 한번 보려 했는데 질려 버렸어요.
저 개인적으로는 C코드가 훨씬 깔끔하다고 생각합니다.
암튼, 제가 별로 도움되는 말은 못했어도
요즘 저와 비슷한 스터디를 하고 있는 분이듯 하여 반갑습니다.
From:
*알지비 (메일: rgbi3307(at)nate.com)
*학창시절 마이크로마우스를 만들었고, 10년동안 IT관련 개발자로 일하고 있음.
*틈틈히 커널연구회(http://www.kernel.bz/) 내용물들을 만들고 있음.
*((공부해서 남을 주려면 남보다 더많이 연구해야함.))
From:
*알지비 (메일: rgbi3307(at)nate.com)
*커널연구회(http://www.kernel.bz/) 내용물들을 만들고 있음.
*((공부해서 남을 주려면 남보다 더많이 연구해야함.))
사실 AVL tree 는
사실 AVL tree 는 그렇게 빠르고 효율적이고 하지는 않아서
일반적으로 그렇게 많이 사용되지는 않습니다.
오히려 RB tree 나 splay tree 가 상대적으로 많이 쓰이죠.
AVL tree 가 depth 를 놓고 보면 더 나아보일지 몰라도
어차피 O notation 으로 보면 쟤네도 별반 다르지 않거든요.
AVL에서 재구축을 하게 되나요?
삽입시 이미 AVL트리 조건을 만족 시키고 거기다 추가 하나를 하는 경우 어떤 경우라도 AVL트리 조건을 만족시키는데 root까지 올라가면서 rebalance를 하지는 않는 것으로 알고 있는데요... 그리고 원래 AVL크리 구현은 복잡하고 짜증나고 힘듭니다. 균형트리중에는 그나마 제일 낫지만요... 사실 웬만한 자료구조론 시간에도 직접 구현을 다 하지는 않으니 그냥 한번 보고 넘어가는 것만 해도 괜찮을 듯 싶습니다.
피드백이 늦어서 죄송합니다. ㅠ ㅠ
Kaeri17님의 설명을 책에서 본것 같습니다. 간단한 설명이 나와있었는데
구현부분을 집중적으로 보느라 놓친 부분이 있었군요
rgbi3307님 보실지는 모르겠지만 자료구조를 공부하게 된 계기는 리눅스 커널을 공부하던 도중
커널내에서 rb 트리를 사용하는 부분이 있어서 그런 것입니다. 저도 C++ 보단 C 구현을 좋아합니다.
여러가지 장점이 있지만 좀더 컴퓨터에 친화적인 언어라고 생각하기 때문입니다
C++ 코드가 C 로 재해석 된다는 사실은 꽤 큰 충격이었죠(?)
다른 balanced 트리를 놔두고 굳이 rb 트리를 사용하는 이유가 효율성에 있었군요.
계속 공부해 rb, b-tree 정도 까지만 볼 생각입니다.
자료구조는 더 많지만 다른 공부해야 할 것도 너무 많네요 ㅠ ㅠ
감사합니다
C로 해석되지는 않을텐데요.
C로 해석되는 것은 옛날 이야기일 듯...
흐흠.....그렇군요^^;;
군대 갔다오기전 PL 시험 시간에 그런문제가 나왔던듯 해서;;
컴파일 과정을 대략적으로 그려넣는 과정이었는데 중간의 답안이 C++->C->Assembly->machine
이라고 생각했는데 틀렸나;;;
뭐, C++->C->Assem 이런 과정은 안거친다 해도
버추얼 함수라던지 클래스 개념이라던지 레퍼런스 인수 같은 것들
은 C로 재해석 할수 있다고 들었거든요.
흠...그런걸 보면 새로운 프로그래밍 개념이라는 것들(대표적으로 OOP)
이 프로그래밍 언어에서 얼마나 지원해 주는가의 차이가 있지 C로도 얼마든지 OOP를
구현할 수 있다는 생각을;;;
주제에서 좀 벗어나지만 졸업하기전에 컴파일러 과목도 들어두면 꽤 도움이 될것같은데
그러지 못해 아쉽군요
Cfront와 같은 초기의
Cfront와 같은 초기의 C++ 컴파일러들은
C++ -> C 변환을 한 적도 있었습니다만,
요새는 굳이 그렇게 하고 있지 않습니다.
오히려 어려울 수 있습니다.
C 표준을 해석하다보면 C++ virtual 함수는 C 표준에 부합되지 않더군요.
댓글 달기