[알고리즘의 첫 단계] 백준 1032번과 Scanner와 StringBuilder자료 구조/알고리즘2025. 8. 4. 23:21
반응형
여기서 나는 아래와 같이 구현 후 비교를 어떻게 할 것인가에 대한 고민을 했다..
Scanner sc = new Scanner(System.in);
int number = sc.nextInt();
String result = ""; //초기화
String str = "";
for(int i = 0; i<number; i++){
str = sc.nextLine();
result // 비교
}
회사에서 반복적인 코드 작성과 AI로 인해 퇴화된 이 느낌..
Scanner로 한 번 더 생각하기
Scanner 스켈레톤 => 한 걸음 더
| Keep | ① for (i < number) 루프 구조 → 입력 개수만큼 반복한다는 문제 조건 충족. ② result·str 두 변수로 “기준 ↔ 비교” 구도를 이미 잡아 두었다. ③ 문자열을 인덱스별로 비교해야 한다는 관점은 맞다. |
| Problem | ① nextInt() 뒤 개행 미소비 → 첫 nextLine()은 빈 문자열이 들어간다. ② String은 불변 → result를 매번 새로 만들면 성능·가독성 저하. ③ 핵심 로직(문자 비교)이 비어 있어 “다음 단계”를 떠올리기 어렵다. ④ 루프 첫 회차(기준 문자열 확보)와 이후 회차(비교·업데이트) 구분이 필요하다. |
| Try | 아래처럼 “스캐너 그대로” 쓰면서 4 단계로 확장해 보자. |
Scanner sc = new Scanner(System.in);
int number = sc.nextInt();
sc.nextLine(); // ① nextInt() 뒤 남은 '\n' 제거 ← 가장 먼저!
String result = "";
for (int i = 0; i < number; i++) {
String str = sc.nextLine(); // ② 한 줄 입력
if (i == 0) { // ③ 첫 줄은 기준 패턴
result = str;
continue;
}
// ④ 이후부터는 기준(result)과 비교해 업데이트
StringBuilder sb = new StringBuilder(result.length());
for (int idx = 0; idx < result.length(); idx++) {
char base = result.charAt(idx);
char cur = str.charAt(idx);
sb.append(base == cur ? base : '?');
}
result = sb.toString(); // 새 패턴으로 교체
}
System.out.println(result);
단계별 사고 포인트
- 입력 버그 차단 – “Scanner → 개행 소비” 패턴 습관화.
- 첫 줄 고정 – 최초 입력을 기준 패턴으로 잡으면 ‘열 투표’ 로직이 단순해진다.
- 불변 String 관리 – 매 회차 StringBuilder(또는 char[])를 써서 수정 → toString()으로 교체.
- 패턴 갱신 루틴 분리 – merge(String a, String b) 같은 메서드로 빼 두면 테스트·재사용이 쉽다.
그렇다면 String 불변성과 StringBuilder가 뭔지, char[]이 왜 더 효율적인지 알아보자.
KPT 요약
| Keep | 1) 자바 String은 불변(immutable) → 안전하고 해시 캐싱, 상수 풀 활용이 가능하다. 2) 읽기 전용∙짧은 문자열엔 String이 가장 간편하다. |
| Problem | 1) 연결(+)·반복 시마다 새 객체가 생성 → GC 비용 & O(n²) 복사 비용. 2) 루프 안에서 result = result + x … 패턴은 메모리와 CPU 모두 비효율적. 3) 긴 문자열을 누적할 때 String을 쓰면 객체와 char 배열이 쌓여서 메모리 파편화가 일어난다. |
| Try | 1) StringBuilder (동기화 필요 없을 때) 또는 StringBuffer(멀티스레드 환경) 사용. 2) 최종 한 번만 toString()으로 변환 → 중간 단계는 mutable 버퍼로 처리. 3) 하위 레벨 최적화가 필요하거나 부분 치환(예: 패턴 비교)엔 char[]를 직접 다루면 가장 가볍다. |
1. 왜 String은 불변인가?
String a = "java";
a = a + "17";
- 실제로는 ① "java" 객체 → ② "java17" 새 객체가 생김.
- 기존 "java"는 그대로 두고, 새 char 배열을 만들어 두 문자열을 복사.
- 장점:
- 스레드 안전: 공유해도 값이 바뀌지 않는다.
- HashCode 캐싱: 한 번 계산 후 재사용.
- 상수 풀(intern pool): 동일 리터럴 재사용.
- 단점:
- 연결·수정할 때마다 새 객체 → 짧은 문자열이라도 루프 안에선 기하급수적으로 복사·GC 발생.
2. 비효율의 실제 모습
String s = "";
for (int i = 0; i < 10_000; i++) {
s = s + i; // ← 매 회차 char[] 복사 + GC
}
- 0 → "0", 01 → "01", 012 → "012" …
길이가 늘어날수록 이전까지 만든 모든 내용을 계속 복사 → 총 O(n²) 시간.
3. StringBuilder란?
- 가변(mutable) 버퍼: 내부에 **확장 가능한 char[]**를 갖고 포인터만 움직임.
- 메서드: append(), insert(), delete(), setCharAt() 등 in-place 수정.
- 최종 결과가 필요할 때 한 번만 toString() ↔ 이때 복사 1회면 끝.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i);
}
String s = sb.toString(); // 복사 단 1회
- StringBuffer: 메서드에 synchronized가 붙어 있어 멀티스레드 안전하지만 느림.
- “StringVuilder”는 오타. 공식 클래스는 java.lang.StringBuilder.
4. char[]를 직접 쓰는 경우
- 부분 비교·치환처럼 위치별 수정이 잦을 때 가장 가볍다.
char[] pattern = base.toCharArray();
pattern[i] = '?'; // 바로 변경, 추가 객체 0개
- 단점: API가 거칠고, 최종 문자열로 쓰려면 new String(pattern) 필요.
5. 언제 무엇을 쓰면 좋을까?
상황 추천 타입 이유
| 읽기 전용, 짧은 리터럴 | String | 코드 간결, 상수 풀 재사용 |
| 루프 안 누적·연결 | StringBuilder | 가변 버퍼, O(n) 연결 |
| 스레드 여러 개가 동시에 버퍼 접근 | StringBuffer | 동기화 내장 |
| 위치별 수정·치환 빈번 | char[] | 직접 인덱스 접근, 객체 0개 추가 |
🏁 핵심 한 줄 정리
“읽기 전용이면 String, 누적·수정이면 StringBuilder(or char[])”
불변(안전)과 가변(성능)의 맞교환을 기억하면 된다!
여기서 한 단계 더 나아가기
💻 시니어의 풀이: 파일 이름 패턴 찾기
1) 코드 전문
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
String[] files = new String[n];
// ➊ 모든 파일 이름 입력
for (int i = 0; i < n; i++) {
files[i] = br.readLine();
}
// ➋ 예외: 파일이 하나뿐이면 그대로 출력
if (n == 1) {
System.out.println(files[0]);
return;
}
StringBuilder sb = new StringBuilder();
// ➌ 첫 파일을 ‘기준 문자열’로 삼아 열(column) 단위 비교
for (int i = 0; i < files[0].length(); i++) {
char ch = files[0].charAt(i); // 기준 문자
boolean same = true; // 같은 열이 모두 같은지 플래그
for (int j = 1; j < n; j++) {
if (files[j].charAt(i) != ch) {
same = false; // 하나라도 다르면 플래그 해제
break;
}
}
sb.append(same ? ch : '?');
}
System.out.println(sb.toString());
}
}
2) 동작 원리
- 입력
- BufferedReader로 N(1 ≤ N ≤ 50)과 파일 이름 N개를 배열에 저장.
- Scanner 대비 I/O 속도 이점.
- 예외 처리
- 파일이 1개면 비교할 필요 없이 그대로 출력.
- 열(column) 비교 알고리즘
- 첫 파일을 기준 문자열로 잡는다.
- i번째 열 문자 ch를 모든 파일과 비교.
- 모두 같으면 sb.append(ch)
하나라도 다르면 sb.append('?').
- 출력
- StringBuilder에 누적된 결과를 toString()으로 한 번만 복사해 출력.
3) 복잡도 분석
| 시간 | O(N × L) (N = 파일 수, L = 파일 길이) |
| 공간 | O(L) (결과 패턴 저장용 버퍼) |
4) 최적화 포인트
- 첫 파일 + 나머지 비교 → 불필요한 2중 반복 제거.
- BufferedReader & StringBuilder → 입력·문자열 누적 성능 ↑.
- 일찍 탈출(break) → 열 비교 중 다른 문자를 발견하면 즉시 다음 열로.
5) 학습 포인트
- 불변 String vs 가변 버퍼
String은 연결 때마다 새 객체를 생성함 → 반복 누적 시 StringBuilder가 필수. - 패턴 문제 접근법
“세로 방향 투표”처럼 인덱스를 회전하면 논리가 단순해진다. - 예외 처리
입력 1개·길이 동일 등 문제 조건을 먼저 확인하면 코드 분기가 깔끔해진다.
6) 관련·확장 문제
- BOJ 1032 <명령 프롬프트> – 동일한 로직, 파일 이름 대신 커맨드.
- “DNA 서열 공통 부분” – 길이가 긴 문자열 세트에서 공통 염기 찾기.
- 와일드카드가 *(0 개 이상)까지 확장되는 변형 문제.
왜 이 풀이(BufferedReader + 열 단위 비교)가 더 ‘좋은’가?
구분 기존 방식 (Scanner + 매회 StringBuilder) 시니어 풀이
| 입력 처리 | Scanner : 토큰 단위 정규식 파싱 → 느림, nextInt() 뒤 개행 처리 필요 | BufferedReader : 줄 단위 버퍼 → 최대 20 배 이상 빠른 I/O, 개행 버그 없음 |
| 알고리즘 구조 | 반복문 안에서 이전 패턴 + 새 문자열을 매번 병합 | 첫 파일을 기준으로 열(column) 한 번만 순회 → 비교 횟수 최소화 |
| 문자열 누적 | 매 루프마다 StringBuilder → toString() 변환 | 1개의 StringBuilder에 O(L) 번만 append |
| 가독성 | 기준·비교 로직이 루프 안에서 뒤섞임 | “➊입력 → ➋예외 → ➌열 비교 → 출력” 단계별 명확 |
| 버그 여지 | nextInt() 뒤 개행 미소비 시 빈 문자열 입력 오류 | 그런 상황 자체가 없음 |
결국 I/O 속도 + 비교 횟수 + 코드 명료성 세 가지 면에서 유리합니다.
BufferedReader & InputStreamReader — 무엇이고 왜 쓰나?
1. InputStreamReader
- 바이트 스트림(System.in) → 문자 스트림으로 바꿔 주는 어댑터.
- 인코딩을 지정할 수도 있다.
- new InputStreamReader(System.in, StandardCharsets.UTF_8);
2. BufferedReader
- 문자 스트림을 8 KB(기본) 버퍼에 모아 한꺼번에 읽는다.
- 메서드: readLine(), read(char[]), skip() 등.
- 버퍼 덕분에 시스템 호출 횟수 ↓ → Scanner 대비 수 ~ 십 배 빠름.
한 줄 요약
- System.in (byte) → InputStreamReader → (char) → BufferedReader → (buffered char)
- 이렇게 층층이 감싸면 문자 단위 고속 입력이 가능하다.
언제 Scanner를 쓰고 언제 BufferedReader를 쓸까?
상황 추천 클래스
| • 입출력양이 작고 편의성이 더 중요 • 공백·줄바꿈을 토큰으로 쉽게 나누고 싶음 |
Scanner |
| • 온라인 저지처럼 수만~수십만 줄 입력 • 줄 단위 그대로 읽어 자체 파싱 |
BufferedReader + 필요하면 StringTokenizer |
마무리 – 핵심 포인트 3줄
1. I/O : BufferedReader가 Scanner보다 훨씬 빠르다.
2. 알고리즘 : “첫 문자열을 기준 세로 비교”가 가장 적은 연산으로 패턴을 만든다.
3. 코드 품질 : 단계별 명확한 흐름(입력 → 예외 → 열 검사 → 출력)으로 버그를 줄이고 읽기 쉽다.
반응형
'자료 구조 > 알고리즘' 카테고리의 다른 글
| [알고리즘 첫 단계] 문자열 분석 10820번 문제 Character 클래스와 나의 문제점 KPT (3) | 2025.08.05 |
|---|---|
| 연결리스트 (0) | 2024.11.22 |
| 구현 알고리즘 (1) | 2024.11.14 |
| 그리디 알고리즘이란? (2) | 2024.11.08 |
@mellona :: 주니어의 다사다난 성장기
안녕하세요. si 회사 소속 sm LMS 팀에 소속중인 2년차 백엔드 개발자입니다😀 함께 나누고 성장하는 것을 좋아해요. 언제든 디스코드나 구글 메일로 질문해도 됩니다!
⭐ 잘못된 내용은 댓글 적어주세요 :)