Frinee의 코드저장소

탐색(Search) 1

by Frinee

이 글은 윤성우 저 - "윤성우의 열혈 자료구조"를 공부하고 정리하여 작성하였습니다.

1. 탐색의 이해와 보간 탐색

❓ 탐색의 이해

  • 데이터를 찾는 방법을 말함.
  • 효율적인 탐색은 대부분 트리의 연장성상에서 이루어짐.

🚨 보간 탐색(interpolation Search)

  • 보간 탐색은 이진 탐색의 비효율성을 개선시킨 알고리즘이다.
  • 보간 탐색은 이진 탐색과 마찬가지로 모두 정렬이 완료된 데이터를 대상으로 진행함.

“이진 탐색처럼 그냥 중앙에서 시작하지 말고, 탐색대상이 앞쪽에 위치해 있으면 앞쪽에서 탐색을 시작하자!”

  • 비례식을 구성하여 탐색 위치의 인덱스 값을 계산한다.
  • 다만 오차율을 최소화하기 위해 실수형 나눗셈을 진행한다.

🔑 탐색 키(Search Key)와 탐색 데이터(Search Data)

typedef int Key;  // 탐색 키에 대한 typedef 선언

typedef double Data; // 탐색 데이터에 대한 typedef 선언

typedef struct item
{
    Key searchKey;    // 탐색 키(search key)
    Data searchData;  // 탐색 데이터(search data)
} Item;

 

🧑🏻‍💻 보간 탐색의 구현

int ISearch(int ar[], int first, int last, int target)
{
    int mid;

    if(ar[first] > target || ar[last] < target)
        return -1;  // 탐색 실패

    // 이진 탐색과의 차이점
    mid = ((double)(target - ar[first]) / (ar[last] - ar[first]) * (last - first)) + first;

    if(ar[mid] == target)
        return mid;
    else if(target < ar[mid])
        return ISearch(ar, first, mid-1, target);
    else
        return ISearch(ar, mid+1, last, target);
}

 

⚠️ 탈출조건을 만족하지 않는 이유

ex)

int arr\[\] = {1,3,5,7,9};

ISearch(arr,1,4,2);

mid = 0 이 저장됨.

  • 그래서 return ISearch(ar, mid+1, last, target) 이 실행이 되버리고 이는 ISearch(arr, 1, 4, 2) 가 되어 탈출할 수 없음
  • 그래서 if(ar\[first\] > target || ar\[last\] < target) return -1; // 탐색 실패를 설정해준다.

 

2. 이진 탐색 트리

  • 이진 트리에 데이터 저장 규칙을 더해 놓은 것이 이진 탐색 트리라 할 수 있다.
    • 이진 탐색 트리의 노드에 저장된 키(key)는 유일하다.
    • 루트 노드의 키가 왼쪽 서브 트리를 구성하는 어떠한 노드의 키보다 크다.
    • 루트 노드의 키가 오른쪽 서브 트리를 구성하는 어떠한 노드의 키보다 작다.
    • 왼쪽과 오른쪽 서브 트리도 이진 탐색 트리이다.

왼쪽 자식 노드의 키 < 부모 노드의 키 < 오른쪽 자식 노드의 키

  • 이진 탐색 트리에 데이터를 추가한다 했을 때
    • 루트 노드와 비교했을 때, 값이 크면 오른쪽 자식 노드로 이동
    • 루트 노드와 비교했을 때, 값이 작으면 왼쪽 자식 노드로 이동
    • 왼쪽/오른쪽 노드가 없으면 그 위치에 저장

🌲 이진 탐색 트리 헤더 파일

  • BinaryTree2.h
