이진 검색의 알고리즘을 적용하는 전제 조건은 데이터가 키 값으로 이미 정렬(Sort)되어 있다는 것입니다.
아래의 이진 검색 알고리즘은 아래와 오름차순으로 정렬이 되어있단 가정하에 key값을 찾습니다.
int bin_search(const int a[], int n, int key) {
int pl = 0; // 검색 범위 맨앞 인덱스
int pr = n - 1; // 검색 범위 맨뒤 인덱스
do {
int pc = (pl + pr) / 2; // 중앙 요소 인덱스
if (a[pc] == key)
return pc; // 검색 성공
else if (a[pc] < key) // 검색 범위를 뒤쪽 반으로 줄임
pl = pc + 1;
else // 검색 범위를 앞쪽 반으로 줄임
pr = pc - 1;
} while (pl <= pr); // 검색 범위의 맨앞 인덱스가 맨뒤 인덱스를 넘지 않을때 반복
return -1; // 검색 실패
}
알고리즘 종료 조건에는 다음과 같은 경우가 있습니다.
1. a[pc]와 key가 일치하는 경우
2. 검색 범위가 더이상 없는 경우
- a[pc]와 key가 일치하는 경우
a[pc]의 값이 7과 일치하면 7이 들어있는 배열의 인덱스(pc)를 반환합니다.
- 검색 범위가 더이상 없는 경우
검색 범위가 없을 경우, 즉, pl > pr을 만족 할경우 -1를 반환합니다.
bsearch 함수
C 언어의 표준 라이브러리는 다양한 요소의 자료형을 가진 배열에서도 검색 가능한 bsearch 함수를 제공합니다. 이 함수를 일반 유틸리티(utility) 함수라고 부릅니다.
특징 1. 검색 대상의 배열은 항상 정렬되어 있어야함
특징 2. 검색하는 값과 같은 요소가 여러개 존재하는 경우, 항상 가장 앞쪽에 있는 요소를 찾아내는 건 아님
besearch 함수를 사용해서 실습을 해보겠습니다.
/* bsearch 함수를 사용하여 오름차순으로 정렬된 배열을 검색 */
#include <stdio.h>
#include <stdlib.h>
/*--- 정수를 비교하는 함수(오름차순) ---*/
int int_cmp(const int* a, const int* b)
{
if (*a < *b)
return -1;
else if (*a > * b)
return 1;
else
return 0;
}
int main()
{
int i, nx, key;
int* x = NULL; /* 배열의 첫 번째 요소에 대한 포인터 */
int* p = NULL; /* 검색한 요소에 대한 포인터 */
puts("bsearch 함수를 사용하여 검색");
printf("요소의 개수 : ");
scanf("%d", &nx);
x = calloc(nx, sizeof(int)); /* 요소의 개수 nx인 int형 배열을 생성 */
printf("오름차순으로 입력하세요.\n");
printf("x[0] : ");
scanf("%d", &x[0]);
for (i = 1; i < nx; i++) {
do {
printf("x[%d] : ", i);
scanf("%d", &x[i]);
} while (x[i] < x[i - 1]); /* 바로 앞의 값보다 작으면 다시 입력 */
}
printf("검색 값 : ");
scanf("%d", &key);
p = bsearch(&key, /* 검색 값에 대한 포인터 */
x, /* 배열 */
nx, /* 요소의 개수 */
sizeof(int), /* 요소의 크기 */
(int(*)(const void*, const void*)) int_cmp /* 비교 함수 */
);
if (p == NULL)
puts("검색에 실패했습니다.");
else
printf("%d는 x[%d]에 있습니다.\n", ky, (int)(p - x));
free(x); /* 배열을 해제 */
return 0;
}
bsearch 다섯번째 인수인 바교함수
int int_cmp(const int* a, const int* b)
{
if (*a < *b)
return -1;
else if (*a > * b)
return 1;
else
return 0;
}
이진 검색의 검색 과정에는 검색하는 값과 배열의 요소값을 비교하여 대소 관계를 판단하는 과정이 포합됩니다. 대소를 판단하는 방법은 요소의 자료형마다 다릅니다. 자료형은 정수, 문자열 혹은 구조체일 수도 있습니다. 그러므로 두값을 비교하고 난 다음의 어떤 값을 반환하는 비교 함수는 사용자가 직접 작성해야 합니다. 따라서 bsearch 함수는 다섯번째 매개변수로 이 비교 함수에 대한 포인터를 전달하는 방식입니다.
bsearch 함수의 반환값
p = bsearch(&key, /* 검색 값에 대한 포인터 */
x, /* 배열 */
nx, /* 요소의 개수 */
sizeof(int), /* 요소의 크기 */
(int(*)(const void*, const void*)) int_cmp /* 비교 함수 */
);
if (p == NULL)
puts("검색에 실패했습니다.");
else
printf("%d는 x[%d]에 있습니다.\n", ky, (int)(p - x));
bsearch 함수의 반환값은 검색을 통해 찾은 요소의 포인터입니다.
p는 bsearch 함수의 반환값을 대입한 p는 찾은 요소를 가리킵니다. 따라서 그 요소의 인덱스는 포인터 p에서 첫 번째 요소의 포인터 x를 뺀 식 p-x로 얻을 수 있습니다. 예를 들어, x의 값이 0x62FE40이고 p의 값이 0x62FE48이면 p-x는 8byte 이고 int형은 요소당 4byte이므로 인덱스는 2입니다. 또한 bsearch 함수는 검색에 실패한 경우 NULL 포인터를 반환합니다.
bsearch 함수의 호출
비교함수 int_cmp가 받는 인수의 자료형은 int *이고 bsearch 함수가 받는 비교 함수의 인수의 자료형은 void *입니다. 두 자료형이 다르므로 bsearch 함수의 호출에 맞도록 형 변환을 해야합니다.
p = bsearch(&key, /* 검색 값에 대한 포인터 */
x, /* 배열 */
nx, /* 요소의 개수 */
sizeof(int), /* 요소의 크기 */
(int(*)(const void*, const void*)) int_cmp /* 비교 함수 */
);
int_cmp를 아래와 같이 정의하면 호출할 때 현 변환을 하지 않아도 됩니다. 대신 비교 함수 안에서 형 변환을 많이 할 수도 있으므로 필요에 따라 함수를 구현하여 형 변환하면 됩니다.
int int_cmp(const void* a, const void* b) //
{
if (*(int *)a < *(int *)b)
return -1;
else if (*(int *)a > * (int *)b)
return 1;
else
return 0;
}
void *형의 포인터 a를 int *형으로 형 변환한 포인터를 간접 연산자 *를 적용하여 *(int *)a는 int형 포인터 a가 가리키는 객체의 *a값을 나타냅니다.
위의 함수 정의는 캐스팅 없이 사용하 수 있는 비교 함수를 구현하였습니다.
Cast는 형 변환이라고 하며 특정한 형으로 자료형을 변환하는 연산자입니다. 예를 들어, int형 변수를 나눗셈할 때 소수점 아래에 자리가 생기는 경우가 있는데, 이때 캐스팅을 하지 않으면 소수점 아래의 값이 사라집니다. 이를 해결하려면 int형 변수를 나눗셈할 떄 double형으로 변환하고 그 결과를 double형 변수에 저장하여야 소수점 아래의 값을 얻을 수 있습니다.
함수 포인터
bsearch 함수의 호출의 다섯번째 인수를 보면 특이한 점을 볼 수 있습니다.
여기서 짚고 넘어가야할 개념인 함수 포인터를 알아보겠습니다.
함수 포인터는 이름 그대로 함수를 가리키는 포인터입니다. 함수 포인터의 자료형은 가리키는 함수에 따라 다릅니다.
아래처러 선언한 함수를 살펴보겠습니다.
double func(int); // int를 받아들여 double을 반환하는 함수
double(*fp)(int); // int를 받아들여 double을 반환하는 함수에 대한 포인터 fp의 선언
double *fn(int); // int를 받아들여 double에 대한 포인터를 반환하는 함수 선언
double *fn(int) 처럼 변수 이름 앞에 *를 붙이는 것은 객체에 대한 포인터의 선언과 같습니다. 다만 변수 이름을 ()로 감싸야합니다. ()를 생략하여 double 형에 대한 포인터를 반환하는 함수의 선언이 됩니다.
/* 덧셈표와 곱셈표 */
#include <stdio.h>
/*--- x1과 x2의 합을 구합니다. ---*/
int sum(int x1, int x2)
{
return x1 + x2;
}
/*--- x1과 x2의 곱을 구합니다. ---*/
int mul(int x1, int x2)
{
return x1 * x2;
}
/*--- 덧셈, 곱셈표를 출력 ---*/
void kuku(int(*calc)(int, int))
{
int i, j;
for (i = 1; i <= 9; i++) {
for (j = 1; j <= 9; j++)
printf("%3d", (*calc)(i, j));
putchar('\n');
}
}
int main(void)
{
puts("덧셈표");
kuku(sum);
puts("\n 곱셈표");
kuku(mul);
return 0;
}
위와 같이 함수에 대한 포인터를 사용하면 호출하는 함수를 실행하여 결정하는 동적 함수 호출을 구현할 수 있습니다. 동적 함수 호출을 사용하면 사용자가 원하는 경우에 sum 함수와 mul 함수를 직접 작성하여 호출하지 않아도 실행할 수 있습니다.
함수에 대한 포인터 표현
void kuku(int calc(int, int))
{
int i, j;
for(i = 1; i <= 9; i++) {
for(j = 1; j <= 9; j++ {
printf("%3d", calc(i, j));
}
putchar('\n');
}
}
매개변수 선언
훔수에 대한 포인터는 매개변수 선언에서만 변수 이름 앞의 *와 ()를 생략한 형식으로 선언할 수 있습니다. 배열을 매개변수로 하는 함수에서 int *p를 int p[]로 선언하는 경우와 비슷합니다.
함수에 대한 포인터를 통한 함수의 호출
일반적으로 함수 호출식의 왼쪽 피연산자는 함수 포인터가 안닌 함수 이름을 사용해도 됩니다. 간단히 (*calc)(i, j)를 calc(i, j)라고 해도 됩니다.
구조체 배열에서 검색하기
자료형이 구조체인 배열에서의 검색을 bsearch 함수로 구현해보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[10]; /* 이름 */
int height; /* 키 */
int weight; /* 몸무게 */
} Person;
/*--- Person형의 비교 함수(오름차순으로 이름 정렬) ---*/
int npcmp(const Person* x, const Person* y)
{
return strcmp(x->name, y->name);
}
int main(void)
{
Person x[] = { /* 배열 요소는 이름의 오름차순으로 */
{ "김영준", 179, 79 }, /* 정렬되어 있지 않으면 안 됩니다. */
{ "박현규", 172, 63 },
{ "이수진", 176, 52 },
{ "최윤미", 165, 51 },
{ "함진아", 181, 73 },
{ "홍연의", 172, 84 },
};
int nx = sizeof(x) / sizeof(x[0]); /* 배열 x의 요소의 개수 */
int retry;
puts("이름으로 검색합니다.");
do {
Person temp, * p;
printf("이름 : ");
scanf("%s", temp.name);
p = bsearch(&temp, x, nx, sizeof(Person),
(int(*)(const void*, const void*)) npcmp);
if (p == NULL)
puts("검색에 실패했습니다.");
else {
puts("검색 성공 !! 아래 요소를 찾았습니다.");
printf("x[%d] : %s %dcm %dkg\n",
(int)(p - x), p->name, p->height, p->weight);
}
printf("다시 검색할까요? (1) 예 / (0) 아니오 : ");
scanf("%d", &retry);
} while (retry == 1);
return 0;
}
실행결과
구조체 Person은 {이름, 키, 몸무게의 멤버}로 구성되어 있습니다.
Person을 자료형으로 하는 배열이 검색 대상인 x입니다. 배열 x의 요소는 이름인 멤버 name을 기준으로 오름차순으로 정렬하면서 초기화합니다.
비교함수 npcmp는 두 문자열 x->name과 y->name의 대소 관계를 strcmp 함수를 호출하여 비교합니다. strcmp 함수의 반환값은 다음과 같습니다.