나는 C/C++/C#/Java/Python을 다 맛보고 특히 Python을 깊게 맛봤지만
문자열 처리만큼은 Python이 최고라고 생각했다.
하지만 언어는 찰흙과 같아서 자기가 만드는 대로 만들어진다.
좀 더 잘 뭉개지는 찰흙이라 쉽게 만들어지느냐
딱딱해서 만들기 어렵냐 차이지.. 결국 뭐든 만들 수 있다.
그래서 이번엔 C++에서도 문자열 처리가 가능한데..
어떤 식으로 해야 유용한가를 탐구해보려고 한다.
알아보자
기본적으로 유용한 라이브러리(정의된)이 많다.
가장 대표적인 2가지
#include <regex> // 정규표현식 -> 필터링할 때 무지 많이 쓰인다.
#include <string> // 문자열을 다루려면 거의 필수
정규표현식과 문자열 라이브러리다.
우선 string 클래스부터 알아보자.
우선 문자열 클래스를 다루기 위해서 어떻게 객체를 생성하나?를 알아보자
생성자는 총 7가지가 있지만
편히 쓸 수 있는 것만 생각해보자
string a("why what where"); // x 객체를 만들어 해당 문자열로 초기화함.
string b(20, 'A'); // 해당 char로 20개 넣어서 string 객체 초기화
string c(a); // 복사 생성자 -> 유용하면서도 다르게 쓰임
string d ; // 크기가 0인 디폴트 생성자 -> 딱히 쓰지 않음 string s = "" <- 개인적으로 이게 더 명확함.. 개인적으로그래
//////////// C++ 스타일 같은 생성자
char alls[] = "abcdefghijklmnopqrstuvwxyz";
string d(alls, alls+ 10); // iterator의 사용 -> 포인터 역할(배열의 이름은 해당 배열의 처음 주소를 가지고 있다)
string e(&alls[0], &alls[10]); // 참조의 사용 -> 포인터 역할(객체의 값이 담긴 주소를 알면 위와 같은 원리로 작동)
하지만 위의 생성자들을 사용하기보다는.. 정말 디폴트 생성자만 놓고
입력을 받는 형태가 훨씬 많다.
나같은 경우는
string s = "";
하고 시작한다.
즉, string 클래스에서 입력을 받는 경우가 대부분이라 생성자는 알고는 있지만
코딩테스트에서는 거의 쓰이지 않는다.
입력을 알아보자
char info[100];
//C 스타일 입력방법 3가지
cin >> info; // info에 한 단어를 읽는다. -> 어절단위로 끊어진다.(많이씀!!)
cin.getline(info, 100); // info에 한 줄을 읽는다. -> 띄어쓰기 무시 단, \n 무시 x
cin.get(info, 100); // 위와 같지만 맨 끝에 "\n" 공백도 취급함.
//일반적
string s;
cin >> s; // -> 한 어절 읽기
getline(cin, s); // -> 한 줄 읽기, \n 무시
// 정말 가끔씩 유용함
getline(s, ':'); // char가 있는 곳 까지 읽고 char는 포함하지 않음
//생각해보면 csv 파일을 이렇게 입력받을 수도 있다는 생각이든다.
근데 우리는 문자열을 할 때 어떤 배열에다 넣을지 생각하지 않는다.
왜냐하면 string 클래스의 객체는 크기 조절을 자동으로 해주기 때문이다.
그래서 우리는 string 클래스로 문자열 처리를 하는 것이 매우 편하고 좋다.
**일반적으로 string 객체의 최대 허용크기는 npos에 저장되어 있는데
그것은 보통 usigned int의 최대크기다. (42....)
나는 2가지 만 쓴다.
string s;
cin >> s;//만 쓴다... 왜냐하면 만약 띄어쓰기가 있다면 거의 모든 문제는 cin >> x >> y 로 받으면 된다.
//다만 예외는 정말 우리가 쓰는 언어가 나올 때, "you are my baby" 같은 것이 나오면
getline(cin, s);//를 써야 정말 잘 받을 수 있겠다.
입력을 받았으면 Processing을 해야 한다.
대표적으로 많이 쓰이는 함수가 몇 가지 있다.
1. 문자열 접근하기 ***** 맨날 씀
먼저 배열처럼 접근할 수 있는데
string s = "abc"라면 s[0] 는 'a'가 되고 Type은 char이다.
하나씩 꺼내서 비교같은 것을 할 때 주의하자.
같은 방법이지만 .at()을 이용하는 것인데
s.at[0] 은 'a'다. 여전히 타입은 char이다.
물론 문자열의 길이를 알면 맨 마지막 문자에 접근할 수 있지만
파이썬과 같이 [-1] 연산이 되지 않는다.
때문에 맨 마지막 문자를 접근하는 방법을 알아놓는 것도 좋다.
s.back()을 하면 'c'가 나온다. 여전히 타입은 char이다.
2. 문자열의 길이(크기) ***** 맨날 씀
크기나 길이를 혼용해서 쓰듯이 여기서도 혼용한다.
string s = "abc";
s.size();
s.length();
둘다 3을 뱉는다.
3. 문자열 검색(***) -> 은근히 쓰이면서 안쓰임
find() 함수를 제공하는데 여러가지 형태가 있다.
요약하자면
string s = "abcdefg";
s.find("bcd");
해당 문자열이 있는 index를 반환하는데
위의 예에서는 1을 반환하겠다.
bcd가 1번부터 시작하니까 그렇다.
**만약 존재하지않으면 npos를 return 한다.
일반적으로 정의되어 있다.
static const size_t npos = -1;
4. 문자열 순서 비교 *** -> 은근히 나옴 어려워짐
사전순으로 -> 사실 아스키코드 순이라고 보면 된다.
"CAP"과 "CAT"을 비교하면
string str1 = "CAP";
string str2 = "CAT";
//참고로 o p q r s t u ... p가 먼저 출현
cout << (str1 < str2); // 앞서면 아스키코드 상으로 작은 값을 가짐
//-> 순서대로 나온다? -> 아스키코드 값이 작다.
// 나중에 나온 값이 더 크다. true return
5. 문자열 비우기 ** -> for문에서 쓰고 초기화할 때 많이씀.
str.clear(); // str = "" 으로 만듬 (단, capacity 값은 그대로 유지)
6. 문자열 <-> 숫자 ***** -> 맨날 나옴/ string to, to string 을 기억하면 된다.
숫자를 문자열로
int a = 12123;
string s;
s = to_string(a); // int에 들어있는 값이 문자로 저장
** int 뿐만이 아니다. to_string에 들어가면 다 문자열이 되어버린다.
다만 문자열에서 숫자로 바꿀 때는
숫자의 타입이 많기떄문에 조금 다르다.
String TO (Int)(Double)(Float)(Long) -> 앞글자만 딴다
string str_a ="123";
string str_b ="1.23";
string str_c ="2.34";
string str_d = "31212121212";
int after_a = stoi(str_a);
double after_b = stod(str_b);
float after_c = stof(str_c);
long int after_d = stof(str_d);
7. 문자열 슬라이싱 **** -> 적당히 나옴
파이썬에선 [:3]으로 간단하지만
C++에선 조금 더 길다.
string s = "ABCD";
s.substr(3); //문자열의 3번부터 끝까지 string 형태로 반환
s.substr(0,3); //문자열의 0번부터 3의 크기만큼 string 형태로 반환
// D 반환
// ABC 반환
8. 문자열 추가(결합)
일반적으로 문자열 추가(결합)이 있다.
+= 연산자나 Append() 함수로 하나의 문자열을 다른 문자열에 덧붙이기 한다.
** 최대 문자열 허용을 넘어서면 length error를 throw 한다.
string s = "I love";
s += "you";
s append(",too");
cout << s;
//"I love you, too
-> 둘다 문자열 뒤에 붙인다는 특징
즉, s = s + (붙이려는 문자열)
그래서 앞에다 추가하고 싶을 때는
*** 직접 s = (앞 추가) + s로 해준다 -> 은근 많이 쓰임
9.문자열 삽입,삭제( 변경과 느낌만 다름)
push_back() 하고 pop_back()이 있는데
다른 것과 마찬가지다.
**push_back(char c) 문자열을 push하는 것은 아니다.
원리는 다른 것과 같다.
다음으로는 insert, erase가 있다.
역시 원리는 다른 것과 같다.
해당 iterator를 받아서 해당 위치에 삽입하거나 위치 범위 안의 것들을 삭제한다.
**이것은 다른 것과 같아 설명하지 않는다.
10. 복사. 치환 **
copy() 메서드는 iterator를 이용해서 다른 곳에서도 많이 쓰인다.
swap()은 서로 값을 바꾼다고 생각하면 된다.
size_type copy(charT* s, size_type len, size_type pos = 0) const;
s는 copy할 대상을 의미하고
n은 복사할 문자 수
pos는 복사를 시작할 위치다.
void swap(basic_string& str);
예를 들면
string s = "ABCDE";
string s1 ="";
string s2 ="xx";
s1.copy(s, 3, 1);
s2.swap(s1);
cout << s1 << "\n";
cout << s2;
// xx
// BCD
우리가 거의 쓸만한 것은 다 썼다 나머지는 프로그래머의 역량이다.
이제는 정규표현식을 배워보려고 한다.
문자열 처리에서 가장 간편하게 작업이 가능한 정규표현식이다.
여기서는 3가지만 알아본다.(가장 많이 쓰임)
1. regex_match 전체 문자열 패턴 매칭하기
2. regex_search 문자열 검색하기 ****
3. regex_replace 문자열 치환하기 ****
카카오 문제를 C++로 푼다면 거의 regex로 풀면 간단해지는 문제가 참 많다.
왜냐하면.. 파이썬은 정규표현식 쓰기 가장 좋다고 생각하거든.. 내가 개인적으로
우선 정규표현식의 객체라는 것이 무엇인지 생각해봐야 한다.
내가 글을 써놓은 것이 있다.
[프로그래밍언어(Programming Language)/파이썬(python)] - Regular expression,정규표현식, 문자열파싱,처리 [Python]
결국 메타 문자로 표현하는 방법만 익히면 다 된다.
**하지만 맨날 까먹어서 봐야한다. ㅠ
핸드폰 번호가 가장 간단한데
000-0000-0000이란 패턴을 항상 가지고 있다.
즉, 해당 문자열들 중에서 전화번호를 알고 싶으면
아래처럼 정규표현식 객체를 만든다.
(내가 어떤 패턴을 이용할지 만든다고 생각하면 된다)
regex re("\d3-\d4-\d4"); //해당 패턴을 객체로 만듬
숫자 3자리 - 숫자4자리 - 숫자4자리를 말한다.
re만 만들면 50% 준비 끝이다.
1. regex_match(비교해볼 문자열)
-> 전화번호 형식이면 true 반환 아니면 false;
만약 해당 부분을 뽑아내고 싶다면 조금 작업이 더 필요하다.
smatch match; // 매치된 결과를 string 으로 넣어줄 것
캡쳐 그룹 (capture group)을 사용해야 한다.
그렇다면 위의 표현식이 좀 바뀌어야 한다.
끝번호만 가지고 싶다?
regex re("\d3-\d4-(\d4)"); //해당 패턴을 객체로 만듬
그리고 매개변수도 추가된다.
regex_match(string, match, re)
즉, 해당되는 문자열에서 re와 매치가 된 것을 match에 저장하려고 한다.
for (const auto &number : phone_numbers) {
if (std::regex_match(number, match, re)) {
for (size_t i = 0; i < match.size(); i++) {
std::cout << match[i].str() << std::endl;
}
}
}
그렇게 match가 되면 match가 된 기존 문자열, 문자열 중 capture가 된 부분을 저장한다.
2. regex_search(비교할 문자열)
1번에서는 match로 전제 문자열이
우리가 만든 패턴에 부합하는가를 봤다면 이번엔
전체 문자열에서 우리가 만든 패턴의 문자열이 존재하는 가를 알아본다.
특히 html처럼 < ...> </...>을 이 많이 나와서 특정 헤더를 가진 것만 받고 싶어서 쓴다.
regex re(R"(<div class="sk[\w -]*">\w*</div>)");
smatch match;
while (regex_search(html, match, re)) {
cout << match.str() << '\n';
html = match.suffix();
}
정규 표현식과 매칭이 되는 패턴이 존재한다면 regex_search 가 true 를 리턴한다.
이렇게 되면 false가 나올 때까지
-> 즉, 해당 문자열에서 모조리 match되는 부분을 찾아서 match에 저장한다.
그것이 될 수 있는 이유는
match.suffix() 를 하면 std::sub_match 객체를 리턴하기 때문이다.
sub_match 는 단순히 어떠한 문자열의 시작과 끝을 가리키는 반복자 두 개를 가지고 있다고 보면 된다.
이 때 suffix 의 경우, 기준 문자열에서 검색된 패턴 바로 뒤 부터,
이전 문자열의 끝 까지에 해당하는 sub_match 객체를 리턴한다.
즉, 해당 match된 부분을 하고 기존 문자열을 suffix()만 업데이트를 하면 된다.
다시 while문으로 그 이후에서 매칭시키는 것이다.
3. regex_replace("비교할 문자열")
그리고 가장 유용하다고 생각하는 치환이다.
그나마 가장 간단하고 유용하다.
regex re(R"r(sk-circle(\d))r"); // 해당 패턴
smatch match; // 매치된 것을 담을
string modified_html = regex_replace(html, re, "$1-sk-circle"); // 해당 패턴이 match된 부분을 "치환할 문자열"로 바꾼다
//해당 패턴의 모든 matching된 부분을 바꾸는 것이다.
cout << modified_html;
문자열은 이것으로 간단하게 마치고 저기서 조금 더 유용하게 쓸 방법이 없는가 찾는 것은
나에게 달려있을 것이라 본다.
21.12.02 업데이트
문자열에서 특정 문자만 삭제하기
<string> 필요
// 1.공백제거
string s = "i love you";
s.erase(remove(s.begin(), s,end(), ' '), s.end());
cout << s << '\n';
//a제거
string sen = "an apple is on the apple's phone";
sen.erase(remove(sen.begin(), sen.end(), 'a'), sen.end());
cout << sen << '\n';
이것이 되는 이유
remove는 <algorithm> 헤더에 들어있다.
remove가 반환하는 값은 iterator이다.
remove를 시킨 다음 remove의 대상의 iterator를 반환한다.
(An iterator to the element that follows the last element not removed.)
remove 함수는 실제로 원소를 제거하는 것이 아니다. 그저 지워야 하는 원소들을 컨테이너 맨 뒤로 보내는 것이다.
뒤로 보냈으면 해당 공간은 남은 원소들을 앞으로 당겨온다.
예를 들어보자
string s = "abcdedd";
auto it = remove(s.begin(), s.end(), 'd');
//remove 가 반환한 it는 마지막 원소 다음을 가리키고 있다.
// "abce" + "ddd"(뒤로 보냄)
//여기서 'd' 를 가리킨다.
//s에서 d를 다 없애보자
//즉, 맨처음 나온 d부터 끝까지 다 없애버린다.
s.erase(remove(s.begin(),s.end(),'d'),s.end());
//결국 s는 "abce"가 된다.
다시 말하면 우리는 remove가 원소들을 뒤로 보냈으니
읽을 때 remove가 보낸만큼 덜 읽으면 될 것 같다.
또는 뒤에부터 2개를 삭제해주면 될 것 같다.
ex) 2개 보냈으면 기존의 문장에서 앞으로 당겨졌고 2개 덜 읽거나 삭제하면 되는 것 아닐까?
이 때 우리는 erase를 쓴다.
** erase는 iterator1 부터 iterator2 까지 모조리 원소를 없애는 작업을 한다.
문자열에서 특정 단어만 삭제하기
<string> 필요
...
int main()
{
string str("There are two needles in this haystack with needles.");
std::string target("needle");
while (1) {
unsigned int found = str.find(target);
if (found == string::npos) break;
int size = target.size();
auto it = str.begin();
str.erase(it + found, it + found + size);
}
cout << str << '\n';
return 0;
}
find와 erase를 이용한다.
find를 이용해서 target이 처음 시작되는 처음 위치를 알아내어
target의 길이만큼을 erase한다.
while문을 통해서 find의 결과가 npos -> 찾을 수 없을 때까지 수행한다.
해당 문자열에 있는 target과 matching 되는 단어 모조리 삭제.
결과
**사실 해당 단어를 삭제하는 것은 정규표현식으로도 충분히 가능하다.
(다만 정규표현식이 아니어도 가능하다는 것을 알려주고자.. 했다.)
특정단어 치환하기
<string>
위와 원리는 같고 여전히 정규표현식 쓰면 더 쉽지만
해보겠다.
...
int main(){
string s = "i love you";
string target = "you";
string replace_word = "me";
s.replace(s.find(target), target.size(), replace_word);
cout << s;
return 0;
}
위와 원리는 같다.
삭제하느냐 치환하느냐의 차이다.
''으로 치환한다면 비어질 것이다.
참고링크
'문제풀이(Problem Solving) > C++ 문제풀이에 유용한 것들' 카테고리의 다른 글
C++문법/ 비트 연산 및 활용(비트플래그) , <bitset> (0) | 2021.12.01 |
---|---|
C++에서 STL 함수 중 유용해 보이는 것들 (0) | 2021.11.25 |
MST, 최소 신장 트리 [CPP] (0) | 2021.07.06 |
각 유용한 함수들 [CPP] (0) | 2021.07.05 |
Hash, 해시 테이블 , 자료구조 [CPP] (0) | 2021.07.05 |