본문 바로가기
지난 학기들의 기록/C 기초

[C실습] 문자열 유형 분석 2 - 포인터배열을 사용한 strtok구현하기(문자열에서 단어 분리하기)

by 아메리카노와떡볶이 2020. 10. 6.
728x90
포인터배열을 사용한 strtok함수 구현하기

문자열 string.h 헤더파일에 내장되어있는 strtok함수는 string token 즉 문자열을 단어로 쪼개주는 함수입니다. 이번 포스트에서는 strtok함수를 C언어로 구현해보며 이해하고 최종적으로 strtok함수를 사용하는것까지 공부해보겠습니다.

 

1. strtok함수를 c언어로 구현하기

먼저 구현해볼 방법은 string문자열을 입력받아서 다른 2중배열을 선언하고 단어별로 쪼개서 2중배열에 저장하는 것입니다.

 

아이디 생각하기

먼저 단어를 구분짓는다는 것은 공백입니다. 따라서 공백을 기점으로 단어를 분리하는 것이 핵심 아이디어 일 것입니다.

예시를 들어 "simple is best"라는 문자열을 생각해보면 공백이 등장할때마다 단어가 끝나는것을 알수있습니다. 그리고 마지막 best의 경우에는 문자열의 끝인 NULL을 생각해주면 될 것입니다.

따라서 문자열의 값중 " "과 NULL을 만날때마다 단어가 완성된다고 생각하면 됩니다.

 

또한 문자열에서 단어를 추출하기 위해 start index와 end index를 나타내는 변수를 지정하고

space나 NULL을 만났을때 end 포인트와 start포인트를 재지정하는 작업을 반복하면서

2중배열에 문자열을 저장해주면 됩니다.

공백과 NULL 이 핵심이므로 이것을 떠올릴 수 있다면 어렵지않게 아래의 코드를 이해할 수 있을것입니다. 

 

