촉촉한초코칩

08. 문자열 본문

Algorithm

08. 문자열

햄친구베이컨 2023. 5. 25. 21:23
08-1 문자열의 기본 

 

문자열  (String)

  • 문자의 나열을 나타낸 것 
  • 문자 하나만 있어도 좋고, 비어 있어도 상관없다. 빈 문자열도 문자열이다. 

 

문자열 리터럴 (String iteral) 

  • 문자의 나열을 2개의 큰따옴표(")로 묶은 것 
  • 문자열 안의 문자는 메모리 공간에 연속으로 배치된다. 
  • 컴퓨터는 문자열 리터럴의 끝을 나타내기 위해 널 문자(null character)를 자동으로 추가한다. 
  • 널문자의 비트 값은 0이다. 
  • 문자열 리터럴 값은 바꿀 수 없다. 
  • 자유롭게 읽고 쓰는 문자열은 배열로 구현해야 한다. 

 

문자열을 구성하는 모든 문자의 값을 16진수와 2진수로 출력하기 

#include <stdio.h>
#include <limits.h>

void str_dump (const char *s) {
  do { 
    int i;
    printf("%c %0*X ", *s, (CHAR_BIT + 3) / 4, *s);
    for(i = CHAR_BIT -1; i>=0; i--)
       putchar(((*s>>i) & 1U) ? '1' : '0');
    putchar('\n');
  } while(*s++ != '\0');
}

int main(void) {
   str_dupm("STRING");
   return 0;
}
  • 문자열 리터럴 자료형 : char형 배열
  • 문자열 리터럴의 표현식을 평가하여 얻는 자료형 : char *s형 > 값은 첫번째 문자에 대한 포인터이다. 
  • 문자열 리터럴 메모리 영역 기간 : 정적 메모리 영역과 동일하다. 프로그램의 시작부터 끝까지 메모리 영역이 유지된다.
  • 같은 문자열 리터럴이 여러 개 있는 경우 : 각각 다른 메모리 영역에 넣어두거나 같은 영역에 넣어두고 공유하는 컴퓨터 환경도 존재
  • 변수가 아니라 상수 성질을 가지고 있어서 문자열 리터럴이 저장된 메모리 영역에 값을 대입할 수 없다. 
  • 문자열의 형식 문자열 : %s 

 

배열에 문자열 저장하기 

#include <stdio.h>

int main(void) {
   char st[10];
   st[0] = 'A';
   st[1] = 'B';
   st[2] = 'C';
   st[3] = 'D';
   st[4] = '\0'; //문자열의 끝을 나타낸다. st[5]에 문자를 대입해도 ABCD만 출력된다. 
   printf("문자열 st에는 \"%s\"가 들어 있습니다.\n", st);
   
   return 0;
}

 

문자열 선언과 동시에 초기화하기

#include <stdio.h>

int main(void) {
   char st[10] = "ABCD";
   printf("문자열 st에는 \"%s\"가 들어 있습니다.\n", st);
   
   //오류 코드
   st = {'A','B','C','D','\0'};
   st = "ABCD";
   
   //요소 개수 생략하고 문자열 선언 : 초기화할 때 입력한 문자열의 요소 개수가 배열 요소 개수가 된다.
   char st[] = "ABCD"; //요소 개수 : 5개 
   
   return 0;
}

 

포인터로 문자열 나타내기 

#include <stdio.h>

int main(void) {
   char *pt = "12345";  //pt는 메모리 영역의 첫번째 문자 1을 가리키며 초기화된다. 
   printf("포인터 pt는 \"%s\"를(을) 가리킵니다.\n", pt);
   
   return 0;
}

 

배열과 포인터에 의한 문자열 메모리 영역 크기 비교 

char st[] = "12345"; //배열에 의한 문자열 크기 : 6바이트
char *pt = "12345";  //포인터에 의한 문자열 크기 : sizeof(char*) + 6바이트
  • 포인터로 표현한 문자열은 문자열 리터럴을 저장하기 위한 영역 외에도 pt가 갖는 메모리 영역이 더 필요하다. 
  • pt를 위한 영역인 sizeof(char *) 바이트와 문자열 리터럴을 위한 영역인 sizeof("12345")바이트만큼 메모리 영역 필요 
  • 포인터로 표현한 문자열 영역 > 배열에 저장한 문자열 영역 

 

문자열 리터럴을 가리키는 두 포인터의 값을 교환하는 프로그램 

#include <stdio.h>

void swap_ptr (char **x, char **y) {
   char *tmp = *x;
   *x = *y;
   *y = tmp;
}

int main(void) {
   char *s1 = "ABCD";
   char *s2 = "EFGH";
   
   printf("포인터 s1은 \"%s\"를 가리킵니다.\n", s1);
   printf("포인터 s2은 \"%s\"를 가리킵니다.\n", s2);
   
   swap_ptr(&s1, &s2);
   
   puts("\n포인터 s1과 s2의 값을 서로 교환했습니다.\n");
   
   printf("포인터 s1은 \"%s\"를 가리킵니다.\n", s1);
   printf("포인터 s2은 \"%s\"를 가리킵니다.\n", s2);
   
   return 0;
}
  • 매개변수 x, y의 자료형은 포인터 (s1, s2)의 주소를 받아야 하기 때문에 char **형으로 정의한다. (포인터를 가리키는 포인터)

 

문자열 길이 

  • 컴퓨터는 배열에 저장된 문자열의 경우, 널 문자까지만 문자열로 인식 > 배열 전체를 문자열로 사용하지 않음 
  • 문자열의 첫문자부터 널문자까지 선형검색해서 문자열의 길이를 구하는 알고리즘을 사용해야 한다. 
#include <stdio.h>

//버전 1
int str_len (const char *s) {
  int len = 0;
  
  while(s[len])  //널문자를 만나면 s[len]가 0이 되어 문자열 스캔을 중단한다.
     len++;
  return len;
}

//버전 2
int str_len(const char *s) {
   int len = 0;
   while(*s++)
      len++;
   return len;
}

//버전 3
int str_len(const char *s) {
   const char *p = s;
   while(*s) 
      s++;
   return s-p;
}

int main(void) {
   char str[256];
   printf("문자열 : ");
   scanf("%s", str);
   
   printf("이 문자열의 길이는 %d입니다.\n", str_len(str));
   
   return 0;
}

 

strlen 함수 

헤더 #include <string.h>
형식 size_t strlen(const char *s);
해설 s가 가리키는 문자열의 길이를 구한다.
반환값 구한 문자열의 길이를 반환한다. 

 

문자열에서 널문자가 아닌 문자 검색하기 

  • 같은 문자가 여러 개 있는 경우 > 가장 앞쪽에 있는 문자 인덱스를 반환한다. 
#include <stdio.h>

int str_chr (const char *s, int c) {  //문자열 s에서 문자 c 선형검색하기 
   int i = 0;
   c = (char)c;
   
   while(s[i] != c) {
     if(s[i] == '\0') //검색 실패 
        return -1;
     i++;
   }
   
   return i; //검색 성공 > 찾은 문자의 인덱스 반환 
}

int main(void) {
   char str[64]; //이 문자열에서 검색
   char tmp[64];
   int ch;       //검색할 문자
   int idx;
   
   printf("문자열 : ");
   scanf("%s", str);
   
   printf("검색할 문자 : ");
   scanf("%s", tmp); //먼저 문자열로 검색할 문자를 읽어 들인다.
   ch = tmp[0];      //첫번째 문자를 검색할 문자로 지정한다. 
   
   if((idx = str_chr(str, ch)) == -1) //처음 나오는 문자 검색
      printf("문자 '%c'은(는) 문자열에 없습니다.\n", ch);
   else
      printf("문자 '%c'은(는) %d번째에 있습니다.\n", ch, idx+1);
      
   return 0;
}

 

strchr, strrchr 함수 : 문자열 안에 들어있는 문자를 검색하는 함수 

  • 표준 라이브러리 함수에서 문자를 주고받을 때는 char형이 아니라 int형으로 주고받는다. 
strchr 함수
헤더 #include <string.h>
형식 char *strchr(const char *s, int c);
해설 s가 가리키는 문자열에서 가장 앞쪽에 있는 c를 찾는다. 
이때 c는 널문자여도 된다.
반환값 찾은 문자에 대한 포인터를 반환한다. > 가장 앞쪽의 문자 검색
문자가 없으면 널 포인터를 반환한다.
strrchr 함수
헤더 #include <string.h>
형식 char *strrchr(const char *s, int c);
해설 s가 가리키는 문자열 가운데 가장 뒤쪽에 있는 c를 찾는다. 
이때 c는 널문자여도 된다.
반환값 찾은 문자에 대한 포인터를 반환한다. > 검색할 문자가 문자열 안에 여러 개 있는 경우, 가장 뒤쪽 문자 검색 
문자가 없으면 널 포인터를 반환한다. 

 

strcmp 함수 : 문자열 비교 

  • 문자 코드 체계에 따라 문자열을 비교하므로 반환하는 값은 컴퓨터 환경마다 다를 수 있다. 
stcmp 함수
헤더  #include <string.h>
형식 int strcmp(const char *s1, const char *s2);
해설 s1, s2가 가리키는 문자열의 대소 관계를 비교한다. 
처음부터 순서대로 한 문자씩 unsigned char 형 값으로 비교한다.
반환값 문자열이 같으면 : 0
s1이 s2보다 크면 : 양의 정수
s1이 s2보다 작으면 : 음의 정수 
#include <stdio.h>

int str_cmp (const char *s1, const char *s2) {
   while(*s1 == *s2) {
      if(*s1 == '\0') //같음
         return 0;
      s1++;
      s2++;
   }
   
   return (unsigned char)*s1 - (unsigned char) *s2;
}

int main(void) {
   char st[128];
   puts("\"ABCD\"와 비교합니다.");
   puts("\"XXXX\"면 종료합니다.");
   
   while(1) {
      printf("문자열 st : ");
      scanf("%s", st);
      if(str_cmp("XXXX", st) == 0)
         break;
      printf("str_cmp(\"ABCD\", st) = %d\n", str_cmp("ABCD", st));
   }
   
   return 0;
}

 

strncmp 함수

  • 3번째 인수로 지정한 문자열의 길이만큼만 비교할 수 있다. 
  • 널문자가 없는 '문자 배열' 간의 비교도 가능하다. 
strncmp 함수  
헤더 #include <string.h>
형식 int strncmp(const char *s1, const char *s2 size_t n);
해설 s1, s2가 가리키는 문자 배열에서 n번째 문자까지의 대소 관계 비교
처음부터 순서대로 한 문자씩 unsigned char형 값으로 비교한다.
널 문자 이후의 비교는 하지 않는다.
반환값 문자 배열이 같으면 : 0
s1이 s2보다 크면 : 양의 정수 값
s1이 s2보다 작으면 : 음의 정수 값
#include <stdio.h>
#include <string.h>

int main(void) {
 char st[128];
 puts("\"STRING\"의 처음 3개의 문자와 비교합니다.");
 puts("\"XXXX\"를 입력하면 종료합니다.");
 while(1) {
    printf("문자열 st : ");
    scanf("%s", st);
    if(strncmp("XXXX", st, 3) == 0)
       break;
    printf("strncmp("\STRING\", st, 3) = %d\n", strncmp("STRING", st, 3));
 }
 
 return 0;
}

 

08-2 부루트-포스법 (brute force method)

 

문자열 검색

  • 어떤 문자열 안에 다른 문자열이 들어 있는지 조사하고 들어 있다면 그 위치 찾아내기 
  • 패턴 (pattern) : 검색할 문자열
  • 텍스트 (text) : 문자열 원본 

 

브루트 포스법 

  • 선형 검색을 확장한 알고리즘 
  • 첫번째 요소부터 비교해서 뒤로 한칸씩 옮겨가며 검색한다. 
  • 하지만 이미 검사를 진행한 위치를 기억하지 못한다. 
#include <stdio.h>

int bf_match (const char txt[], const char pat[]) {
   int pt = 0;
   int pp = 0;
   while(txt[pt] != '\0' && pat[pp] != '\0') {
     if(txt[pt] == pat[pp]) {
        pt++;
        pp++;
     else {
        pt = pt - pp + 1;
        pp = 0;
     }
   }
   
   if(pat[pp] == '\0') 
      return pt - pp;
   return -1;
}

int main(void) {
   int idx;
   char s1[256];  //텍스트
   char s2[256];  //패턴
   puts("브루트-포스법");
   printf("텍스트 : ");
   scanf("%s", s1);
   
   printf("패턴 : ");
   scanf("%s", s2);
   idx = bf_match(s1, s2);
   
   if(idx == -1)
      puts("텍스트에 패턴이 없습니다.");
   else
      printf("%d번째 문자부터 match합니다.\n", idx+1);
   
   return 0;
}

bf_match 함수 

  • 텍스트에서 패턴을 검색 > 텍스트의 위치(인덱스) 반환 
  • 텍스트에 패턴이 여러 개 있는 경우 > 가장 앞쪽에 위치한 텍스트의 인덱스 반환, 검색에 실패하면 -1 반환 
  • 텍스트를 스캔하기 위한 변수로 pt 사용, 패턴을 스캔하기 위해 pp 사용 
  • 두 변수는 처음에 0으로 초기화하고 스캔하거나 패턴을 옮길 때마다 업데이트한다. 

 

08-3 KMP법

 

KMP법

  • 다른 문자를 만나면 패턴에서 문자를 검사했던 위치 결과를 버리고, 다음 텍스트의 위치로 1칸 이동한 다음 다시 패턴의 첫번째 문자부터 검색한다. 
  • 중간 검사 결과를 효율적으로 사용하는 알고리즘이다. 
  • 이미 검사를 마친 부분의 다음문자부터 패턴과 비교하여 일치하는지 검사한다. 
  • 하지만 몇번째 문자부터 다시 검색을 시작할지 패턴을 이동시킬 때마다 다시 계산해야 하는 단점이 있다. > 표로 만들어서 문제 해결 
  • 표를 작성할 때는 패턴 안에서 중복되는 문자의 나열을 먼저 찾아야 한다. > KMP법 사용
int kmp_match(const char txt[], const char pat[]) {
   int pt = 1;
   int pp = 0;
   int skip[1024]; 
   
   skip[pt] = 0;
   while(pat[pt] != '\0') {  //표 만들기
     if(pat[pt] == pat[pp]) 
        skip[++pt] = ++pp;
     else if(pp == 0)
        skip[++pt] == pp;
     else 
        pp = skip[pp];
    }
    
    pt = pp = 0;
    //텍스트를 스캔하는 커서 pt는 다시 뒤로 돌아오지 않는다. 
    while(txt[pt] != '\0' && pat[pp] != '\0') { //검색 수행 
       if(txt[pt] == pat[pp]) {
          pt++; pp++;
       } else if(pp == 0)
          pt++;
       else 
          pp = skip[pp];
    }
    
    if(pat[pp] == '\0')
       return pt - pp;
       
    return -1;
}

 

08-4 Boyer-Moore 법 

 

Boyer-Moore 법

  • 패턴의 마지막 문자부터 앞쪽으로 검사를 진행하면서 일치하지 않는 문자가 있으면 미리 준비한 표에 따라 패턴을 옮길 크기를 정한다. 
  • 텍스트와 패턴의 첫번째 문자를 위, 아래로 겹치고 패턴의 마지막 문잘ㄹ 검사한다. 
  • 텍스트 안에서 패턴이 들어있지 않은 문자를 찾으면 해당 위치까지의 문자는 건너뛸 수 있다. 
    패턴 길이가 n이라면, 현재 검사하고 있는 텍스트의 문자 위치로부터 '다음에 검사할 패턴의 마지막 문자 위치'가 n만큼 떨어질 수 있도록 패턴을 옮기면 된다. 
  • KMP법과 동일하게 각각의 문자를 만났을 때 패턴을 옮길 크기를 저장할 표(건너뛰기 표)를 미리 만들어야 한다. 

 

패턴 문자열의 길이가 n일 때 옮길 크기 결정하는 방법 

1. 패턴이 들어 있지 않은 문자를 만난 경우

  • 패턴을 옮길 크기 (n)만큼 옮긴다. 

2. 패턴에 들어 있는 문자를 만난 경우 

  • 마지막에 나오는 위치의 인덱스가 k이면, 패턴을 옮길 크기는 n-k-1이다. 
  • 같은 문자가 패턴 안에 중복해서 들어있지 않다면, 패턴을 옮길 크기는 n이 된다. 

 

건너뛰기 표의 요소 개수 : UCHAR_MAX + 1 
패턴에 존재할 수 있는 모든 문자의 옮길 크기를 계산하고 저장해야 하기 때문 

 

#include <stdio.h>
#include <string.h>
#include <limits.h>

int bm_match(const char txt[], const char pat[]) {
  int pt; 
  int pp;
  int txt_len = strlen(txt);
  int pat_len = strlen(pat);
  int skip[UCHAR_MAX+1];
  
  for(pt = 0; pt<=UCHAR_MAX; pt++)  //건너뛰기 표 만들기
     skip[pt] = pat_len;
  for(pt = 0; pt<pat_len - 1; pt++)
     skip[pat[pt]] = pat_len - pt - 1;
     
  while(pt < txt_len) {
     pp = pat_len - 1;
     while(txt[pt] == pat[pp]) {
        if(pp == 0) 
           return pt;
        pt--; pp--;
     }
     pt+= (skip[txt[pt]] > pat_len - pp) ? skip[txt[ppt]] : pat_len - pp;
  }
  return -1;
}

int main(void) {
  int idx;
  char s1[256];
  char s2[256];
  
  puts("Boyer-Moore법");
  printf("텍스트 : ");
  scanf("%s", s1);
  
  printf("패턴 : "):
  scanf("%s", s2);
  
  idx = bm_match(s1, s2);
  
  if(idx == -1)
     puts("텍스트에 패턴이 없습니다.");
  else 
     printf("%d번째 문자부터 match합니다.\n", idx+1);
 
  return 0;
}

 

strstr 함수 : 문자열 검색하는 표준 라이브러리 

헤더 #include <string.h>
형식 char *strstr(const char *s1, const char *s2);
해설 s1이 가리키는 문자열에서 s2가 가리키는 문자열과 일치하는 (널 문자를 포함하지 않는) 문자열을 찾는다.
가장 앞쪽에 나오는 문자열을 찾는다.
반환값 찾아낸 문자열에 대한 포인터 (첫번째 문자에 대한 포인터) 반환
찾지 못하면 > 널 포인터 반환
s2길이가 0인 문자열이면, s1을 반환한다. 
#include <stdio.h>
#include <string.h>

int main(void) {
   char s1[256], s2[256];
   char *p;
   
   puts("strstr 함수");
   printf("텍스트 : ");
   scanf("%s", s1);
   printf("패턴 : ");
   scanf("%s", s2);
   
   p = strstr(s1, s2);
   
   if(p == NULL)
      printf("텍스트에 패턴이 없습니다.\n");
   else {
      int ofs = p - s1;
      printf("\n%s\n", s1);
      printf("%*s|\n", ofs, "");
      printf("%*s%s\n", ofs, "", s2);
   }
   
   return 0;
}

 

문자열 검색 알고리즘의 시간복잡도와 실용성 

부르트-포스법

  • 시간복잡도 : O(mn), O(n)
  • 단순 알고리즘이지만 실제는 아주 빠르게 동작한다. 

KMP법

  • 시간복잡도 : O(n) 
    * 처리가 복잡하고 패턴 안에 반복이 없으면 효율이 좋지 않다. 
  • 검색 과정에서 검사하는 위치를 앞으로 되돌릴 필요가 없어서 순서 파일을 읽어 들이며 검색할 때 많이 사용한다. 

Boyer-Moore법

  • 시간 복잡도 : O(n), 평균 : O(n/m) 
  • 2개의 배열을 사용하면 KMP법과 마찬가지로 배열을 만드는데 복잡한 처리가 필요해 효과가 떨어진다. 
  • 1개의 배열을 사용하는 방법으로 간단하게 구현한 Boyer-Moore법을 사용해도 충분히 빠르다. 

* 가장 실용적인 문자열 검색 알고리즘 : Boyer-Moore법 (경우에 따라 브루트-포스법을 사용하기도 한다.)

'Algorithm' 카테고리의 다른 글

10. 트리  (0) 2023.06.08
09. 리스트  (0) 2023.06.05
[미해결] 2023 인하대학교 프로그래밍 경진대회(IUPC) (A-모비스)  (0) 2023.05.21
[백준] 25305(커트라인) c언어  (0) 2023.05.21
[백준] 2501(약수 구하기) c언어  (0) 2023.05.21