BTreeNode * MakeBTreeNode(void);
- 노드를 동적으로 할당해서 그 노드의 주소 값을 반환한다.
BTData GetData(BTreeNode * bt);
- 노드에 저장된 데이터를 반환한다.
void SetData(BTreeNode *bt, BTData data);
- 인자로 전달된 데이터를 노드에 저장한다.
BTreeNode * GetLeftSubTree(BTreeNode * bt);
- 인자로 전달된 노드의 왼쪽 자식 노드의 주소 값을 반환
BTreeNode * GetRightSubTree(BTreeNode * bt);
- 인자로 전달된 노드의 오른쪽 자식 노드의 주소 값을 반환
void MakeLeftSubTree(BTreeNode *main, BTreeNode *sub);
- 인자로 전달된 노드의 왼쪽 자식 노드를 교체
void MakeRightSubTree(BTreeNode *main, BTreeNode *sub);
- 인자로 전달된 노드의 오른쪽 자식 노드를 교체
  • BinarySearchTree.h
#include "BinaryTree2.h"

typedef BTData BSTData;

// BST의 생성 및 초기화
void BSTMakeAndInit(BTreeNode ** pRoot);

// 노드에 저장된 데이터 반환
BSTData BSTGetNodeData(BTreeNode *bst);

// BST를 대상으로 데이터 저장(노드의 생성과정 포함)
void BSTInsert(BTreeNode ** pRoot, BSTData data);

// BST를 대상으로 데이터 탐색
BTreeNode * BSTSearch(BTreeNode *bst, BSTData target);

 

🌳 이진 탐색 트리: 삽입과 탐색

void BSTInsert(BTreeNode ** pRoot, BSTData data)
{
	BTreeNode * pNode = NULL;    // parent Node
	BTreeNode * cNode = *pRoot;  // current Node
	BTreeNode * nNode = NULL;    // new Node
	
	// 새로운 노드가 추가될 위치를 찾는다.
	while(cNode != NULL)
	{
		if(data == GetData(cNode))
			return;   // 키의 중복 허용하지 않음
		
		pNode = cNode;
		if(GetData(cNode) > data)
			cNode = GetLeftSubTree(cNode);
		else
			cNode = GetRightSubTree(cNode);
	}
	
	//pNode의 자식노드로 추가할 새 노드의 생성
	nNode = MakeBTreeNode();  // 새 노드 생성
	SetData(nNode, data);     // 새 노드에 데이터 저장
	
	//pNode의 자식 노드로 새 노드를 추가
	if(pNode != NULL)   // 새 노드가 루트 노드가 아니라면,
	{
		if(data < GetData(pNode))
			MakeLeftSubTree(pNode, nNode);
		else
			MakeRightSubTree(pNode, nNode);
	}
	else
	{
		*pRoot = nNode;
	}
}

BTreeNode * BSTSearch(BTreeNode *bst, BSTData target)
{
	BTreeNode * cNode = bst;  // current node
	BSTData cd;               // current data
	
	while(cNode != NULL)
	{
		cd = GetData(cNode);
		
		if(target == cd)
			return cNode;
		else if(target < cd)
			cNode = GetLeftSubTree(cNode);
		else
			cNode = GetRightSubTree(cNode);
	}
	
	return NULL;
}

 

🌳❌ 이진 탐색 트리: 삭제

  • 이진 탐색 트리에서 임의의 노드를 삭제하는 경우, 삭제 후에도 이진 탐색 트리가 유지되도록 빈 자리를 채워야 함.
  • 상황 1 : 삭제할 노드가 단말 노드인 경우

if(삭제할 노드가 단말 노드이면)
{
	if(GetLeftSubTree(pNode) == dNode)  // 삭제할 노드가 왼쪽 자식 노드라면
		RemoveLeftSubTree(pNode);
	else
		RemoveRightSubTree(pNode);
}
  • 상황 2: 삭제할 노드가 하나의 자식 노드를 갖는 경우

