[백준] 1152번 : 단어의 개수 - JAVA [자바]
https://www.acmicpc.net/problem/1152
- 문제
단어의 개수를 물어보는 문제다.
다만 주의 할 것이 있다.
문자열의 앞과 뒤에는 공백이 있을 수도 있다는 것이다.
즉, 공백 단위로 처리할 때 그냥 공백의 개수대로 단어를 세면 예외가 발생할 수 있다는 것이다.
- 3가지 풀이방법을 제시한다.
먼저 흔히 입력으로 쓰는 Scanner 를 통해 풀어볼 것이다.
또한 Scanner 로 풀이한 것과 동일한 알고리즘을 사용하되 입력방식은 BufferedReader 로 바꾸어 풀어 볼 것이고, 마지막으로 가장 기본입력스트림인 바이트 스트림을 통해 (System.in.read()) 다른 알고리즘을 사용하여 풀이해보고자 한다.
- 알고리즘
알고리즘은 크게 두 가지 방법이 있다.
- 문자열을 읽고 charAt 을 통해 공백을 검사하는 방법
- StringTokenizer 을 사용하는 방법
이 중에서 우리는 가장 간편한 StringTokenizer 을 사용할 것이다.
StringTokenizer 을 사용하여 분리 기준을 공백으로 지정해주면 공백을 기준으로 나뉘어 토큰에 저장해주는 것이다.
단 StringTokenizer 을 사용하기 위해서는 java.util.StringTokenizer; 을 import 해주어야 한다.
그래서 문자열의 처음과 마지막의 공백에 대해 별다른 예외처리를 안해줘도 되니 매우 편리한 방법이다.
그럼 차근차근 하나씩 짜보자.
그리고 StringTokenizer 에 들어간 토큰, 즉 문자열의 개수를 세어주는 메소드를 출력하면 문제는 끝난다.
무슨 말인지 모르겠다면 아래 풀이 방법을 보자.
[split 함수 을 사용할 시 주의할 점]
split을 활용하여 쓸 경우 틀렸다고 나온다는 질문이 많아 이 부분에 대해 추가로 설명을 하겠다.
가장 쉽게 예외 케이스를 먼저 알려드리자면 공백만 입력받아보면 된다.
공백만 입력받았을 경우에 분명 단어의 개수는 0으로 출력은 0이 되어야 정답이지만, split 함수를 사용하여 출력해보면 1이 나올 것이다.
이는 String클래스의 split함수에 대한 이해와 빈 문자열이 무엇인지를 이해해야한다.
먼저, 다음과 같은 과정을 거친다고 가정해보자.
String s = scan.nextLine(); (BufferedReader의 readLine()도 가능)
그리고 공백(" ") 을 입력받는다.
그러면 s 는 공백(" ")을 담고 있는 문자열일 것이다.
그리고 보통 앞 뒤 공백을 없애기 위해 trim() 혹은 strip() 함수를 쓸 것이다.
그럼 s 변수는 무엇으로 바뀔까?
바로 '빈 문자열'이다.
예로들어 s = s.strip(); 을 하게 된다면, s는 ""(빈 문자열)로 바뀐다는 것이다. 이 부분이 중요한데, strip() 함수에 의해 공백 문자열(" ")이 빈 문자열("")로 대치된다는 것이지 null하고는 다르다.
즉, 빈 문자열("")과 null은 다른 것이라는 뜻이다.
예로들어
String a = "";
String b = null;
이렇게 두 개가 있다고 해보자.
위 두 문자열 변수의 차이를 보면, a는 빈 문자열이기는 하지만 실제 값(빈 값)을 갖는 인스턴스화 된 문자열이다. 쉽게 말해서 빈 문자열을 갖는 문자열이다.
반면에 b는 null로 할당 자체가 되지 않음을 의미한다. 즉, 인스턴스화 되지 않기에 참조하고있는 것 자체가 없다.
그러면 다시 돌아와서 strip() 혹은 trim()에 의해 공백이 빈 문자열로 대치 되었다면, 0의 길이를 갖고있는 String 변수라는 뜻이다.
이 부분을 간과하는 분들이 많아 아마 많이 틀리셨을 수도 있다.
그럼 여기까지 s = "" 이라는 것을 확인 할 수 있겠다.
그리고나서 split(" ") 을 쓰게 되면, 즉 s.splt(" "); 을 쓰게 되면 어떻게 될까?
String[] spl = s.split(" ");
split 함수를 뜯어보면 매칭되는 정규식이 없을 경우 자기 자신을 반환하게 되어있다.
if (off == 0)
return new String[]{this};
그럼 this는 무엇일까? 아까 trim() 혹은 strip()을 통해 공백을 제거한 String s는 빈 문자열 "" 을 갖는다고 했다.
즉, String s = "" 일 것이다.
결국 split(" ")을 썼지만, 공백(" ")에 매칭되는 문자열이 존재하지 않으므로 return new String[]{this} 이 실행 될 것이고, this는 자기 자신이 담고있던 빈 문자열("")일 것이다.
한 마디로 반환 된 String[] 배열은 index 0에 빈 문자열 하나가 반환 될 것이다.
그러면 String[] spl = s.split(" "); 에서 실제 spl 배열의 크기는 1이라는 것이다.
쉽게 말해서 spl[0] = "" 인 spl 문자열 배열이 된다는 것이다.
결과적으로 spl 배열의 길이를 출력하고자 한다면 0이 아닌 1이 출력되는 것이다.
그렇기 때문에 만약 여러분들이 split함수를 써서 통과를 하고 싶다면 trim()혹은 strip() 을 통해 양쪽 공백을 지운 뒤 해당 문자열이 '빈 문자열'인지 확인하여 빈 문자열이라면 0을 출력하고 그 외에는 split()함수를 통해 String[] 배열로 반환하여 배열의 길이를 출력해주어야 한다.
- 풀이
- 방법 1
import java.util.Scanner;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
String S = in.nextLine();
in.close();
// st 에 공백을 기준으로 나눈 토큰들을 st 에 저장한다
StringTokenizer st = new StringTokenizer(S," ");
// countTokens() 는 토큰의 개수를 반환한다
System.out.println(st.countTokens());
}
}
이게 끝이야? 한다면.. 맞다. 끝이다.
- 방법 2
BufferedReader 을 쓰는 방식이다.
이 방법도 입력 방법만 바뀔 뿐 알고리즘은 달라진 것이 없다.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine()," ");
System.out.print(st.countTokens());
}
}
위 코드를 실행하면 물론 Scanner 보다 BufferedReader 가 속도가 빠르니 시간이 단축되지만 더 빠른 방법이 있다.
아래 방법 3 을 한 번 보자.
- 방법 3
buffer 을 사용하지 않고 원시 입력 형태로 문자 하나씩 읽어들이면서 읽어들인 문자가 공백인지 아닌지에 따라 count 를 해주는 방법이다.
이 방법을 사용하기 위해서는 두 가지 변수가 필요하다.
먼저 이전의 문자를 저장할 pre_str, 그리고 현재 입력받은 문자를 저장할 str. 이렇게 두 개의 변수가 필요하다.
일단 아래 코드를 보자.
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
int count = 0;
int pre_str = 32; // 공백을 의미한다.
int str ;
while(true) {
str = System.in.read();
// 입력받은 문자가 공백일 때,
if(str == 32) {
// 이전의 문자가 공백이 아니면
if(pre_str != 32) count++;
}
// 입력받은 문자가 개행일때 ('\n')
else if(str == 10) {
// 이전의 문자가 공백이 아니면
if(pre_str != 32) count++;
break;
}
pre_str = str;
}
System.out.println(count);
}
}
이렇게 된다.
먼저 입력받은 문자가 공백이면서 이전의 문자가 공백이 아닐 경우에 count 변수를 1 증가시킨다.
공백을 입력받을 때 count 를 1 증가시키지 않고 추가 조건이 붙어 이전의 문자가 공백이 아닐 경우도 포함할 때만 1 증가 시키는 이유는 첫 문자와 마지막 문자가 만약 공백이라면 count 변수가 1 이 증가하기 때문에 이러한 예외를 발생시키지 않게 하기 위해서다.
- 성능 차이
위에서 부터 순서대로
채점 번호 : 18564794 - System.in.read
채점 번호 : 18564786 - BufferedReader
채점 번호 : 18564772 - Scanner
시간을 보면 BufferedReader 와 Scanner 의 성능차이 및 출력 방법에 따른 성능 차이 또한 볼 수 있다.
- 정리
보면 아주 단순한 문제다.
다만, 단순히 공백을 count 해주는 것이 아닌 예외의 케이스도 고려해야 한다는 점이 중요하다.
자바의 경우 StringTokenizer 클래스가 있어서 이를 알고 있다면 매우 쉬운 문제였을 것이다. 하지만 이 클래스를 몰라서 charAt() 으로 반복하면서 처음과 마지막의 공백에 대하여 예외처리를 안하다보니 아무래도 정답률이 엄청 낮았던 것 같다.
참고로 전체 정답 비율은 26% 였고 자바 제출자의 정답 비율은 29% 였다.
'JAVA - 백준 [BAEK JOON] > 문자열' 카테고리의 다른 글
[백준] 5622번 : 다이얼 - JAVA [자바] (28) | 2020.03.24 |
---|---|
[백준] 2908번 : 상수 - JAVA [자바] (12) | 2020.03.20 |
[백준] 1157번 : 단어 공부 - JAVA [자바] (42) | 2020.03.19 |
[백준] 2675번 : 문자열 반복 - JAVA [자바] (22) | 2020.03.19 |
[백준] 10809번 : 알파벳 찾기 - JAVA [자바] (41) | 2020.03.18 |