728x90
반응형

1. 탐색이란

 - 탐색(search)은 컴퓨터가 가장 많이 하는 작업 중의 하나이다.

 - 탐색은 많은 시간이 요구되므로 효율적으로 수행하는 것은 매우 중요하다.

 - 탐색의 단위는 항목이다. 항목 안에는 항목과 항목을 구별시켜주는 키(key)가 존재한다. 이를 탐색 키(search key)라고 한다.

 

2. 정렬되지 않은 배열에서의 탐색

 1) 순차 탐색(sequential search)

int seq_search(int key, int low, int high)
{
	int i;
	for (i = low; i <= high; i++)
		if (list[i] == key)
			return i;  // 탐색에 성공하면 키 값의 인덱스 반환 
	return -1;    // 탐색에 실패하면 -1 반환 
}

 2) 개선된 순차 탐색

  - 리스트의 끝에 찾고자 하는 키값을 저장, 비교 연산의 수를 반으로 줄일 수 있다.

int seq_search2(int key, int low, int high)
{
	int i;
	list[high + 1] = key;
	for (i = low; list[i] != key; i++) // 키값을 찾으면 종료  
		;
	if (i == (high + 1)) return -1;   // 탐색 실패 
	else     return i;           // 탐색 성공 
}

 

3. 정렬된 배열에서의 탐색

 1) 이진 탐색

  - 정렬된 배열의 탐색에는 이진 탐색(binary search)이 가장 적합하다.

  - 10억 명의 이름이 정렬된 배열에서 이진 탐색을 이용하면 단지 30번의 비교만으로 검색이 완료된다.

  - 이진 탐색은 데이터의 삽입이나 삭제가 빈번할 시에는 적합하지 않고, 주로 고정된 데이터에 대한 탐색에 적합하다.

  - 재귀 호출을 이용한 이진 탐색

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])
			low = middle + 1;
		else
			high = middle - 1;
	}
	return -1;   // 발견되지 않음 
}

 

 2) 색인 순차 탐색

  - 색인 순차 탐색(indexed sequential search) 방법은 인덱스(index)라 불리는 테이블을 사용하여 탐색의 효율을 높이는 방법이다.

  - 인덱스 테이블은 주자료 테이블에서 일정 간격으로 발췌한 자료를 가지고 있다.

  - 이때 주자료 테이블과 인덱스 테이블은 모두 정렬되어 있어야 한다.

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;
	}
	else {
		low = index_list[i].index;
		high = index_list[i + 1].index;
	}
	// 예상되는 범위만 순차 탐색 
	return seq_search(key, low, high);
}

 

 3) 보간 탐색

  - 보간 탐색(interpolation search)은 사전이나 전화번호부를 탐색하는 방법과 같이 탐색 키가 존재할 위치를 예측하여 탐색하는 방법이다.

  - 탐색 위치 = $$ \frac{k-list[low]}{list[high]-list[low]}*(high-low)+low $$

  - 위의 식은 탐색 위치를 결정할 때 찾고자 하는 키 값이 있는 곳에 근접하게 되도록 가중치를 주는 것이다.

  - 값과 위치는 비례한다는 가정

