촉촉한초코칩

03. 검색 본문

Algorithm

03. 검색

햄친구베이컨 2023. 3. 22. 14:06
03-1 검색 알고리즘

 

검색 기법 3가지

  1. 배열 검색
  2. 선형 리스트 검색 : 무작위로 늘어놓은 데이터 모임에서 검색 수행 
  3. 이진검색트리 검색  : 일정한 규칙으로 늘어놓은 데이터 모임에서 빠른 검색 수행 
  4. 해시법 : 추가, 삭제가 자주 일어나는 데이터 모임에서 빠른 검색 수해
    1. 체인법 : 같은 해시값의 데이터를 선형 리스트로 연결하는 방법
    2. 오픈 주소법 : 데이터를 위한 해시값이 충돌할 때 재해시하는 방법

* 추가/삭제가 자주 일어나는 경우 비용이 많이 든다. 

 

03-2 선형 검색 (linear search)

 

선형 검색 (순차 검색 sequential search)

  • 원하는 키 값을 갖는 요소를 만날 때까지 앞에서부터 순서대로 요소를 검색하는 것 

 

배열 검색의 종료 조건

  1. 검색할 값을 발견하지 못하고 배열 끝을 지나간 경우 > 검색 실패
  2. 검색할 값과 같은 요소를 발견한 경우 > 검색 성공 
#include <stido.h>
#include <stdlib.h>

int search(const int a[], int n, int key) {
     int i=0; 
     while(1) {
     	if(i==n)
           return -1;     //검색 실패
        if(a[i] == key) 
           return i;     //검색 성공
        i++;
     }
     
     for(i=0; i<n; i++) 
          if(a[i] == key)
               return i;  //검색 성공
     return -1;  //검색 실패 
}

int main(void) {
     int i, nx, ky, idx;
     int *x;
     puts("선형검색 ");
     printf("요소 개수 : ");
     scanf("%d", &nx);
     x = calloc(nx, sizeof(int));
     
     for(i=0; i<nx; i++) {
          printf("x[%d] : ", i);
          scanf("%d", &x[i]);
     }
    
    printf("검색값 : ");
    scanf("%d", &ky);
    idx = search(x, nx, ky);
    if(idx == -1) 
         puts("검색에 실패했습니다.");
    else
    	printf("%d(은)는 x[%d]에 있습니다.\n", ky, idx);
    free(x);
    
    return 0;
}

search 함수 

  • 배열 a의 처음부터 끝까지 n개의 요소를 대상으로 값이 key인 요소를 선형 검색한다. 
  • 반환값 > 발견한 요소의 인덱스 
  • 값이 key인 요소가 여러 개 존재하면 반환값은 검색 과정에서 처음 발견한 요소의 인덱스가 된다. 
  • 값이 key인 요소가 존재하지 않으면 -1을 반환한다. 

 

* 무한 루프 구현

while(1) { } for(; ; ) { } do { 

} while(1);

 

보초법 (sentinel method)

  • 선형검색은 반복할 때마다 다음 종료 조건(검색 실패/성공)을 모두 체크함 > 종료 조건을 검사하는 비용이 꽤 많이 듦
  • 보초법은 검색하기 전에 검색하고자 하는 키 값을 맨 끝 요소에 저장 = 보초 (sentinel)

 

  1. 선형검색 : 검색 성공에 대한 코드 필요 + 검색에 실패했을 때 반복 횟수 > 2 * n번
  2. 보초법 : 검색 성공에 대한 코드 불필요 + 검색에 실패했을 때 반복 횟수 > n번
#include <stido.h>
#include <stdlib.h>

int search(const int a[], int n, int key) {
     int i=0; 
     a[n]= key;
     while(1) {
         if(a[i] == key)
              break;
         i++;
     }
     return i==n? -1 : i; //i와 n이 같다 = 보초에 넣은 값과 같다 = 검색 실패 
}

int main(void) {
     int i, nx, ky, idx;
     int *x;
     puts("선형검색 ");
     printf("요소 개수 : ");
     scanf("%d", &nx);
     x = calloc(nx, sizeof(int));
     
     for(i=0; i<nx; i++) {
          printf("x[%d] : ", i);
          scanf("%d", &x[i]);
     }
    
    printf("검색값 : ");
    scanf("%d", &ky);
    idx = search(x, nx, ky);
    if(idx == -1) 
         puts("검색에 실패했습니다.");
    else
    	printf("%d(은)는 x[%d]에 있습니다.\n", ky, idx);
    free(x);
    
    return 0;
}

 

03-3 이진 검색

 

이진 검색 (Binary Search)

  • 데이터가 키 값으로 이미 정렬되어 있다. (오름차순/내림차순)
  • 선형 검색보다 조금 더 빠르게 검색할 수 있다. 

 

검색 과정

  1. 배열 중앙에 위치한 요소부터 검색 시작
  2. 배열 중앙 값 > 검색값 비교 : 검색 대상을 앞쪽으로 좁힌다.
  3. 배열 중앙 값 < 검색 값 비교 : 검색 대상을 뒤쪽으로 좁힌다. 
  4. 다시 배열 중앙에 위치한 요소를 찾아 비교한다. 