코드로 구현하기

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#pragma warning(disable:4996)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main() {
    char sentence[1000],words[10][100];
    char* ps = sentence,temp[100];//문자열 sentence를 가리키는 포인터변수 선언
    gets(sentence);//문자열을 입력받습니다
    int sen_len = strlen(sentence), i = 0;//문자열의 길이를 구합니다
    int start = 0end = 0, word_idx = 0,t,k;
    
    //start와 end변수를 통해서 단어를 분리합니다.
    for (ps = sentence, i = 0; ps <= sentence + sen_len; i++, ps++) {
        if (i != 0) {
            if (*ps == ' ' || *(ps) == NULL) {//공백이나 문자열의 끝을 만날경우
                end = i;// 현재의 인덱스를 end지점으로 설정하고
                for (t = start, k = 0; t < end; k++, t++) {//start부터 end까지를
                    words[word_idx][k] = sentence[t];//문자 words[word_idx]에 저장합니다.
                }
                words[word_idx][k] = NULL;//문자열의 끝을 NULL처리 해주고
                word_idx++;//문자words의 개수를 증가시킵니다.
                start = end + 1;//start지점은 end +1로 재지정됩니다.
            }
        }
    }
    //분리된 단어를 출력합니다
    for (int j = 0; j < word_idx; j++) {
        puts(words[j]);
    }
    //단어들을 알파벳순으로 정렬합니다.
    for (int i = 0; i < word_idx; i++) {
 
        for (int j = 0; j < word_idx - i - 1; j++) {
//사전순정렬
            if ((strcmp(words[j], words[j + 1]) > 0&& words[j][0!= words[j + 1][0]) {
 
                strcpy(temp, words[j]);//temp에 words[j]를 담고
                strcpy(words[j], words[j + 1]);//wodrs[j]에 j+1을 담고
                strcpy(words[j + 1], temp);//j+1에 temp를 담으면서 swap해줍니다.
            }
        }
    }
    //정렬한 단어들을 출력합니다.
    for (int j = 0; j < word_idx; j++) {
        puts(words[j]);
    }
    return 0;
}
cs

 

작성한 코드에서 아래부분이 단어를 분리하는 핵심코드입니다.

' ' 와 null을 만나는 지점에서 end포인트를 지정해주게 됩니다.

그리고 start부터 end지점까지 문자열 sentence의 원소들을 words배열에 옮겨담습니다. 이 한번의 반복이 단어하나를 추출해내는 과정입니다.

그리고 그 과정이 끝나면 start지점을 end +1로 재지정합니다. end 포인트는 공백에 해당하기때문에 그 다음 start는 공백보다 한칸 앞선 start=end+1 이 되는 것입니다.

 

 

그리고 저의 코드를 보면 알수있듯이 strcpy 함수를 활용해서 분리된 단어들을 사전순 정렬하는 작업까지 진행했는데,

여기서 좀 더 내용을 심화할수있습니다.

 

2. 단어를 분리하라, 단 포인터배열을 사용해서 저장하라

1번 내용을 심화하기 위해 하나의 조건이 붙었습니다. strtok의 구현에서, 포인터배열을 사용하라는 것입니다.

위의 코드를 포인터배열을 사용해서 구현하게 되면 2가지의 장점이 생깁니다.

 

첫번째는 추가적인 배열을 temp를 사용하지 않아도 됩니다.

사전순 정렬에서 swap을 할때 문자열의 값을 교환하기때문에 빈 공간인 temp배열이 필요하게 됩니다. 포인터 배열을 사용하게 될 경우, 주소를 교환해주면 되므로 temp배열 메모리를 아낄수 있습니다.

 

두번째는 코드가 더 짧고 효율적이게 됩니다.

앞서 1번에서 start와 end변수를 통해서 계속 문자열의 start 인덱스와 end인덱스를 재지정하면서 단어를 분리했었습니다. 하지만 포인터배열을 이용한다면, 단순히 포인터가 단어의 시작점을 가리키게 하면 끝입니다. 따라서 코드가 더 간략해집니다.

 

이와 같은 이유로 포인터배열을 사용해서 구현해보겠습니다.

 

아이디어 생각하기

혹시 몰라서 포인터 배열에 대해서 잠깐 언급을 하자면, 말 그대로 포인터를 여러개 선언한 것입니다. 배열의 개념을 이해하고 있다면 아주 쉽습니다. 

제가 작성한 코드에서 발췌해온 것인데, 여기서 char *words[100]이라는 표현이 어색할수있습니다. 하지만 쉽게 생각하면 char *words 라는 포인터가 *words[0] 부터 *words[99]까지 100개 존재하는 것입니다.

각각은 기존에 이해하고 있던 문자형 포인터와 다른것이 없습니다.

 

이러한 100개의 포인터를 이용해서 단어를 분리하기 위해서는,

단어가 시작되는 지점마다 한개의 포인터를 이용해서 그 시작점을 가리키게 하면 각각의 포인터들은 단어를 가리키게 되는 것입니다. 

이 개념을 그대로 코드로 구현해보겠습니다.

 

포인터배열을 이용한 단어분리하기

위의 1번 코드와 비교했을때 다른것이 느껴지시나요?

이 작업을 단순히

words[word_idx] = &sentence[start]로 대체할수있게 되는 것입니다. 문자열의 단어가 시작되는 주소 를 가리키면요 !

 

여기서 주의해야할 점은 단어가 끝나는 공백지점을 NULL ( = \n)으로 바꾸어주어야한다는 것입니다. 문자열의 끝을 인식시켜주기 위함입니다.

 

코드 구현하기

위의 사항을 바탕으로 코드를 구현해보겠습니다

공부하는 과정이므로 이번에는 string함수들도 다 직접 구현해서 써보았습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#pragma warning(disable:4996)
#include <stdio.h>
#include <stdlib.h>
int str_compare(char* arr1, char* arr2);
int return_len(char* a);
int main() {
    char sentence[1001], *words[100];
    char* ps = sentence;//문자열 sentence를 가리키는 포인터변수 선언
    gets(sentence);//문자열을 입력받습니다
    int sen_len = return_len(sentence), i = 0;//문자열의 길이를 구합니다
    int start = 0end = 0, word_idx = 0, t, K, k;
    char* temp = NULL;
    scanf("%d"&K);
    int j = 0;
    //start와 end변수를 통해서 단어를 분리합니다.
    for (ps = sentence, i = 0; ps <= sentence + sen_len; i++, ps++) {
        if (i != 0) {
            if (*ps == ' ' || *(ps) == NULL) {
                end = i;// 현재의 인덱스를 end지점으로 설정하고
                sentence[end= NULL;
                words[word_idx] = &sentence[start];
                word_idx++;
                start = end + 1;//start지점은 end +1로 재지정됩니다.
            }
        }
    }
    for (int i = 0; i < word_idx; i++) puts(words[i]);
    //단어들을 길이순으로 정렬합니다. 단 길이가 같은경우 사전순정렬입니다.
    for (int i = 0; i < word_idx; i++) {
 
        for (int j = 0; j < word_idx - i - 1; j++) {
            if (return_len(words[j]) > return_len(words[j + 1])) {
                printf("\n변환전 %s %s\n", words[j], words[j + 1]);
                temp = words[j];
                words[j] = words[j + 1];
                words[j + 1= temp;
                printf("\n변환후 %s %s\n", words[j], words[j + 1]);
            }
            else if (return_len(words[j]) == return_len(words[j + 1])) {
                if ((str_compare(words[j], words[j + 1]) > 0)) 
                    temp = words[j];
                    words[j] = words[j + 1];
                    words[j + 1= temp;
                }
 
            }
        }
    }
 
    //정렬한 단어중 K번째 단어를 출력합니다.
    for (int i = 0; i < word_idx; i++) puts(words[i]);
    return 0;
}
int return_len(char* a) {
    char* p = a, len = 0;
    for (p = a; *!= NULL; p++) len++;
    return len;
}
 
int str_compare(char* pstr1, char* pstr2) {
    int i = 0;
    while (pstr1[i] != '\0' || pstr2[i] != '\0') {
        if (pstr1[i] > pstr2[i]) {
            return pstr1[i] - pstr2[i];
        }
        else if (pstr1[i] < pstr2[i])
            return pstr1[i] - pstr2[i];
        i++;
    }
    return 0;
}
cs

정렬방식에서도 조금 더 복잡하게 길이순 정렬에 길이가 같을때는 사전순 정렬이라는 조건을 추가했습니다.

코드에서 보면 알수있다싶이, 추가적인 temp배열없이 temp포인터만으로 swap이 가능함을 알수있습니다.

 

느낀점 :

배열을 최소한으로 사용해서 strtok함수를 구현하고 단어들을 정렬하고싶다면 포인터배열을 사용하자!

728x90

댓글