[LeetCode] 0003. Longest Substring Without Repeating Characters
3. Longest Substring Without Repeating Characters
문제 링크
LeetCode - Longest Substring Without Repeating Characters
문제 설명
문자열 s가 주어졌을 때, 반복되는 문자가 없는 가장 긴 부분 문자열(substring)의 길이를 구합니다.
예시:
Input: s = "abcabcbb"
Output: 3
설명: 정답은 "abc"이며, 길이는 3입니다.
핵심 개념
1. 슬라이딩 윈도우 (Sliding Window)
windowStart~windowEnd두 포인터로 “반복 문자가 없는 구간”을 표현windowEnd는 항상 오른쪽으로 한 칸씩만 전진 (되돌아가지 않음)- 중복 문자를 만나면
windowStart만 앞으로 점프시켜 윈도우를 좁힘
2. “마지막으로 본 위치” 테이블
Map<Character, Integer>에 각 문자가 가장 최근에 등장한 인덱스만 저장- 중복 여부는
containsKey로, 그 위치가 현재 윈도우 안인지(>= windowStart)까지 같이 확인해야 함- 윈도우 밖에서 봤던 문자는 “중복”으로 치면 안 됨 (흔한 함정)
3. 한 번의 순회로 끝내기
- 모든 시작점에서 다시 훑지 않고, 문자열 전체를 딱 한 번만 순회
접근 방법 (최종 O(n) 풀이)
lastSeen맵과windowStart = 0,maxLength = 0초기화windowEnd를 0부터s.length() - 1까지 순회하며- 현재 문자가
lastSeen에 있고, 그 위치가windowStart이상이면 →windowStart를 그 위치 + 1로 이동 lastSeen에 현재 문자의 최신 위치 갱신maxLength = max(maxLength, windowEnd - windowStart + 1)
- 현재 문자가
maxLength반환
처음에 시도했던 방식과 문제점
❌ 시도 1: 컴파일은 되지만 느린 브루트포스 (O(n²))
public int lengthOfLongestSubstring(String s) {
int result = 0;
for (int i = 0; i < s.length(); i++) {
HashMap<String,Integer> map = new HashMap<>();
map.put(String.valueOf(s.charAt(i)), i);
for (int j = i + 1; j < s.length(); j++) {
if (map.containsKey(String.valueOf(s.charAt(j)))) {
break;
}
map.put(String.valueOf(s.charAt(j)),j);
}
result = Math.max(result, map.size());
}
return result;
}
문제:
- 모든 시작 인덱스
i마다 안쪽 루프를 다시 돌기 때문에 최악의 경우(반복 문자가 거의 없는 긴 문자열)O(n²) i마다HashMap을 새로 만들어 GC 부담(할당/해제) 추가- 이미 지나온 구간을 계속 재탐색 — 한 번 확인한 정보를 버리고 다시 계산
해결:
HashMap하나를 계속 재사용하면서 “마지막으로 본 위치”만 기억 → 윈도우 시작점을 앞으로만 이동 (아래 “올바른 풀이” 참고)
올바른 풀이
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> lastSeen = new HashMap<>();
int maxLength = 0;
int windowStart = 0;
for (int windowEnd = 0; windowEnd < s.length(); windowEnd++) {
char c = s.charAt(windowEnd);
if (lastSeen.containsKey(c) && lastSeen.get(c) >= windowStart) {
windowStart = lastSeen.get(c) + 1;
}
lastSeen.put(c, windowEnd);
maxLength = Math.max(maxLength, windowEnd - windowStart + 1);
}
return maxLength;
}
복잡도 분석
- 시간 복잡도: O(n)
windowEnd가 문자열을 한 번만 순회,windowStart도 총 n번 이하로만 이동
- 공간 복잡도: O(min(n, Σ))
Σ는 사용 가능한 문자 집합 크기 (예: ASCII 128) — 맵에는 현재 윈도우 안의 서로 다른 문자만 저장됨
핵심 포인트
- 슬라이딩 윈도우: 포인터를 되돌리지 않고 한 방향으로만 이동시켜야 O(n)이 됨
- “윈도우 안에 있는지” 체크 필수: 단순히
containsKey만 보면 윈도우 밖의 과거 위치까지 중복으로 오인함 - HashMap 실제 API 확인:
push/contain이 아니라put/containsKey(get으로 값 조회) - 자료구조 재사용: 루프마다 새 컬렉션을 만들지 말고 하나를 유지하며 갱신하면 성능과 GC 부담 모두 개선됨
테스트 케이스
// Case 1: 기본 케이스
s = "abcabcbb"
결과 = 3 // "abc"
// Case 2: 모두 같은 문자
s = "bbbbb"
결과 = 1 // "b"
// Case 3: 윈도우 밖 문자는 중복 아님
s = "pwwkew"
결과 = 3 // "wke"
// Case 4: 빈 문자열
s = ""
결과 = 0
// Case 5: 윈도우 재설정 확인
s = "abba"
결과 = 2 // "ab" 또는 "ba"
댓글 남기기