탐색: 탐색 키와 데이터로 이루어진 항목 중에서 원하는 탐색 키를 가지고 있는 항목을 찾는 것
순차 탐색(sequential search): 탐색 방법 중에서 가장 간단하고 직접적인 탐색 방법
int seq_search(int key, int low, int high){
for(int i=low;i<=high;i++){
if(list[i] == key) return i; // 탐색 성공 시
}
return -1; // 탐색 실패 시
}
int seq_search2(int key, int low, int high){
int i;
list[high+1] = key;
for(i= low; list[i] != key; i++){} // 키 값을 찾으면 종료 -> 비교 연산을 for문 안에서 안함
if(i == (high+1)) return -1; // 탐색 실패 시
else return i; // 탐색 성공 시
}
탐색 성공 시 평균 (n+1)/2를 비교하고, 실패 시 n번 비교 -> 시간복잡도는
// 오름차순으로 정렬된 배열 리스트의 순차 탐색
int sorted_seq_search(int key, int low, int high){
for(int i=low; i<=high;i++){
if(list[i] > key) retuern -1; // 키 값보다 큰 항목을 만났을 경우 return
if(list[i] == key) return i;
}
}
탐색 성공 시 일반 순차 탐색과 동일, 실패 시 비교 횟수가 반으로 줄어듬 -> 시간복잡도
이진 탐색(binary search): 배열의 중앙에 있는 값을 조사하여 찾고자 하는 항목이 왼쪽 또는 오른쪽 부분 리스트에 있는지를 알아내어 탐색의 범위를 반으로 줄임
search_binary(list, low, high){
middle <- low에서 high 사이의 중간 위치
if( 탐색 값 = list[middle])
return TRUE;
else if( 탐색 값 < list[middle])
return list[low] 부터 list[middle-1]에서의 탐색;
else if( 탐색 값 > list[middle])
return list[middle+1] 부터 list[high]에서의 탐색;
}
int search_binary(int key, int low, int high){
int middle;
if(low<=high){ // 아직 숫자가 남아 있는 경우
middle = low+high/2;
if(key == list[middle]) return middle; // 탐색 성공
else if(key < list[middle]) return search_binary(key, low, middle-1); // 왼쪽 부분 리스트
else return search_binary(key, middle+1, high); // 오른쪽 부분 리스트
}
return -1; // 탐색 실패
}
int search_binary2(int key, int low, int high){
int middle;
while(low<=high){ // 아직 숫자들이 남아 있으면
middle = low+high/2;
if(key == list[middle]){
return middle; // 탐색 성공
}else if(key < list[middle]){
high = middle-1; // 왼쪽 부분 리스트
}else{
low = middle+1; // 오른쪽 부분 리스트
}
}
return -1; // 탐색 실패
}
이진 탐색의 시간 복잡도
색인 순차 탐색(indexed sequential search): 인덱스(index)라 불리는 테이블을 사용하여 탐색의 효율을 높이는 방법
색인 순차 탐색 알고리즘을 위한 인덱스 테이블 구조체
typedef struct{
int key; // 인덱스가 가리키는 곳의 키 값 저장
int index; // 리스트의 인덱스 값 저장
} itable;
itable index_list[INDEX_SIZE];
// INDEX_SIZE는 인덱스 테이블의 크기, n은 전체 데이터의 수
int search_index(int key){
int i, low, high;
// 키 값이 리스트 범위 내의 값이 아니면 탐색 종료
if(key <list[0] || key > list[n-1])
return -1;
// 인덱스 테이블을 조사하여 해당 키의 구간 결정
for(i=0;i<INDEX_SIZE;i++){
if(index_list[i].key <= key && index_list[i+1].key > key){
break;
}
}
if(i==INDEX_SIZE){ // 인덱스 테이블의 끝이면
low = index_list[i-1].index;
high = n-1;
}else{
low = index_list[i].index;
high = index_list[i+1].index-1;
}
// 예상되는 범위만 순차 탐색
return seq_search(key,low,high);
}
보간 탐색(interpoliation search): 사전이나 전화번호부를 탐색하는 방법과 같이 탐색 키가 존재할 위치를 예측하여 탐색하는 방법
탐색 위치 결정 방법
->
-> k는 찾고자 하는 키 값, low와 high는 각각 탐색할 범위의 최소와 최대 인덱스 값
-> 위의 식은 탐색 위치를 결정할 때 찾고자하는 키 값이 있는 곳에 근접하게 되도록 가중치를 주는 것
ex) 3, 9, 15, 22, 31, 55, 67, 88, 89, 92로 구성된 리스트에서 탐색 구간이 0-9이고, 찾을 값이 55일 경우
int search_interpolation(int key, int n){
int low =0;
int high = n-1;
while((list[high] >= key) && (key > list[low])){
int j= ((float)(key-list[low])/(list[high]-list[low])*(high-low))+low;
if(key >list[j]) low = j+1;
else if(key <list[j]) high = j-1;
else low = j;
}
if(list[low] == key) return low; // 탐색 성공
else return -1; // 탐색 실패
}
보간탐색의 시간복잡도
이진 탐색 ve 이진 탐색 트리
이진 탐색 = 자료들이 배열에 저장되어 있으므로 삽입과 삭제가 힘듦.(즉, 자료 삽입, 삭제시 앞 뒤의 원소를 이동시켜야함)
이진 탐색 트리 = 비교적 빠른 시간 안에 삽입과 삭제를 끝마칠 수 있는 구조
AVL 트리: Adelson-Velskii와 Landis에 의해 제안된 트리로서 각 노드에서 왼쪽 서브 트리의 높이와 오른쪽 서브 트리의 높이가 차이가 1이하인 이진 탐색 트리를 말함
AVL 트리는 트리가 비균형 상태가 되면 스스로 노드를 재배치하여 균형 상태로 만듦
-> AVL 트리는 균형 트리가 항상 보장되므로 탐색이 시간 안에 끝나게 됨 (+ 삽입, 삭제도 )
AVL 트리도 탐색에서는 일반 이진 탐색 트리의 탐색 연산과 동일 -> 시간복잡도
AVL 트리에서 균형이 깨지는 경우(4가지)
1. LL 타입: N(새로 삽입된 노드)이 A(가장 가까우면서 균형 인수가 가 된 조상 노드)의 왼쪽 서브 트리의 왼쪽 서브 트리에 삽입됨
2. RR 타입: N이 A의 오른쪽 서브 트리의 오른쪽 서브 트리에 삽입됨
3. RL 타입: N이 A의 오른쪽 서브 트리의 왼쪽 서브 트리에 삽입됨
4. LR 타입: N이 A의 왼쪽 서브 트리의 오른쪽 서브 트리에 삽입됨
재균형시키는 방법
1. LL 회전: A부터 N까지의 경로 상의 노드들을 오른쪽으로 회전시킴
2. RR 회전: A부터 N까지의 경로 상의 노드들을 왼쪽으로 회전시킴
3. RL 회전: A부터 N까지의 경로 상의 노드들을 오른쪽-왼쪽으로 회전시킴
4. LR 회전: A부터 N까지의 경로 상의 노드들을 왼쪽-오른쪽으로 회전시킴
rotate_LL(A)
B의 오른쪽 자식을 A의 왼쪽 자식으로 만듦
A를 B의 오른쪽 자식 노드로 만듦
rotate_RR(A)
B의 왼쪽 자식을 A의 오른쪽 자식으로 만듦
A를 B의 왼쪽 자식 노드로 만듦
rotate_RL(A)
rotate_LL(B)가 반환하는 노드를 A의 오른쪽 자식으로 만듦
rotate_RR(A)
rotate_LR(A)
rotate_RR(B)가 반환하는 노드를 A의 왼쪽 자식으로 만듦
rotate_LL(A)
ex) 7, 8, 9, 2, 1, 5, 3, 6, 4 데이터가 주어졌을 때, AVL 트리 구축 예)
삽입되는 노드: 주황색 / 위치가 변경되는 노드: 노란색
typedef struct AvlNode{
int data;
struct AvlNode *left_child, *right_child; // 왼쪽과 오른쪽 자식을 가리키는 포인터
}AvlNode;
AvlNode *root;
// LL 회전
AvlNode * rotate_LL(AvlNode *parent){
AvlNode *child = parent->left_child;
parent->left_child = child->right_child;
child->right_child = parent;
return child;
}
// RR 회전
AvlNode * rotate_RR(AvlNode *parent){
AvlNode *child = parent->right_child;
parent->right_child = child->left_child;
child->left_child = parent;
return child;
}
// RL 회전
AvlNode * rotate_RL(AvlNode *parent){
AvlNode *child = parent->right_child;
parent->right_child = rotate_LL(child);
return rotate_RR(parent);
}
// LR 회전
AvlNode * rotate_LR(AvlNode *parent){
AvlNode *child = parent->left_child;
parent->left_child = rotate_RR(child);
return rotate_LL(parent);
}
// 트리의 높이를 반환
int get_height(AvlNode *node){
int height =0;
if(node !=NULL){
height = 1+ max(get_height(node->left_child), get_height(node->right_child));
}
return height;
}
// 노드의 균형 인수 반환
int get_height_diff(AvlNode *node){
if(node == NULL) return 0;
return get_height(node->left_child) - get_height(node->right_child);
}
// 트리를 균형 트리로 만듦
AvlNode * rebalance(AvlNode **node){ // node가 포인터의 포인터가 된 이유는 node의 값이 함수 내에서 변경되어야 하기 때문
int height_diff = get_height_diff(*node);
if(height_diff > 1){
if(get_height_diff((*node)->left_child) > 0){
*node = rotate_LL(*node);
}else{
*node = rotate_LR(*node);
}
}else if(height_diff < -1){
if(get_height_diff((*node)->right_child) < 0){
*node = rotate_RR(*node);
}else{
*node = rotate_RL(*node);
}
}
return *node;
}
// AVL 트리의 삽입 연산
AvlNode *avl_add(AvlNode **root, int new_key){
if(*root == NULL){
*root = (AvlNode *)malloc(sizeof(AvlNode));
if(*root == NULL){
fprintf(stderr,"Memory allocate error\n");
exit(1);
}
(*root)->data = new_key;
(*root)->left_child = (*root)->right_child = NULL;
}else if(new_key < (*root)->data){
(*root)->left_child = avl_add(&((*root)->left_child), new_key);
*root = rebalance(root);
}else if(new_key > (*root)->data){
(*root)->right_child = avl_add(&((*root)->right_child), new_key);
*root = rebalance(root);
}else{
fprintf(stderr,"duplicate error\n");
exit(1);
}
return *root;
}
// AVL 트리의 탐색 함수
AvlNode *avl_search(AvlNode *node, int key){
if(node == NULL) return NULL;
printf("%d ",node->data);
if(key == node->data) return node;
else if(key < node->data) return avl_search(node->left_child, key);
else return avl_search(node->right_child, key);
}
int main()
{
// 8,9,10,2,1,5,3,6,4,7,11,12
avl_add(&root1, 8);
avl_add(&root1, 9);
avl_add(&root1, 10);
avl_add(&root1, 2);
avl_add(&root1, 1);
avl_add(&root1, 5);
avl_add(&root1, 3);
avl_add(&root1, 6);
avl_add(&root1, 4);
avl_add(&root1, 7);
avl_add(&root1, 11);
avl_add(&root1, 12);
printf("#1: ");
avl_search(root1, 12);
printf("\n");
// 7,8,9,2,1,5,3,6,4
avl_add(&root2, 7);
avl_add(&root2, 8);
avl_add(&root2, 9);
avl_add(&root2, 2);
avl_add(&root2, 1);
avl_add(&root2, 5);
avl_add(&root2, 3);
avl_add(&root2, 6);
avl_add(&root2, 4);
printf("#2: ");
avl_search(root2, 4);
printf("\n");
}
2-3 트리: 차수가 2 또는 3인 노드를 가지는 트리
int tree23_search(Tree23Node *root, int key){
if(root == NULL){ // 트리가 비어 있으면
return FALSE:
}else if(key == root.key1){ // 루트의 키 == 탐색 키
return TRUE;
}else if(root->type == TWO_NODE){ // 2-노드
if(key <root->key1){
return tree23_search(root->left, key);
}else{
return tree23_search(root->right, key);
}
}else{ // 3-노드
if(key <root->key1){
return tree23_search(root->left, key);
}else if(key > root->key1){
return tree23_search(root->right, key);
}else{
return tree23_search(root->middle, key);
}
}
}
2-3 트리의 노드는 2개의 데이터 값을 저장할 수 있음
2-3 트리에 데이터 추가 시에 노드에 추가할 수 있을 때까지 데이터는 추가되고 더 이상 저장할 장소가 없는 경우에는 노드를 분리하게 됨
단말 노드를 분리하는 경우
비단말 노드를 분리하는 경우
루트 노드를 분리하는 경우
2-3-4 트리: 하나의 노드가 4개의 자식까지 가질 수 있도록 2-3트리를 확장한 것
2-3 트리는 삽입 또는 삭제를 위한 순회와 분할, 합병의 영향으로 인한 순회가 필요함
-> 따라서, 2-3-4 트리의 장점은 순회를 한 번에 삽입 삭제가 가능하다는 것
<참고자료>
천인국 · 공용해 · 하상호 지음,『C언어로 쉽게 풀어쓴 자료구조』, 생능출판(2016)