> 검색을 한 단계씩 진행할 때마다 검색 범위가 반으로 좁혀진다. 

* 사용자가 각 요소의 값을 입력할 때는 바로 앞에 입력한 요소보다 크거나/작아야 한다. 

 

n개의 요소가 오름차순으로 늘어선 배열 a에서 키를 이진 검색으로 검색하는 과정 

  1. 검색 범위의 맨 앞 인덱스 : pl  (0)
  2. 맨 끝 인덱스 : pr  (n-1)
  3. 중앙 인덱스 : pc  (n-1)/2
a[pc] < key 검색 범위 : a[pc+1] ~ a[pr]
pl = pc + 1
pc = (pl + pr / 2)
a[pc] > key 검색 범위 : a[pl] ~ a[pc-1]
pr = pc-1
pc = (pl + pr) / 2

 

이진검색 비교횟수 평균값 : log n

알고리즘 종료 조건

  1. a[pc]와 key가 일치하는 경우 > log n - 1회
  2. 검색 범위가 더 이상 없는 경우 > [log(n+1)]회 

* [x] : 천장 함수(ceiling funcion), 올림 함수 > x보다 크거나 같으면서 가장 작은 정수 

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

int bin_search(const int a[], int n, int key) {
     int pl = 0;
     int pr = n-1;
     int pc;
     do {
         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;		//검색실패 
}

int main(void) {
    int i, nx, ky, idx;
    int *x;
    puts("이진 검색 ");
    printf("요소 개수 : ");
    scanf("%d", &nx);
    x = calloc(nx, sizeof(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", &ky);
    idx = bin_search(x, nx, ky);
    if(idx == -1)
        puts("검색에 실패했습니다.");
    else
        printf("%d(는)은 x[%d]에 있습니다.\n", ky, idx);
    free(x);
      
    return 0;
}

 

복잡도 (Complexity)

  • 복잡도 : 알고리즘의 성능을 객관적으로 평가하는 기준 
  • 프로그램 실행 속도 > 하드웨어, 컴파일러 등 조건에 따라 달라진다. 

 

복잡도 두 가지 요소

  • 시간 복잡도 (time complexity) : 실행에 필요한 시간을 평가한 것
  • 공간 복잡도 (space complexity) : 기억 영역과 파일 공간이 얼마나 필요한가를 평가한 것 
실행 횟수 복잡도
1 O(1)
n O(n)
log n O(log n)

* 2개 이상의 복잡도로 구성된 알고리즘의 전체 복잡도는 차원이 더 높은 쪽의 복잡도를 우선시한다. 

 

bsearch 함수(정렬된 배열에서 검색하는 함수)

  • C언어의 표준 라이브러리, 다양한 요소의 자료형을 가진 배열에서도 검색 가능한 bsearch 함수 제공 > 일반 유틸리티(Utility) 함수 

bsearch 함수의 특징과 요소

  1. 검색 대상의 배열은 항상 정렬되어 있어야 한다.
  2. 검색하는 값과 같은 요소가 여러 개 존재하는 경우, 항상 가장 앞쪽에 있는 요소를 찾아내는 것은 아니다.
헤더 #include <stdlib.h>
형식 void *bsearch(const void *key, const void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *));
해설 맨 처음 요소를 base가 가리키는 요소의 개수가 nmemb개이고 요소 크기가 size인 객체의 배열에서 key가 가리키는 객체와 일치하는 요소를 검색한다.
compar이 가리키는 비교 함수 key 객체에 대한 포인터를 첫 번째 인수로, 배열 요소에 대한 포인터를 두 번째 인수로 하여 호출한다. 
compar이 가리키는 비교 함수key 객체가 배열 요소보다 작으면 작은 값을, 일치하면 0을, 크면 큰 값을 반환하도록 작성해야 한다.
compar이 가리키는 배열key 객체와 비교가 가능한 작은 요소, 같은 요소, 큰 요소의 세 부분으로 구성되어 있다. 이 세 부분이 소개한 순서로 존재해야 한다. 
반환값 검사하는 대상(배열) 중에 일치하는 요소에 대한 포인터를 반환한다. 
일치하는 요소가 없다면 NULL 포인터를 반환한다. 
두 요소의 값이 같을 때 어느 요소와 일치하는지는 규정되어 있지 않다.  (사용자가 설정)

bsearch 인수

key 키 값 (검색할 값이 저장된 객체에 대한 포인터)
base 배열의 포인터 
nmemb 배열이 요소 개수 
size 배열의 요소 크기 
compar 비교함수 
> 이진 검색의 검색 과정에는 검색하는 키 값과 배열의 요소값을 비교하여 대소 관계를 판단하는 과정이 포함된다. 
> 두 값을 비교하고 어떤 값을 반환할지는 사용자가 직접 작성한다. 
#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;
         
//   return *a < *b ? -1 : *a > *b ? 1 : 0;
}

int main(void) {
     int i, nx, ky;
     int *x; //배열의 첫 번째 요소에 대한 포인터
     int *p; //검색한 요소에 대한 포인터
     puts("bsearch 함수를 사용해서 검색 ");
     printf("요소 개수 : ");
     scanf("%d", &nx);
     x = calloc(nx, sizeof(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", &ky);
     
     p = bsearch(&ky,  //검색값에 대한 포인터
     	         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 *
  • bsearch 함수가 받는 비교 함수의 인수의 자료형 : void * 
  • > bsearch 함수 호출에 맞도록 형변환을 해야 한다. 
  • 이때 비교함수의 인수 자료형을 void로 바꾸면 형변환을 하지 않아도 된다. (필요에 따라 구현)
//캐스팅 없이 사용할 수 있는 비교 함수
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;
}

bsearch 함수의 반환값 

  • 함수 반환값 : 검색을 통해 찾은 요소의 포인터 
  • 요소의 인덱스 : (요소 포인터 - 첫 번째 요소의 포인터) 
  • 만약 검색에 실패한 경우 : 널(NULL) 포인터 반환

 

* 내림차순으로 정렬된 배열에서 검색하기 

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

int int_cmpr(const int *a, const int *b) {
     if(*a < *b) 
        return 1;
     else if(*a > *b) 
        return -1;
     else 
        return 0;
}

int main(void) {
   int i, ky, nx;
   int *x; //배열 첫번째 요소에 대한 포인터
   int *p; //검색한 요소에 대한 포인터
   puts("bsearch 함수를 사용하여 검색");
   printf("요소 개수 : ");
   scanf("%d", &nx);
   x = calloc(nx, sizeof(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", &ky);
   p = bsearch(&ky, 	//검색값에 대한 포인터
               x, 		//배열
               nx,		//요소 개수 
               sizeof(int),	//요소 크기
               (int(*)(const void *, const void *)) int_cmpr);		//비교 함수
   if(p == NULL)
       puts("검색에 실패했습니다");
   else
       printf("%d는(은) x[%d]에 있습니다.\n", ky, (int)(p-x));
   free(x);
   
   return 0;
}

 

* 함수 포인터 

  • 함수를 가리키는 포인터
  • 함수 포인터의 자료형 : 가리키는 함수에 따라 다르다. 
  • *변수명 : 객체에 대한 포인터 선언
  • (*변수명) : 포인터를 반환하는 함수 
  • 함수 이름 = 그 함수에 대한 포인터 
double func(int);  //int를 받아들여 double을 반환하는 함수 
double(*fp)(int);  //int를 받아들여 double을 반환하는 함수에 대한 포인터 fp 선언 
double *fn(int);   //int를 받아들여 double에 대한 포인터를 반환하는 함수 선언
#include <stdio.h>

int sum(int x1, int x2) {
   return x1 + x2;
}

int mul(int x1, int x2) {
   return x1 * x2;
}

//sum, mul 함수에 대한 포인터를 매개변수 calc로 받아들인다.
//받아들인 함수에 대한 포인터를 사용하는 코드 : (*calc)(i,j)
//함수에 대한 포인터에 간접연산자 *를 사용하면 그 포인터가 가리키는 함수가 호출된다. 
//해당 코드는 컴파일할 때가 아니라 실행할 때 실제로 어떤 함수를 호출할지 결정한다. 
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');
   }
}

//함수에 대한 포인터는 매개변수 선언에서만 변수 이름 앞에 *와 ()를 생략한 형식으로 선언할 수 있다. 
//void kuku(int calc(int int)) {
//    int i, j;
//    ...
//}

int main(void) {
  puts("덧셈표");
  kuku(sum);
  puts("\n 곱셈표 ");
  kuku(mul);
  
  return 0;
}

 

  • 함수에 대한 포인터를 사용하면 호출하는 함수를 실행하여 결정하는 동적 함수 호출을 구현할 수 있다. 
  • 동적 함수 호출을 사용하면 사용자가 원하는 경우에 sum/mul 함수를 직접 작성하여 호출하지 않아도 실행할 수 있다. 
  • 함수에 대한 포인터는 매개변수 선언에서만 변수 이름 앞에 *와 ()를 생략한 형식으로 선언할 수 있다. 
    •   배열을 매개변수로 하는 함수에서 int *p를 int p[]로 선언하는 것과 비슷하다. 
  • 함수 호출식의 왼쪽 피연산자는 함수 포인터가 아닌 함수 이름을 사용해도 된다 > calc(i, j)

 

구조체 배열에서 검색하기 

#include <stdio.h>
#include <stdlib.h>
#include <sring.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]);
  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;
}

 

* strcmp 반환하는 값 

  1. x->name 쪽이 작으면 (알파벳 순서의 앞쪽이면) 음수 
  2. x->name과 y->name이 같으면 0
  3. x->name 쪽이 크면 (알파벳 순서의 뒤쪽이면) 양수 

'Algorithm' 카테고리의 다른 글

04. 스택과 큐  (0) 2023.03.31
[백준] 2587(대표값2) c언어  (0) 2023.03.25
[백준] 2566(최댓값) c언어  (0) 2023.03.18
[백준] 2738(행렬 덧셈) c언어  (0) 2023.03.13
[백준] 10810(공 넣기) c언어  (0) 2023.03.06