자바로 코딩 테스트 — 입출력부터 자주 쓰는 패턴까지
Java로 코딩 테스트를 보면 Python보다 코드가 길어지는데, 그래도 Java를 쓰는 이유가 있나요?
Java로 코딩 테스트를 보는 이유
Java는 Python보다 코드가 길지만, 몇 가지 장점이 있습니다.
- **타입 안전성 **: 컴파일 시점에 타입 오류를 잡을 수 있습니다
- ** 실행 속도 **: Python보다 10~50배 빠릅니다
- ** 풍부한 자료구조 **: Collections Framework가 강력합니다
- ** 백엔드 실무와 일치 **: Spring 기반 개발자라면 Java가 자연스럽습니다
단, 올바른 패턴을 알아야 Java의 장점을 살릴 수 있습니다.
빠른 입출력
BufferedReader + StringTokenizer
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringBuilder sb = new StringBuilder();
// 한 줄 읽기
int n = Integer.parseInt(br.readLine().trim());
// 공백으로 구분된 여러 값 읽기
StringTokenizer st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
// 배열 입력
int[] arr = new int[n];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < n; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
// 출력 — StringBuilder에 모아서 한 번에
for (int i = 0; i < n; i++) {
sb.append(arr[i]).append('\n');
}
System.out.print(sb);
}
}
Scanner vs BufferedReader 속도 비교
입력 100만 줄:
Scanner: 약 2.5초
BufferedReader: 약 0.3초
→ 약 8배 차이!
코딩 테스트에서 시간 초과의 원인이 Scanner인 경우 가 생각보다 많습니다. 항상 BufferedReader를 사용하는 것을 습관화합니다.
입력 템플릿
// 코딩 테스트 기본 템플릿
import java.io.*;
import java.util.*;
public class Main {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StringBuilder sb = new StringBuilder();
public static void main(String[] args) throws IOException {
solve();
System.out.print(sb);
}
static void solve() throws IOException {
int n = nextInt();
// 풀이 작성
}
static int nextInt() throws IOException {
return Integer.parseInt(br.readLine().trim());
}
static int[] nextIntArray() throws IOException {
StringTokenizer st = new StringTokenizer(br.readLine());
int n = st.countTokens();
int[] arr = new int[n];
for (int i = 0; i < n; i++) arr[i] = Integer.parseInt(st.nextToken());
return arr;
}
}
자주 쓰는 자료구조 패턴
HashMap — 빈도 세기
// 빈도 카운트
Map<Integer, Integer> freq = new HashMap<>();
for (int x : arr) {
freq.merge(x, 1, Integer::sum);
// 또는: freq.put(x, freq.getOrDefault(x, 0) + 1);
}
// 최빈값 찾기
int maxKey = Collections.max(freq.entrySet(),
Map.Entry.comparingByValue()).getKey();
TreeMap — 정렬된 맵
// 자동 정렬되는 맵
TreeMap<Integer, Integer> treeMap = new TreeMap<>();
treeMap.put(3, 30);
treeMap.put(1, 10);
treeMap.put(2, 20);
treeMap.firstKey(); // 1 (최솟값)
treeMap.lastKey(); // 3 (최댓값)
treeMap.floorKey(2); // 2 (2 이하 최대)
treeMap.ceilingKey(2); // 2 (2 이상 최소)
treeMap.subMap(1, true, 3, false); // {1=10, 2=20}
PriorityQueue — 힙
// 최소 힙 (기본)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// 최대 힙
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// 커스텀 정렬 — (비용, 노드) 쌍
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{10, 1}); // 비용 10, 노드 1
pq.offer(new int[]{5, 2}); // 비용 5, 노드 2
int[] top = pq.poll(); // [5, 2] — 비용이 작은 것 먼저
Deque — 양방향 큐
Deque<Integer> deque = new ArrayDeque<>();
deque.offerFirst(1); // 앞에 추가
deque.offerLast(2); // 뒤에 추가
deque.pollFirst(); // 앞에서 제거
deque.pollLast(); // 뒤에서 제거
// 스택으로 사용
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // = offerFirst
stack.pop(); // = pollFirst
// 큐로 사용
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1); // = offerLast
queue.poll(); // = pollFirst
Set — 중복 제거
Set<Integer> set = new HashSet<>();
set.add(1);
set.contains(1); // true
// 정렬된 Set
TreeSet<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.first(); // 1
sorted.floor(2); // 1 (2 이하 최대)
sorted.ceiling(2); // 3 (2 이상 최소)
정렬 패턴
// 기본 정렬 (오름차순)
int[] arr = {3, 1, 2};
Arrays.sort(arr);
// 내림차순 — Integer[] 필요
Integer[] arr2 = {3, 1, 2};
Arrays.sort(arr2, Collections.reverseOrder());
// 2차원 배열 정렬 — 첫 번째 값 기준, 같으면 두 번째 값
int[][] intervals = {{3, 5}, {1, 3}, {1, 2}};
Arrays.sort(intervals, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
// → [[1,2], [1,3], [3,5]]
// List 정렬
List<int[]> list = new ArrayList<>();
list.sort(Comparator.comparingInt(a -> a[0]));
주의: int[]의 부분 정렬
// 인덱스 2~5만 정렬
Arrays.sort(arr, 2, 6); // [fromIndex, toIndex)
자주 쓰는 유틸리티
배열 초기화
int[] arr = new int[n];
Arrays.fill(arr, -1);
int[][] grid = new int[n][m];
for (int[] row : grid) Arrays.fill(row, Integer.MAX_VALUE);
최대/최소
int max = Math.max(a, b);
int min = Math.min(a, b);
// 배열의 최대값
int arrMax = Arrays.stream(arr).max().orElse(0);
// 주의: 스트림은 느릴 수 있음. 루프가 더 빠름
String 처리
// 문자열 → 문자 배열
char[] chars = s.toCharArray();
// 문자열 뒤집기
String reversed = new StringBuilder(s).reverse().toString();
// 부분 문자열
String sub = s.substring(2, 5); // 인덱스 2~4
// 문자열 비교
s1.equals(s2); // 내용 비교 (== 쓰지 말 것!)
s1.compareTo(s2); // 사전순 비교
시간/메모리 관리
int vs long
// N이 10만이고 값이 10만이면, 합은 10^10 → int 범위 초과!
// int 범위: 약 ±2.1 × 10^9
// long 범위: 약 ±9.2 × 10^18
long sum = 0;
for (int x : arr) sum += x; // long으로 누적
재귀 깊이
// Java의 기본 스택 크기로 깊이 약 5,000~10,000까지만 가능
// N이 10만이면 재귀 DFS가 스택 오버플로 발생
// 해결 1: 스레드 스택 크기 늘리기
public class Main {
public static void main(String[] args) {
new Thread(null, () -> {
// 여기서 풀이 실행
}, "main", 1 << 26).start(); // 64MB 스택
}
}
// 해결 2: 재귀를 반복문으로 변환
메모리 절약
// boolean 배열: 1바이트/원소 (BitSet은 1비트/원소)
BitSet visited = new BitSet(1_000_000);
visited.set(0);
visited.get(0); // true
// int[][] 대신 1차원 배열
int[] grid = new int[rows * cols];
// grid[r][c] → grid[r * cols + c]
유용한 코드 스니펫
그래프 인접 리스트
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
// 간선 추가 (양방향, 가중치)
graph.get(u).add(new int[]{v, weight});
graph.get(v).add(new int[]{u, weight});
Union-Find
int[] parent = new int[n];
int[] rank = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void union(int a, int b) {
a = find(a); b = find(b);
if (a == b) return;
if (rank[a] < rank[b]) { int t = a; a = b; b = t; }
parent[b] = a;
if (rank[a] == rank[b]) rank[a]++;
}
이분 탐색
// lower_bound — target 이상인 첫 위치
int lowerBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo;
}
// upper_bound — target 초과인 첫 위치
int upperBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (arr[mid] <= target) lo = mid + 1;
else hi = mid;
}
return lo;
}
주의할 점
Scanner 대신 BufferedReader를 사용하지 않아 시간 초과
Scanner는 입력 파싱이 느립니다. 입력이 수만 줄 이상이면 BufferedReader + StringTokenizer 조합이 필수입니다. 출력도 StringBuilder에 모아서 한 번에 출력해야 합니다.
배열 크기를 넉넉하게 잡지 않아 ArrayIndexOutOfBoundsException
"1-indexed"로 문제를 풀 때 배열 크기를 n이 아닌 n+1로 잡아야 합니다. 이 1 차이로 런타임 에러가 빈번합니다.
정리
- BufferedReader + StringTokenizer 는 Scanner보다 5~10배 빠릅니다. 항상 사용합니다
- StringBuilder 에 출력을 모아서 한 번에 출력합니다
- HashMap, TreeMap, PriorityQueue, Deque를 상황에 맞게 선택합니다
int오버플로에 주의합니다. N × N이 21억을 넘으면long을 씁니다- 재귀 깊이가 깊으면 스레드 스택 확장 또는 ** 반복문 변환 **을 고려합니다
- lower_bound, Union-Find, 그래프 인접 리스트 등 ** 스니펫을 미리 준비 **해두면 실전에서 시간을 절약합니다
댓글 로딩 중...