평소 Python으로 문제를 풀다가 Java로 코딩테스트를 봐야 할 때, 손에서 바로 나와야 하는 것들만 모았다. 알고리즘 이론보다 "Java로 생각을 코드로 옮기는 속도"에 초점을 맞췄다. (Java 17 기준)
오프라인 IDE 테스트는 채점기가 없으므로 main에서 직접 함수를 호출해 결과를 출력하는 형태가 기본이다. 입력이 주어지는 경우엔 BufferedReader가 안전하다.
import java.util.*;
import java.io.*;
import java.util.stream.*;
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());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
int[] arr = Arrays.stream(br.readLine().split(" "))
.mapToInt(Integer::parseInt).toArray();
StringBuilder sb = new StringBuilder();
for (int x : arr) sb.append(x).append('\n');
System.out.print(sb);
}
}
| 상황 | 쓰는 것 | 비고 |
|---|---|---|
| 입력 적음, 빨리 짜기 | Scanner sc = new Scanner(System.in); sc.nextInt(), sc.next(), sc.nextLine() | nextInt() 뒤 nextLine()은 개행 하나 먹으니 한 번 더 호출 |
| 입력 10만 줄 이상 | BufferedReader + StringTokenizer | readLine()은 String 반환, 파싱 필요 |
| 출력 많음 | StringBuilder에 모아 마지막에 한 번 출력 | System.out.println 반복은 느림 |
| 한 줄 정수 여러 개 | Arrays.stream(line.split(" ")).mapToInt(Integer::parseInt).toArray() | |
| 문자열 → 숫자 | Integer.parseInt(s), Long.parseLong(s), Double.parseDouble(s) | |
| 숫자 → 문자열 | String.valueOf(n), Integer.toString(n), "" + n | |
| 진법 변환 | Integer.toBinaryString(n), Integer.toString(n, 2), Integer.parseInt("1011", 2) | |
| 포맷 출력 | String.format("%.2f", d), System.out.printf("%d %s%n", a, b) |
과제형(플레이리스트 만들기 등)이면 입력 파싱 대신 main에 테스트 데이터를 직접 만들어 호출하고, 결과를 println으로 보여주면 된다. 클래스 여러 개는 한 파일에 class Song {}처럼 public 없이 두면 된다.
String은 불변이라 반복 +=는 O(n²)다. 문자열을 만들거나 바꿀 땐 무조건 StringBuilder.
| 할 일 | 코드 |
|---|---|
| 길이 | s.length() (배열은 arr.length, 리스트는 list.size()) |
| i번째 문자 | s.charAt(i) → char |
| 부분 문자열 | s.substring(from, to) (to 미포함), s.substring(from) |
| 문자 배열로 | s.toCharArray() / 다시 문자열로 new String(chars) |
| 분리 | s.split(" "), s.split(",") — 정규식이라 \\., `\ |
| 합치기 | String.join(",", list), String.join(" ", arr) |
| 포함·위치 | s.contains("ab"), s.indexOf("ab") (없으면 -1), s.startsWith, s.endsWith |
| 바꾸기 | s.replace("a", "b") (전부), s.replaceAll("[0-9]", "") (정규식) |
| 대소문자 | s.toUpperCase(), s.toLowerCase() |
| 공백 제거 | s.trim(), s.strip() |
| 비교 | s.equals(t) (절대 == 금지), s.equalsIgnoreCase(t), s.compareTo(t) (사전순, 음수/0/양수) |
| 비었는지 | s.isEmpty(), s.isBlank() |
| 반복 | "ab".repeat(3) |
| 문자 판별 | Character.isDigit(c), isLetter(c), isUpperCase(c), isAlphabetic(c) |
| 문자 ↔ 숫자 | c - '0' (숫자 문자→int), (char)(n + '0'), c - 'a' (알파벳 인덱스) |
| 문자 변환 | Character.toUpperCase(c), Character.toLowerCase(c) |
StringBuilder sb = new StringBuilder();
sb.append("abc").append(1).append('x');
sb.insert(0, "z"); // 앞에 삽입
sb.deleteCharAt(sb.length() - 1);
sb.setCharAt(0, 'q');
sb.reverse(); // 뒤집기 — 팰린드롬 판별에 바로 쓰임
sb.setLength(0); // 비우기
String result = sb.toString();
문자열 뒤집기는 new StringBuilder(s).reverse().toString(). 문자 정렬은 char[] c = s.toCharArray(); Arrays.sort(c); new String(c). 아스키 연산은 char가 정수로 자동 승격되니 (char)(c + 1)처럼 캐스팅 필요.
크기가 고정이면 배열, 늘어나면 ArrayList. int[]와 Integer[]는 다른 타입이라 Comparator 정렬은 Integer[]에만 된다.
int[] a = new int[n]; // 0으로 초기화
int[] b = {3, 1, 2};
int[][] grid = new int[r][c];
boolean[][] visited = new boolean[r][c];
Arrays.fill(a, -1); // 전체 채우기
for (int[] row : grid) Arrays.fill(row, Integer.MAX_VALUE); // 2차원은 행별로
Arrays.sort(a); // 오름차순 (기본형은 역순 직접 불가)
Integer[] boxed = {3, 1, 2};
Arrays.sort(boxed, Collections.reverseOrder());
Arrays.sort(a, from, to); // 구간 정렬
int[] copy = Arrays.copyOf(a, a.length);
int[] part = Arrays.copyOfRange(a, 1, 4); // [1,4)
int[] c2 = a.clone();
Arrays.toString(a); // 디버그 출력 "[1, 2, 3]"
Arrays.deepToString(grid); // 2차원
Arrays.equals(a, b);
Arrays.stream(a).sum(); .max().getAsInt(); .min().getAsInt();
Arrays.asList(boxed); // 고정 크기 List (add 불가)
List<Integer> list = new ArrayList<>(Arrays.asList(boxed)); // 수정 가능
List<Integer> li = Arrays.stream(a).boxed().collect(Collectors.toList()); // int[] → List
int[] back = li.stream().mapToInt(Integer::intValue).toArray(); // List → int[]
int idx = Arrays.binarySearch(a, key); // 정렬된 배열만, 없으면 음수
2차원 배열 정렬은 Arrays.sort(arr, (x, y) -> x[0] - y[0]) (int[][] 은 객체 배열이라 가능). 방향 배열은 int[] dr = {-1, 1, 0, 0}; int[] dc = {0, 0, -1, 1};로 두고 범위 검사 if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;를 반드시 먼저.
과제형 문제의 8할은 ArrayList + HashMap으로 끝난다. 선언은 인터페이스 타입으로, 제네릭은 래퍼 타입(Integer, Long, Character)만 가능.
List<Integer> list = new ArrayList<>();
list.add(x); list.add(0, x); list.get(i); list.set(i, x);
list.remove(i); // 인덱스로 삭제
list.remove(Integer.valueOf(x)); // 값으로 삭제 (int면 인덱스로 오해함)
list.size(); list.isEmpty(); list.contains(x); list.indexOf(x);
Collections.sort(list); list.sort(null); list.sort(Comparator.reverseOrder());
Collections.reverse(list); Collections.max(list); Collections.min(list);
Collections.swap(list, i, j);
list.subList(from, to); // 뷰(복사 아님)
new ArrayList<>(list); // 복사
List.of(1, 2, 3); // 불변
Map<String, Integer> map = new HashMap<>();
map.put(k, v);
map.get(k); // 없으면 null → int에 넣으면 NPE
map.getOrDefault(k, 0);
map.put(k, map.getOrDefault(k, 0) + 1); // 카운팅 기본
map.merge(k, 1, Integer::sum); // 카운팅 한 줄
map.computeIfAbsent(k, x -> new ArrayList<>()).add(v); // 그룹핑 한 줄
map.containsKey(k); map.containsValue(v); map.remove(k); map.size();
for (Map.Entry<String, Integer> e : map.entrySet()) { e.getKey(); e.getValue(); }
for (String k : map.keySet()) {}
for (int v : map.values()) {}
TreeMap<Integer, String> tm = new TreeMap<>(); // 키 정렬 유지
tm.firstKey(); tm.lastKey(); tm.floorKey(x); tm.ceilingKey(x); tm.headMap(x); tm.tailMap(x);
LinkedHashMap<> // 삽입 순서 유지
Set<Integer> set = new HashSet<>();
set.add(x); set.contains(x); set.remove(x); set.size();
new HashSet<>(list).size(); // 중복 제거 개수
TreeSet<Integer> ts = new TreeSet<>(); // 정렬 유지
ts.first(); ts.last(); ts.floor(x); ts.ceiling(x); ts.pollFirst();
Deque<Integer> dq = new ArrayDeque<>();
// 큐: offer / poll / peek (뒤에 넣고 앞에서 뺀다)
// 스택: push / pop / peek (앞에 넣고 앞에서 뺀다)
dq.offerFirst(x); dq.offerLast(x); dq.pollFirst(); dq.pollLast(); dq.peekFirst(); dq.peekLast();
dq.isEmpty();
// Stack 클래스는 느리고 권장 안 함. ArrayDeque는 null 불가.
PriorityQueue<Integer> pq = new PriorityQueue<>(); // 최소 힙
PriorityQueue<Integer> maxPq = new PriorityQueue<>(Collections.reverseOrder());
PriorityQueue<int[]> pq2 = new PriorityQueue<>((a, b) -> a[1] - b[1]); // 배열 우선순위
pq.offer(x); pq.poll(); pq.peek(); pq.size(); pq.isEmpty();
| 자료구조 | 언제 | 주요 연산 비용 |
|---|---|---|
| ArrayList | 순서 있는 목록, 인덱스 접근 | get O(1), 중간 삽입/삭제 O(n) |
| HashMap / HashSet | 카운팅, 중복 체크, 키→값 조회 | O(1) |
| TreeMap / TreeSet | 정렬 상태 유지, 범위 검색 | O(log n) |
| LinkedHashMap | 삽입 순서 유지하는 Map (LRU 등) | O(1) |
| ArrayDeque | 큐, 스택, BFS | O(1) |
| PriorityQueue | 항상 최소/최대 꺼내기, 다익스트라 | O(log n) |
람다 (a, b) -> a - b는 오름차순, b - a는 내림차순. 값이 클 수 있으면 뺄셈 대신 Integer.compare(a, b)를 써야 오버플로우를 피한다.
// 기본
Collections.sort(list);
list.sort(Comparator.reverseOrder());
Arrays.sort(arr2d, (a, b) -> a[0] - b[0]);
// 다중 조건: 점수 내림차순, 같으면 이름 오름차순
list.sort((a, b) -> {
if (a.score != b.score) return Integer.compare(b.score, a.score);
return a.name.compareTo(b.name);
});
// 같은 것을 Comparator 체이닝으로 (면접에서 더 좋아 보임)
list.sort(Comparator.comparingInt((Song s) -> s.score).reversed()
.thenComparing(s -> s.name));
// 문자열 길이 순, 같으면 사전순
words.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
// Map을 값 기준으로 정렬해 상위 N개
List<Map.Entry<String, Integer>> entries = new ArrayList<>(map.entrySet());
entries.sort((a, b) -> b.getValue() - a.getValue());
객체가 기본 정렬 기준을 가지게 하려면 Comparable 구현:
class Song implements Comparable<Song> {
String title; int plays;
Song(String title, int plays) { this.title = title; this.plays = plays; }
@Override
public int compareTo(Song o) { return Integer.compare(o.plays, this.plays); } // 재생수 내림차순
@Override
public String toString() { return title + "(" + plays + ")"; }
}
Collections.sort와 Arrays.sort(객체)는 안정 정렬(TimSort)이라 같은 키는 원래 순서 유지. Arrays.sort(int[])는 듀얼 피벗 퀵소트로 최악 O(n²)가 있으나 실무 테스트에선 신경 안 써도 됨.
과제형에서 "필터링해서 정렬해서 상위 N개" 같은 요구는 Stream 한 줄이 가장 읽기 좋다. 다만 성능 질문이 나오면 "반복문과 동일한 O(n), 가독성 때문에 썼다"고 답하면 된다.
import java.util.stream.*;
// 필터 + 변환 + 수집
List<String> titles = songs.stream()
.filter(s -> s.plays > 100)
.map(s -> s.title)
.collect(Collectors.toList()); // Java 16+ 면 .toList()
// 정렬 + 상위 N
List<Song> top3 = songs.stream()
.sorted(Comparator.comparingInt((Song s) -> s.plays).reversed())
.limit(3)
.collect(Collectors.toList());
// 그룹핑: 아티스트별 곡 목록 / 개수 / 합계
Map<String, List<Song>> byArtist = songs.stream().collect(Collectors.groupingBy(s -> s.artist));
Map<String, Long> countByArtist = songs.stream().collect(Collectors.groupingBy(s -> s.artist, Collectors.counting()));
Map<String, Integer> playsByArtist = songs.stream().collect(Collectors.groupingBy(s -> s.artist, Collectors.summingInt(s -> s.plays)));
// 집계
int total = songs.stream().mapToInt(s -> s.plays).sum();
Optional<Song> best = songs.stream().max(Comparator.comparingInt(s -> s.plays));
double avg = songs.stream().mapToInt(s -> s.plays).average().orElse(0);
boolean any = songs.stream().anyMatch(s -> s.plays == 0); // allMatch, noneMatch
long cnt = songs.stream().filter(s -> s.plays > 0).count();
// 중복 제거, 문자열 합치기
List<String> artists = songs.stream().map(s -> s.artist).distinct().collect(Collectors.toList());
String joined = titles.stream().collect(Collectors.joining(", "));
// 범위 반복, 문자열 문자 순회
IntStream.range(0, n).forEach(i -> ...); // 0..n-1
IntStream.rangeClosed(1, n).sum();
s.chars().filter(Character::isDigit).count();
// List → Map (키 중복 시 터짐, 세 번째 인자로 병합 규칙)
Map<String, Song> byTitle = songs.stream().collect(Collectors.toMap(s -> s.title, s -> s, (a, b) -> a));
Optional은 .orElse(기본값), .orElseThrow(), .isPresent(), .get()로 꺼낸다. Collectors.toList()로 만든 리스트는 수정 가능, .toList()(Java 16+)는 불변이라 add 하면 예외.
1시간 오프라인 테스트에서 실제로 나오는 건 이 여섯 개가 거의 전부다. 각각 손에서 바로 나와야 한다.
// 조합: n개 중 r개 고르기
static void comb(int[] arr, int start, int r, List<Integer> cur, List<List<Integer>> out) {
if (cur.size() == r) { out.add(new ArrayList<>(cur)); return; }
for (int i = start; i < arr.length; i++) {
cur.add(arr[i]);
comb(arr, i + 1, r, cur, out);
cur.remove(cur.size() - 1);
}
}
// 순열은 start 대신 boolean[] used 로, 부분집합은 비트마스크 for (int mask = 0; mask < (1 << n); mask++)
// 합이 target 이상인 최소 길이 구간
int left = 0, sum = 0, best = Integer.MAX_VALUE;
for (int right = 0; right < n; right++) {
sum += arr[right];
while (sum >= target) {
best = Math.min(best, right - left + 1);
sum -= arr[left++];
}
}
int[] pre = new int[n + 1];
for (int i = 0; i < n; i++) pre[i + 1] = pre[i] + arr[i];
// [l, r] 구간 합 = pre[r + 1] - pre[l]
int[] dr = {-1, 1, 0, 0}, dc = {0, 0, -1, 1};
int[][] dist = new int[R][C];
for (int[] row : dist) Arrays.fill(row, -1);
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{sr, sc}); dist[sr][sc] = 0;
while (!q.isEmpty()) {
int[] cur = q.poll();
for (int d = 0; d < 4; d++) {
int nr = cur[0] + dr[d], nc = cur[1] + dc[d];
if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
if (grid[nr][nc] == 1 || dist[nr][nc] != -1) continue;
dist[nr][nc] = dist[cur[0]][cur[1]] + 1;
q.offer(new int[]{nr, nc});
}
}
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
adj.get(a).add(b); adj.get(b).add(a);
boolean[] visited = new boolean[n];
static void dfs(int u) {
visited[u] = true;
for (int v : adj.get(u)) if (!visited[v]) dfs(v);
}
// 재귀 깊이 1만 넘으면 스택으로 바꿀 것 (Java 기본 스택은 작음)
// 정렬된 배열에서 target 이상인 첫 위치 (lower bound)
int lo = 0, hi = n;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (arr[mid] < target) lo = mid + 1; else hi = mid;
}
// 매개변수 탐색: "X가 가능한가"가 단조이면 같은 틀로 최대/최소 X를 찾는다
int[] parent;
static int find(int x) { return parent[x] == x ? x : (parent[x] = find(parent[x])); }
static void union(int a, int b) { parent[find(a)] = find(b); }
Math.max(a, b); Math.min(a, b); Math.abs(x); Math.pow(2, 10); Math.sqrt(x); Math.floorMod(a, m);
long gcd(long a, long b) { return b == 0 ? a : gcd(b, a % b); } // lcm = a / gcd * b
Integer.MAX_VALUE; Integer.MIN_VALUE; Long.MAX_VALUE;
Integer.bitCount(n); 1 << k; (mask & (1 << k)) != 0;
이 표의 절반은 컴파일 에러로 바로 잡히지만, 나머지 절반은 조용히 틀린 답을 낸다. 특히 ==, 정수 나눗셈, 오버플로우는 테스트 끝나기 전에 한 번씩 훑어볼 것.
| 함정 | 틀린 예 | 맞는 예 |
|---|---|---|
| 문자열 비교 | s == "abc" | s.equals("abc") |
| Integer 비교 (128 이상이면 틀림) | list.get(i) == list.get(j) | list.get(i).equals(list.get(j)) 또는 intValue() |
| 정수 나눗셈 | 7 / 2 → 3 (Python 3.5 기대) | 7 / 2.0 또는 (double) a / b |
| 음수 나머지 | -7 % 3 → -1 | Math.floorMod(-7, 3) → 2 |
| int 오버플로우 (약 21억) | int sum = a * b | long sum = (long) a * b — 곱하기 전에 캐스팅 |
| 오버플로우 Comparator | (a, b) -> a - b (큰 값) | Integer.compare(a, b) |
| char + 숫자 | 'a' + 1 → 98 (int) | (char)('a' + 1) → 'b' |
| 배열 출력 | System.out.println(arr) → 주소 | Arrays.toString(arr) |
| 기본형 배열 역순 정렬 | Arrays.sort(intArr, reverseOrder()) 컴파일 에러 | Integer[]로 바꾸거나 정렬 후 뒤집기 |
| List.remove 오해 | list.remove(3) → 인덱스 3 삭제 | 값 삭제는 list.remove(Integer.valueOf(3)) |
| 순회 중 삭제 | for-each 안에서 list.remove() → ConcurrentModificationException | removeIf(x -> ...) 또는 Iterator |
| Map.get null | int v = map.get(k) → NPE | map.getOrDefault(k, 0) |
| 제네릭에 기본형 | List<int> | List<Integer> |
| 스트림 재사용 | 같은 stream 두 번 소비 | 매번 .stream() 새로 |
| 스택 오버플로우 | 재귀 깊이 수만 | 반복 + ArrayDeque로 전환 |
| 객체 복사 | List<Integer> b = a (같은 참조) | new ArrayList<>(a) |
| 리스트 안 리스트 복사 | out.add(cur) (백트래킹에서 다 같은 객체) | out.add(new ArrayList<>(cur)) |
| substring 범위 | s.substring(i, j) j 포함으로 착각 | j 미포함 (Python 슬라이스와 동일) |
| 리스트 마지막 | list.get(-1) | list.get(list.size() - 1) |
| 자료구조 다시 보기 | 적은 배열에서만 테스트 | 빈 입력, 한 개 입력, 중복, 최대 크기 확인 |
문법 상 자주 막히는 것: 삼항은 a ? b : c, 향상된 for문은 for (int x : arr), static 메서드에서는 인스턴스 필드 접근 불가(헬퍼 메서드를 모두 static으로 통일하거나 객체를 만들어 호출), 예외를 던지는 메서드 쓰면 throws IOException 또는 try-catch. 프로그래머스 스타일은 class Solution { public int solution(int[] arr) {...} }이니 그 형태도 익숙해지기.
회사 장비는 맥북일 수도 윈도우일 수도 있다. 이 열 개만 양쪽으로 외우면 된다. 마우스로 해도 되니 기억 안 나면 그냥 클릭.
| 할 일 | Mac | Windows |
|---|---|---|
| 실행 | Ctrl + R | Shift + F10 |
| 현재 파일 main 실행 | Ctrl + Shift + R | Ctrl + Shift + F10 |
| 자동완성 | Ctrl + Space | Ctrl + Space |
| import 자동 추가 | Opt + Enter (빨간 줄에서) | Alt + Enter |
| 모든 import 정리 | Ctrl + Opt + O | Ctrl + Alt + O |
| 코드 포맷 | Cmd + Opt + L | Ctrl + Alt + L |
| 줄 복제 | Cmd + D | Ctrl + D |
| 줄 삭제 | Cmd + Backspace | Ctrl + Y |
| 줄 이동 | Opt + Shift + ↑/↓ | Alt + Shift + ↑/↓ |
| 주석 토글 | Cmd + / | Ctrl + / |
| 이름 일괄 변경 | Shift + F6 | Shift + F6 |
| 메서드 추출 | Cmd + Opt + M | Ctrl + Alt + M |
| 정의로 이동 | Cmd + B | Ctrl + B |
| 파라미터 힌트 | Cmd + P | Ctrl + P |
| 어디서나 검색 | Shift Shift | Shift Shift |
| 에러 위치로 | F2 | F2 |
라이브 템플릿 (타이핑 후 Tab): main → main 메서드, sout → println, fori → for (int i = 0; ...), iter → 향상된 for, psvm → main, ifn → if null, .var (식 뒤에) → 변수 추출, .for → 반복문 감싸기.
시작 직후 할 일 (2분)
.toList(), var, record는 16+ / 10+ / 16+)Main 클래스에 sout("hi") 하고 실행해서 콘솔 나오는지 확인Ctrl + Space 수동 호출, 그래도 안 되면 외운 대로 친다테스트 중 전략: 먼저 클래스와 메서드 시그니처만 빈 몸으로 다 만들어 놓고 설명 주석 한 줄씩 달기 → 핵심 로직 구현 → main에서 테스트 데이터로 호출해 출력 → 엣지 케이스 추가. 시간이 끝나도 뼈대가 다 있으면 면접에서 설명할 게 남는다.
코드를 놓고 바로 면접이 이어지므로, 선택마다 이유를 한 문장으로 말할 수 있어야 한다. 답의 형식은 항상 "필요한 연산 + 그 연산에서 이 구조가 빠른 이유 + 대안을 안 고른 이유".
| 질문 | 답 |
|---|---|
| 왜 HashMap? | 키로 값을 찾는 게 핵심이고 평균 O(1)이라. 순서나 정렬이 필요했다면 TreeMap이나 LinkedHashMap을 썼을 겁니다. |
| 왜 HashSet? | 중복 제거와 포함 여부 확인만 필요하고 값은 필요 없어서. List.contains는 O(n)이라 안 썼습니다. |
| 왜 ArrayList? | 인덱스 접근과 마지막에 추가가 대부분이라. 중간 삽입/삭제가 많았다면 LinkedList나 다른 구조를 고려했을 겁니다. |
| 왜 ArrayDeque? Stack 클래스는? | Stack은 Vector 기반이라 동기화 오버헤드가 있고 공식적으로도 Deque 사용을 권장합니다. |
| 왜 PriorityQueue? | 매번 최소/최대만 꺼내면 되니 전체 정렬 O(n log n) 대신 삽입·삭제 O(log n)으로 충분해서. |
| 왜 TreeMap? | 키 정렬 상태를 유지하면서 범위 검색(floor/ceiling)이 필요해서. 조회만 하면 HashMap이 더 빠릅니다. |
| 왜 StringBuilder? | String은 불변이라 반복 결합이 O(n²)가 되어서. |
| 왜 Stream? / 왜 반복문? | Stream은 의도가 읽히고 복잡도는 동일. 다만 디버깅이나 이른 종료(break)가 필요한 곳은 반복문을 썼습니다. |
| 시간 복잡도는? | 입력 n에 대해 정렬 O(n log n)이 지배적이고 나머지는 O(n)입니다. (자기 코드의 가장 비싼 연산 하나를 집어 말하기) |
| 데이터가 100만 개면? | 메모리는 객체당 약 바이트 계산해서 말하고, 병목 연산을 짚고, 인덱스나 페이징으로 풀겠다고. |
| 동시에 수정하면? | HashMap은 스레드 안전하지 않으니 ConcurrentHashMap이나 동기화, 실제 서비스면 DB 트랜잭션으로. |
| 예외 처리는? | 입력 검증은 경계에서 한 번, 내부는 신뢰. null은 Optional이나 기본값으로. |
| 테스트는 어떻게? | 빈 입력, 한 개, 중복, 경계값을 main에서 확인했고, 실무면 JUnit으로 분리합니다. |
| 다 못 풀었는데? | 여기까지 구현했고, 남은 부분은 이런 방법으로 할 계획이었다고 구체적으로. 변명 말고 설계를 말하기. |
하나 더: 변수명은 a, tmp 대신 playCount, songsByArtist처럼 의미 있게. 코드 스타일을 본다고 했으니 이게 점수에 직접 들어간다.