if(삭제할 노드가 하나의 자식 노드를 갖고 있다면)
{
	BTreeNode * dcNode;
	
	// 삭제 대상의 자식 노드를 찾는다.
	if(GetLeftSubTree(dNode) != NULL)  // 자식 노드가 왼쪽이라면
		dcNode = GetLeftSubTree(dNode);
	else
		dcNode = GetRightSubTree(dNode);
		
	// 삭제 대상의 부모 노드와 자식 노드를 연결한다.
	if(GetLeftSubTree(pNode) == dNode)  // 삭제 대상이 왼쪽 자식 노드이면
		ChangeLeftSubTree(pNode, dcNode); // 왼쪽 연결
	else
		ChangeRightSubTree(pNode, dcNode);
}
  • 상황 3: 삭제할 노드가 두 개의 자식 노드를 갖는 경우

  • 왼쪽 서브트리의 가장 큰 값, 또는 오른쪽 서브트리의 가장 작은 값으로 대체한다.

  • 가장 큰 값을 찾을 때는 NULL을 만날 때까지 계속 오른쪽 자식 노드로 이동
  • 가장 작은 값을 찾을 때는 NULL을 만날 때까지 계속해서 왼쪽 자식 노드로 이동
“삭제할 노드의 오른쪽 서브 트리에서 가장 작은 값을 지니는 노드를 찾아서 이것으로 삭제할 노드를 대체한다.”

  • 단계 1 : 삭제할 노드를 대체할 노드를 찾는다.
  • 단계 2 : 대체할 노드에 저장된 값을 삭제할 노드에 대입한다.
  • 단계 3 : 대체할 노드의 부모 노드와 자식 노드를 연결한다.
if(삭제할 노드가 두 개의 자식 노드를 지닌다.)
{
	BTreeNode * mNode = GetRightSubTree(dNode);  // 대체 노드
	BTreeNode * mpNode = dNode;  // 대체 노드의 부모 노드
	
	// 단계 1. 삭제 대상의 대체 노드를 찾는다.
	while(GetLeftSubTree(mNode) != NULL)
	{
		mpNode = mNode;
		mNode = GetLeftSubTree(mNode);
	}
	
	// 단계 2. 대체할 노드에 저장된 값을 삭제할 노드에 대입한다.
	SetData(dNode, GetData(mNode));
	
	// 단계 3. 대체할 노드의 부모 노드와 자식 노드를 연결한다.
	if(GetLeftSubTree(mpNode) == mNode)
	{
		// 대체할 노드의 자식 노드를 부모 노드의 왼쪽에 연결
		ChangeLeftSubTree(mpNode, GetRightSubTree(mNode));
	}
	else
	{
		ChangeRightSubTree(mpNode, GetRightSubTree(mNode));
	}
}

 

🎄❌ 이진 탐색 트리: 삭제를 위한 이진 트리 확장

// 왼쪽 자식 노드를 트리에서 제거, 제거된 노드의 주소 값이 반환된다.
BTreeNode * RemoveLeftSubTree(BTreeNode *bt)
{
	BTreeNode * delNode;
	
	if(bt != NULL){
		delNode = bt->left;
		bt->left = NULL;
	}
	return delNode;
}

// 오른쪽 자식 노드를 트리에서 제거, 제거된 노드의 주소 값이 반환된다.
BTreeNode * RemoveRightSubTree(BTreeNode *bt)
{
	BTreeNode * delNode;
	
	if(bt != NULL){
		delNode = bt->right;
		bt->right = NULL;
	}
	return delNode;
}

// 메모리 소멸을 수반하지 않고 main의 왼쪽 자식 노드를 변경
void ChangeLeftSubTree(BTreeNode *main, BTreeNode * sub)
{
	main->left = sub;
}

// 메모리 소멸을 수반하지 않고 main의 오른쪽 자식 노드를 변경
void ChangeRightSubTree(BTreeNode *main, BTreeNode * sub)
{
	main->right = sub;
}

 

🎄이진 탐색 트리: 완전한 구현 🎄

BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target)
{
	BTreeNode *pVRoot = MakeBTreeNode();
	BTreeNode *pNode = pVRoot;
	BTreeNode *cNode = *pRoot;
	BTreeNode *dNode;
	
	// 루트 노드를 pVRoot가 가리키는 노드의 오른쪽 자식 노드가 되게 한다.
	ChangeRightSubTree(pVRoot, *pRoot);
	
	// 삭제 대상인 노드를 탐색
	while(cNode != NULL && GetData(cNode) != target)
	{
		pNode = cNode;
		if(target < GetData(cNode))
			cNode = GetLeftSubTree(cNode);
		else
			cNode = GetRightSubTree(cNode);
	}
	
	if(cNode == NULL)
		return NULL;
		
	dNode = cNode;
	
	// 1. 삭제할 노드가 단말 노드이면
	if(GetLeftSubTree(dNode) == NULL && GetRightSubTree(dNode) == NULL)
	{
		if(GetLeftSubTree(pNode) == dNode)  // 삭제할 노드가 왼쪽 자식 노드라면
			RemoveLeftSubTree(pNode);
		else
			RemoveRightSubTree(pNode);
	}
	// 2. 삭제할 노드가 하나의 자식 노드를 갖고 있다면
	else if(GetLeftSubTree(dNode) == NULL || GetRightSubTree(dNode) == NULL)
	{
		BTreeNode * dcNode;
		
		// 삭제 대상의 자식 노드를 찾는다.
		if(GetLeftSubTree(dNode) != NULL)  // 자식 노드가 왼쪽이라면
			dcNode = GetLeftSubTree(dNode);
		else
			dcNode = GetRightSubTree(dNode);
			
		// 삭제 대상의 부모 노드와 자식 노드를 연결한다.
		if(GetLeftSubTree(pNode) == dNode)  // 삭제 대상이 왼쪽 자식 노드이면
			ChangeLeftSubTree(pNode, dcNode); // 왼쪽 연결
		else
			ChangeRightSubTree(pNode, dcNode);
	}
	// 3. 삭제할 노드가 두 개의 자식 노드를 지닌다.
	else
	{
		BTreeNode * mNode = GetRightSubTree(dNode);  // 대체 노드
		BTreeNode * mpNode = dNode;  // 대체 노드의 부모 노드
		int delData;
		
		// 단계 1. 삭제 대상의 대체 노드를 찾는다.
		while(GetLeftSubTree(mNode) != NULL)
		{
			mpNode = mNode;
			mNode = GetLeftSubTree(mNode);
		}
		
		// 단계 2. 대체할 노드에 저장된 값을 삭제할 노드에 대입한다.
		delData = GetData(dNode);  // 대입 전 데이터 백업
		SetData(dNode, GetData(mNode));
		
		// 단계 3. 대체할 노드의 부모 노드와 자식 노드를 연결한다.
		if(GetLeftSubTree(mpNode) == mNode)
			// 대체할 노드의 자식 노드를 부모 노드의 왼쪽에 연결
			ChangeLeftSubTree(mpNode, GetRightSubTree(mNode));
		else
			ChangeRightSubTree(mpNode, GetRightSubTree(mNode));
		
		dNode = mNode;
		SetData(dNode, delData);
	}
	
	// 삭제된 노드가 루트노드라면
	if(GetRightSubTree(pVRoot) != *pRoot)
		*pRoot = GetRightSubTree(pVRoot);  // 루트 노드 변경
	 	
	free(pVRoot);  // 가상 루트 노드 소멸
	return dNode;
}

  • 가상 루트 노드를 둔 이유는 삭제할 노드가 루트 노드인 경우를 일반화하기 위함.

 

자료

  • 윤성우의 열혈 자료구조 (윤성우 저, 2023.10)

'[컴퓨터 과학자 스터디] > 자료구조' 카테고리의 다른 글

테이블(Table)과 해쉬(Hash)  (2) 2024.12.05
탐색(Search) 2  (0) 2024.12.02
정렬(Sorting)  (0) 2024.11.27
우선순위 큐(Priority Queue)와 힙(Heap)  (0) 2024.11.24
트리(tree)  (0) 2024.11.23

블로그의 정보

프리니의 코드저장소

Frinee

활동하기