int search_interpolation(int key, int n)
{
	int low, high, j;
	low = 0;
	high = n - 1;
	while ((list[high] >= key) && (key > list[low])) {
		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;  // 탐색실패
}

 

4. 균형 이진 탐색 트리

 - 이진 탐색 트리는 비교적 빠른 시간 안에 삽입과 삭제를 끝마칠 수 있는 구조

 - 삽입, 삭제가 빈번히 이루어진다면 이진 탐색 트리를 사용하는 것이 적합하다.

 - 이진 탐색 트리가 경사 트리가 되면 효율이 매우 떨어지므로 균형을 유지하는 것이 무엇보다 중요하다.

 

 1) AVL 트리

  - 각 노드에서 왼쪽 서브 트리의 높이와 오른쪽 서브 트리의 높이 차이가 1 이하인 이진 탐색 트리를 말한다.

  - 균형 인수(balance factor) (왼쪽 서브 트리의 높이 - 오른쪽 서브 트리의 높이)로 정의된다.

  - 모든 노드의 균형 인수가 ±1이하이면 AVL 트리이다.

 

  (1) AVL 트리의 탐색 연산

   - AVL 트리도 탐색에서는 일반 이진 탐색 트리의 탐색 연산과 동일하다. 따라서 시간 복잡도는 O(log2n)이다.

  (2) AVL 트리의 삽입 연산

   - 균형 상태의 이진 탐색 트리에서 균형이 깨지는 것은 삽입 연산과 삭제 연산 때문이다.

   - 균형 인수가 ±2가 된 가장 가까운 조상 노드의 서브 트리들에 대하여 다시 균형을 잡아야 한다. 그 외의 다른 노드들은 일체 변경할 필요가 없다.

   - A는 ±2가 된 가장 가까운 조상 노드, N은 새로 삽입된 노드

   - LL 회전 : A부터 N까지의 경로상의 노드들을 오른쪽으로 회전시킨다.

   - RR 회전 : A부터 N까지의 경로상의 노드들을 왼쪽으로 회전시킨다.

   - RL 회전 : A부터 N까지의 경로상의 노드들을 오른쪽 - 왼쪽으로 회전시킨다.

   - LR 회전 : A부터 N까지의 경로상의 노드들을 왼쪽 - 오른쪽으로 회전시킨다.

 

   - AVL 트리에서의 단순 회전 알고리즘

rotate_LL(A)

           B(중간 노드)의 오른쪽 자식을 A의 왼쪽 자식으로 만든다.

           A B의 오른쪽 자식 노드로 만든다.

rotate_RR(A)

           B(중간 노드)의 왼쪽 자식을 A의 오른쪽 자식으로 만든다.

           A B의 왼쪽 자식 노드로 만든다.

 

  - AVL 트리에서의 이중 회전 알고리즘

rotate_RL(A)

           rotate_LL(B)가 반환하는 노드를 A의 오른쪽 자식으로 만든다.

           rotate_RR(A)

rotate_LR(A)

           rotate_RR(B)가 반환하는 노드를 A의 왼쪽 자식으로 만든다.

           rotate_LL(A)

 

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

struct avl_node   {
    struct avl_node *left_child, *right_child;  /* Subtrees. */
    int data;                   /* Pointer to data. */
};

struct avl_node *root;

