인트로
1편에서 기본기를 익혔다면, 이제 복잡한 입출력과 성능 최적화를 마스터해보자!
[코테] JAVA 백준 알고리즘 시작하기 - 입출력 가이드 초보자
1. 입력 패턴들
1-1. 2차원 배열 입력받기
// 입력:
// 3
// 1 2 3
// 4 5 6
// 7 8 9
int n = Integer.parseInt(br.readLine());
int[][] map = new int[n][n];
for(int i = 0; i < n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
for(int j = 0; j < n; j++) {
map[i][j] = Integer.parseInt(st.nextToken());
}
}
응용: 직사각형 배열
// 입력: 3 4 (세로 3, 가로 4)
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
int[][] map = new int[n][m];
- StringTokenizer
→ 문자열을 공백이나 특정 구분자로 분리해주는 클래스
→ 기존 객체에 새 문자열을 재설정할 수 없으므로, 줄마다 새로운 객체를 생성해야 한다.
1-2. 문자 배열 처리
// 입력: "ABC" 같은 문자열을 문자 하나씩 처리
String line = br.readLine();
char[] chars = line.toCharArray();
// 또는 바로 접근
for(int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
}
- String.toCharArray()
→ 문자열을 문자 배열로 변환 - String.charAt(int)
→ 특정 인덱스 위치의 문자를 반환
1-3. 숫자를 문자로 입력받기
// 입력: "12345" (공백 없는 숫자들)
String line = br.readLine();
int[] digits = new int[line.length()];
for(int i = 0; i < line.length(); i++) {
digits[i] = line.charAt(i) - '0'; // 문자를 숫자로 변환
}
Character 클래스
→ '0'을 빼는 이유는 아스키 코드 때문이다. '1'의 아스키는 49, '0'의 아스키는 48이므로 49-48=1이 된다.
1-4. 여러 테스트케이스 처리
int t = Integer.parseInt(br.readLine());
StringBuilder sb = new StringBuilder();
while(t-- > 0) {
// 각 테스트케이스 처리
int n = Integer.parseInt(br.readLine());
// 로직 처리
sb.append(result).append('\n');
}
System.out.print(sb);
입력 형태
3 <- 테스트케이스 개수
5 3 <- 첫 번째 테스트케이스
10 7 <- 두 번째 테스트케이스
1 9 <- 세 번째 테스트케이스
핵심 포인트
- t--의 동작: t 값을 사용한 후 1 감소. t=3이면 3→2→1→0 순서로 실행
- StringBuilder 사용 이유: 각 테스트케이스마다 System.out.println() 쓰면 느림
- 한 번에 출력: 모든 결과를 모아서 마지막에 System.out.print(sb)
실제 동작 과정
t = 3
첫 번째: t=3 (조건 true) → 실행 → t=2
두 번째: t=2 (조건 true) → 실행 → t=1
세 번째: t=1 (조건 true) → 실행 → t=0
네 번째: t=0 (조건 false) → 종료
- StringBuilder
→ 문자열을 효율적으로 누적/조작할 수 있는 가변 버퍼로 출력 성능 최적화에 사용된다. - 후위 연산자(t--)
→ 먼저 현재 값을 사용한 뒤에 1 감소한다.
1-5. EOF 처리 (입력 끝까지 읽기)
String line;
while((line = br.readLine()) != null) {
// 입력 개수가 주어지지 않을 때
StringTokenizer st = new StringTokenizer(line);
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
sb.append(a + b).append('\n');
}
- BufferedReader.readLine()
→ 줄 끝 문자를 제외하고 문자열을 반환한다. 입력이 끝나면 null을 반환한다.
1-6. 다양한 타입 파싱
// Long, Double 처리
long l = Long.parseLong(st.nextToken());
double d = Double.parseDouble(st.nextToken());
// 여러 구분자 사용
StringTokenizer st = new StringTokenizer(br.readLine(), " ,:");
// 앞뒤 공백 제거
String line = br.readLine().trim();
- Long.parseLong()
- Double.parseDouble()
→ 문자열을 숫자 타입으로 변환. - String.trim()
→ 앞뒤 공백 제거.
2. 고급 출력 패턴들
2-1. 한 줄에 여러 값 출력 (공백 구분)
StringBuilder sb = new StringBuilder();
for(int i = 0; i < n; i++) {
sb.append(arr[i]);
if(i < n-1) sb.append(' '); // 마지막엔 공백 안붙임
}
sb.append('\n');
System.out.print(sb);
- StringBuilder.append()
→ 정수, 문자, 문자열 등 다양한 타입을 문자열로 변환해 붙일 수 있다.
2-2. 조건부 출력
StringBuilder sb = new StringBuilder();
if(isPossible) {
sb.append("YES\n");
sb.append(answer).append('\n');
} else {
sb.append("NO\n");
}
System.out.print(sb);
2-3. 형식화된 출력
// 소수점 둘째 자리까지
System.out.printf("%.2f\n", result);
// StringBuilder와 함께 쓸 때
sb.append(String.format("%.2f", result)).append('\n');
- System.out.printf()
→ 형식 지정자(%.2f) 등을 이용해 출력 가능하다. - String.format()
→ 문자열을 특정 형식에 맞춰 반환한다.
2-4. BufferedWriter를 쓸 수도 있다
StringBuilder 대신 BufferedWriter를 사용할 수도 있다.
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
for(int i = 1; i <= n; i++) {
bw.write(String.valueOf(i));
bw.newLine();
}
bw.flush();
bw.close();
성능
일반적으로 StringBuilder와 BufferedWriter 모두 System.out.println보다 빠르다. 구체적인 성능은 환경에 따라 다르지만, 알고리즘 문제에서는 둘 다 시간초과를 피하기에 충분히 빠르다.
대부분 StringBuilder면 충분하다.
BufferedWriter는 출력이 극도로 많거나 파일 입출력할 때 고려한다.
- BufferedWriter
→ 버퍼링된 문자 출력 스트림. write(), newLine(), flush() 메서드를 제공한다. - Writer.flush()
→ 버퍼에 남은 데이터를 강제로 출력한다.
3. 성능 최적화 & 실수 방지
3-1. StringBuilder vs System.out.println
// ❌ 출력이 많을 때 (1000번 이상) - 시간초과 위험
for(int i = 1; i <= 100000; i++) {
System.out.println(i);
}
// ✅ 안전한 방법
StringBuilder sb = new StringBuilder();
for(int i = 1; i <= 100000; i++) {
sb.append(i).append('\n');
}
System.out.print(sb);
System.out.println()은 매번 flush가 발생하여 느리다.
StringBuilder는 메모리에 모아뒀다가 한 번에 출력한다.
- PrintStream
→ println()은 줄바꿈 시 버퍼를 flush할 수 있어 반복 호출 시 성능 저하가 발생할 수 있다. - StringBuilder
→ append로 문자열을 누적 후, 한 번에 출력 가능하다.
3-2. StringTokenizer vs split()
// ✅ 빠른 방법 - StringTokenizer
StringTokenizer st = new StringTokenizer(br.readLine());
while(st.hasMoreTokens()) {
int num = Integer.parseInt(st.nextToken());
}
// ❌ 느린 방법 - split (정규식 사용)
String[] tokens = br.readLine().split(" ");
for(String token : tokens) {
int num = Integer.parseInt(token);
}
split()은 내부적으로 정규식을 사용하여 StringTokenizer보다 느리다.
- StringTokenizer
→ 레거시 클래스. 문서에서 새로운 코드에서는 split 사용을 권장한다고 안내한다. - String.split()
→ 정규식을 사용해 문자열을 분리한다.
3-3. StringTokenizer 재사용 관련 주의사항
StringTokenizer는 생성자에서 전달받은 문자열에 대해서만 토큰화를 수행한다. Java SE API 문서를 확인해보면 기존 StringTokenizer 객체에 새로운 문자열을 설정하는 메서드는 제공되지 않는다.
사용 가능한 주요 메서드들:
- hasMoreTokens(): 더 많은 토큰이 있는지 확인
- nextToken(): 다음 토큰 반환
- countTokens(): 남은 토큰 개수 반환
따라서 새로운 줄의 입력을 처리하려면 새로운 StringTokenizer 객체를 생성해야 한다.
// ❌ 잘못된 방법 - 한 줄에서만 토큰을 가져올 수 있음
StringTokenizer st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
// 다음 줄로 넘어갔는데 같은 StringTokenizer 사용하려 함
// ✅ 올바른 방법 - 새 줄마다 새로운 StringTokenizer
for(int i = 0; i < n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
// 이 줄의 토큰들 처리
}
- StringTokenizer
→ 문자열을 새로 설정하는 메서드는 제공하지 않는다. 새로운 입력을 처리하려면 반드시 새 객체를 만들어야 한다.
3-4. 자주하는 실수들
StringBuilder에서 println 사용
// ❌ 잘못된 방법
StringBuilder sb = new StringBuilder();
sb.append("Hello");
System.out.println(sb); // 불필요한 줄바꿈 추가
// ✅ 올바른 방법
System.out.print(sb);
마지막 공백/줄바꿈 처리
// 문제에서 "마지막에 공백 없이" 라고 할 때
StringBuilder sb = new StringBuilder();
for(int i = 0; i < n; i++) {
sb.append(arr[i]);
if(i < n-1) sb.append(' '); // 조건 확인 필수!
}
// 마지막에 \n 붙일지 문제 조건 확인
IOException 처리 깜빡하기
// ❌ 컴파일 에러
public static void main(String[] args) {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
}
// ✅ 정상
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
}
- PrintStream.println(Object)
→ 문자열 뒤에 줄바꿈 포함한다. - PrintStream.print(Object)
→ 줄바꿈 없이 출력한다.
4. 실전 응용 예제
4-1. 바둑판 입력 (BFS/DFS용)
int n = Integer.parseInt(br.readLine());
char[][] map = new char[n][n];
for(int i = 0; i < n; i++) {
String line = br.readLine();
for(int j = 0; j < n; j++) {
map[i][j] = line.charAt(j);
}
// 또는 map[i] = line.toCharArray();
}
4-2. 그래프 인접리스트 입력
int n = Integer.parseInt(br.readLine()); // 정점 수
int m = Integer.parseInt(br.readLine()); // 간선 수
List<Integer>[] graph = new ArrayList[n+1];
for(int i = 1; i <= n; i++) {
graph[i] = new ArrayList<>();
}
for(int i = 0; i < m; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
graph[a].add(b);
graph[b].add(a); // 무방향 그래프인 경우
}
- ArrayList
→ 동적 배열로, add() 메서드를 통해 원소를 추가할 수 있다.
5. 주의사항
5-1. StringTokenizer는 레거시 클래스
Java SE API 문서에 따르면 StringTokenizer는 "호환성을 위해 유지되는 레거시 클래스"이며, 새로운 코드에서는 String.split() 사용을 권장한다.
하지만 알고리즘 문제에서는 성능상 이유로 여전히 StringTokenizer를 사용하기도 한다.
5-2. BufferedReader의 readLine() 동작
Java SE API 문서에 따르면 readLine()은 "한 줄의 텍스트를 읽는다"고 명시되어 있다.
일반적으로 줄바꿈 문자를 만나면 그 전까지의 문자열을 반환하며, 파일 끝에 도달하면 null을 반환한다.
5-3. Integer.parseInt() 예외 처리
실제 프로젝트에서는 NumberFormatException 처리가 필요하지만, 알고리즘 문제에서는 입력이 보장되므로 생략한다.
- Integer.parseInt()
→ 문자열이 숫자 형식이 아닐 경우 NumberFormatException 발생~
5-4. close() 관련
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
- BufferedReader / BufferedWriter와 close() 관계
- BufferedReader는 Reader(문자 입력 스트림)를 BufferedWriter는 Writer(문자 출력 스트림)를 감싼 래퍼 클래스다.
- 실제 리소스를 소유한 건 System.in / System.out
- 따라서 br.close()나 bw.close()를 호출하면 내부적으로 원본 스트림(System.in, System.out)까지 닫힌다.
- System.in / System.out
→ System.in, System.out, System.err는 JVM이 종료 시 자동으로 닫히도록 보장된다.
→ 따라서 코딩테스트 환경에서는 BufferedReader, BufferedWriter를 close() 하지 않아도 입력/출력에 문제가 발생하지 않는다.
- 실무와 차이점
→ 실무에서는 파일, 네트워크 소켓 등 외부 리소스를 다룰 때 반드시 close() 또는 try-with-resources로 닫아줘야 리소스 누수 문제가 없다.
→ 하지만 코딩테스트에서는 단일 실행 후 즉시 프로그램이 종료되므로 close() 생략해도 무방하다.
6. 마무리
정리
- 입력: 2차원 배열, 문자 처리, EOF 등 다양한 패턴 숙지
- 출력: StringBuilder 위주, 필요시 BufferedWriter 고려
- 성능: StringTokenizer > split(), StringBuilder > println
- 실수 방지: 매번 new StringTokenizer, 마지막 공백 주의
7. 참고자료
Java SE 8 API 공식 문서
- BufferedReader - 버퍼링된 문자 입력 스트림, readLine() 메서드 상세
- StringTokenizer - 레거시 클래스, split() 사용 권장
- StringBuilder - 가변 문자열 버퍼, append() 메서드들
- InputStreamReader - 바이트 스트림을 문자 스트림으로 변환
- BufferedWriter - 버퍼링된 문자 출력 스트림
- OutputStreamWriter - 문자 스트림을 바이트 스트림으로 변환
성능 관련 참고
- StringTokenizer가 split()보다 빠른 이유: 정규식 엔진을 사용하지 않음
- StringBuilder vs String 연결: StringBuilder는 내부 버퍼를 사용하여 효율적
'공부일기.. > Java' 카테고리의 다른 글
| [java] 예외 처리와 트랜잭션 롤백 (PSA (Portable Service Abstraction)) (0) | 2025.10.29 |
|---|---|
| [java] Enum 추상 메서드 활용과 이해 (0) | 2025.10.03 |
| [java] 자바 파일 처리 기본기 가이드 - 주니어 개발자 필수 FILE/FILES (0) | 2025.09.22 |
| [java] static 제대로 활용하기 - 패턴과 함정 (0) | 2025.09.21 |
| [java] static 키워드 - 제약사항~ (0) | 2025.09.20 |