// 오른쪽 회전 함수
struct avl_node *
rotate_right(struct avl_node *parent)
{
	struct avl_node *child = parent->left_child;
	parent->left_child = child->right_child;
	child->right_child = parent;
	return child;
}
// 왼쪽 회전 함수
struct avl_node *
rotate_left(struct avl_node *parent)
{
	struct avl_node *child = parent->right_child;
	parent->right_child = child->left_child;
	child->left_child = parent;
	return child;
}
// 오른쪽-왼쪽 회전 함수
struct avl_node *
rotate_right_left(struct avl_node *parent)
{
	struct avl_node *child = parent->right_child;
	parent->right_child = rotate_right(child);
	return rotate_left(parent);
}
// 왼쪽-오른쪽 회전 함수
struct avl_node *
rotate_left_right(struct avl_node *parent)
{
	struct avl_node *child = parent->left_child;
	parent->left_child = rotate_left(child);
	return rotate_right(parent);
}
// 트리의 높이를 반환
int get_height(struct avl_node *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(struct avl_node *node) 
{
	if( node == NULL ) return 0;
	return get_height(node->left_child) - get_height(node->right_child);
} 

// 트리를 균형트리로 만든다
struct avl_node *
rebalance(struct avl_node **node)
{
	int height_diff = get_height_diff(*node);
	if( height_diff > 1 ){
		if( get_height_diff((*node)->left_child) > 0 )
			*node = rotate_right(*node);
		else
			*node = rotate_left_right(*node);
	}
	else if ( height_diff < -1 ){
		if( get_height_diff((*node)->right_child) < 0 )
			*node = rotate_left(*node);
		else
			*node = rotate_right_left(*node);
	}
	return *node;
}

// AVL 트리의 삽입 연산
struct avl_node *
avl_add(struct avl_node **root, int new_key)
{
	if( *root == NULL ){
		*root = (struct avl_node *)
			malloc(sizeof(struct avl_node));
		if( *root == NULL ){
			fprintf(stderr, "메모리 할당 에러\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, "중복된 키 에러\n");
		exit(1);
	}
	return *root;
}

// AVL 트리의 탐색함수
struct avl_node *avl_search(struct avl_node *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); 
} 

void main()
{
	//8,9,10,2,1,5,3,6,4,7,11,12
	avl_add(&root, 8);
	avl_add(&root, 9);
	avl_add(&root, 10);
	avl_add(&root, 2);
	avl_add(&root, 1);
	avl_add(&root, 5);
	avl_add(&root, 3);
	avl_add(&root, 6);
	avl_add(&root, 4);
	avl_add(&root, 7);
	avl_add(&root, 11);
	avl_add(&root, 12);
	avl_search(root, 12);
}

 

 2) 2-3 트리

  - 2-3트리는 차수가 2또는 3인 노드를 가지는 트리

  - 2-노드는 일반 이진 탐색 트리처럼 하나의 데이터 k1과 두 개의 자식 노드를 가진다.

  - 3-노드는 2개의 데이터 k1, k2 3개의 자식 노드를 가진다.

  - 2-3 트리에서의 탐색

tree23_search(Tree23Node *root, int key)
{
	if (root == NULL)			// 트리가 비어 있으면
		return FALSE;
	else if (key == root->data)// 루트의 키==탐색키 
		return TRUE;
	else if (root->type == TWO_NODE) {  // 2-노드
		if (key < root->key)
			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->key2)
			return tree23_search(root->right, key)
		else
			return tree23_search(root->middle, key)
	}
}

 

  (1) 2-3 트리의 삽입 연산 리스트 ADT의 구현

   - 2-3 트리의 노드는 2개의 데이터 값을 저장할 수 있다. 2-3 트리에 데이터 추가 시에 노드에 추가할 수 있을 때까지 데이터는 추가되고 더 이상 저장할 장소가 없는 경우에는 노드를 분리하게 된다.

   - 중간 값을 한 레벨 위로 올리고 제일 작은 값을 왼쪽 노드로, 제일 큰 값을 오른쪽 노드로 만드는 것이다.

   - 3가지의 경우로 나누어진다. 단말 노드를 분리 / 비단말 노드를 분리 / 루트 노드를 분리

   - 2-3 트리에서 트리의 높이가 증가하게 되는 경우는 루트 노드를 분리하는 경우뿐이다.

 

 3) 2-3-4 트리

  - 2-3-4 트리는 하나의 노드가 4개의 자식까지 가질 수 있도록 2-3 트리를 확장한 것이다.

  - 4-노드는 4개의 자식을 가질 수 있고, 3개의 데이터(small, middle, large)를 가질 수 있다.

  - 2-3-4 트리에서 키를 삽입해야 할 단말 노드가 만약 2-노드 또는 3-노드이면 간단하게 삽입만 하면 된다. 문제는 삽입해야 할 단말 노드가 4-노드이면 후진 분할이 일어나게 된다.

  - 이를 방지하기 위하여 삽입 노드를 찾는 순회(루트 단말)시에 4-노드를 만나면 미리 분할을 수행한다.

  - 3가지 경우를 고려해서 알고리즘을 만들어야 한다.

  - 4-노드가 루트인 경우 / 4-노드의 부모가 2-노드인 경우

  - 4-노드의 부모가 3-노드인 경우

 

 

[Reference]

  • 천인국, 공용해, 하상호, 「C언어로 쉽게 풀어쓴 자료구조」, 생능출판, 2009년
반응형

'DataStructure' 카테고리의 다른 글

Hashing(해싱)  (0) 2022.03.14
Graph(그래프) - 2  (0) 2022.03.11
Graph(그래프) - 1  (0) 2022.03.11
Sorting(정렬)  (0) 2022.03.11
Priority Queue(우선순위 큐)와 Heap(힙)  (0) 2022.03.11