
1. 문제 분석
문제분석
--------------------
문자열 s, t와 이들의 길이 m, n이 각각 주어진다.
중복을 포함한 t 안의 모든 문자가 s의 부분 문자열이 되는 최소 window를 반환하라.
부분 문자열이 없다면 빈 문자열을 반환하라.
제한조건
--------------------
- m == s.length
- n == t.length
- 1 <= m, n <= 10^5
- s와 t는 대소문자로 구성된 영문자이다.
2. 접근 방법
접근방법 - Sliding Window + Hash Table
--------------------
t 문자열의 hash table을 만들고 start, end 포인터로 window 크기를 구하여
점차 window 크기를 점차 줄여나가는 방향으로 접근한다.
- tHash: t 문자열의 hash
- sHash: s 문자열의 hash
- required: 필요한 고유 문자 갯수
- start, end: Sliding window 포인터
- minStart: 최소길이 부분 문자열 시작 포인터
- minLen: 부분 문자열 최소 길이
- matches: sHash의 빈도수와 tHash의 빈도수가 동일한 문자의 갯수
-> t에서 필요한 문자 수만큼 채운 경우 matches 증가
1. hash 생성
우선, t의 hash table을 만들어 필요한 고유 문자 수를 카운팅한다.
고유 문자수를 카운팅하는 이유는 t 문자들이 window 내에 모두 포함될 때 증가하는 matches 변수와 비교하여
이 후에 sliding window 크기를 줄이기 위한 조건으로 사용하기 위함이다.
2. sliding window 탐색
- end 포인터가 s 문자열 끝에 도달할 때 까지 반복하여 sliding window를 탐색한다.
- tHash에 존재하는 문자일 경우, sHash에 빈도수를 추가
- sHash 빈도수와 tHash 빈도수가 동일할 경우 matches를 증가시킨다.
3. window 크기 줄이기 ⭐️
matches와 required가 동일한 경우엔 이미 t 문자열이 window에 모두 포함되어 있는 상태이므로
이 때는 start, end 포인터를 비교하여 최소길이를 업데이트하고
최소길이를 갖는 부분문자열의 시작 포인터도 업데이트한다.
sHash의 빈도수를 줄여주고 sHash 빈도수가 tHash 보다 작으면
t 문자열을 모두 포함하고 있는 것이 아니므로 matches를 감소시킨다.
이 후 start 포인터를 이동시킨다.
Key Point는 matches === required 가 만족하면 반복해서 start 포인터를 이동시켜
window 크기의 최솟길이와 그 때의 부분 문자열 시작 포인터를 구하는 것이다.
3. 코드
var minWindow = function(s, t) {
let tHash = {};
let sHash = {};
let required = 0;
let start = 0, end = 0, minStart = 0, minLen = Infinity, matches = 0;
// t의 문자를 해시맵에 저장
for (let char of t) {
if (tHash[char]) {
tHash[char]++;
} else {
tHash[char] = 1;
required++; // 필요한 고유 문자 수 증가
}
}
// 슬라이딩 윈도우 탐색
while (end < s.length) {
let endChar = s[end];
// end 포인터가 가리키는 문자가 tHash에 있으면 카운팅
if (tHash[endChar]) {
if (!sHash[endChar]) sHash[endChar] = 0;
sHash[endChar]++;
// t에서 필요한 문자 수만큼 채운 경우 matches 증가
if (sHash[endChar] === tHash[endChar]) {
matches++;
}
}
// 모든 t의 문자가 포함된 경우
while (matches === required) {
// 최소 길이 업데이트
if (end - start + 1 < minLen) {
minLen = end - start + 1;
minStart = start;
}
let startChar = s[start];
// start 포인터가 가리키는 문자가 tHash에 있으면 카운팅 감소
if (tHash[startChar]) {
sHash[startChar]--;
if (sHash[startChar] < tHash[startChar]) {
matches--; // t의 문자가 더 이상 충분히 포함되지 않음
}
}
start++; // 윈도우를 최소화하기 위해 start 이동
}
end++; // 윈도우 확장을 위해 end 이동
}
// 결과 반환: 최소 윈도우 길이가 갱신된 적이 있으면 해당 부분 문자열, 아니면 빈 문자열
return minLen === Infinity ? "" : s.slice(minStart, minStart + minLen);
};
4. 복잡도 분석
- 시간복잡도: O(m), m은 s 문자열의 길이
- 공간복잡도: O(1)
sliding window, hash table 방법으로 시도했다. 하지만 내 방법으론 단순히 tHash, sHash의 빈도수 대소비교를 하여 최소의 window 길이를 구하기에는 적절하지 않았다. GPT에게 나의 코드를 질문하여 내 코드를 기반으로 최적의 코드를 얻어내었다.
window를 탐색 시에는 end 포인터는 window 크기 증가 즉, t의 모든 문자를 포함할 때 까지 증가시키는 방식으로 고정한다.
이 후 start 포인터를 움직여 window 크기를 최소화한다.
5. 이해하기 좀 더 쉬운 풀이 feat. Claude
Claude AI에게 물어보며 좀 더 이해하기 쉬운 코드로 풀어봤다.
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
// t의 문자별 필요한 개수를 저장
const need = {};
for (let char of t) {
need[char] = (need[char] || 0) + 1;
}
// 필요한 unique 문자의 개수
const requiredChars = Object.keys(need).length;
// 현재 윈도우에서 조건을 만족하는 문자의 개수
let matches = 0;
// 현재 윈도우의 문자 개수를 저장
const window = {};
// 결과 변수들
let left = 0; // s 문자열 시작 포인터
let right = 0; // s 문자열 끝 포인터
let minLen = Infinity;
let minLeft = 0; // 최소 부분 문자열 시작 포인터
while (right < s.length) {
const char = s[right];
window[char] = (window[char] || 0) + 1
// window에 꼭 필수적인 문자의 갯수가 동일할 때만 matches 추가
if (need[char] && window[char] === need[char]) {
matches++;
}
while (matches === requiredChars) {
// 현재 부분 window의 길이 갱신
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minLeft = left;
}
const leftChar = s[left];
window[leftChar]--;
if (need[leftChar] && window[leftChar] < need[leftChar]) {
matches--;
}
left++;
}
right++;
}
return minLen === Infinity ? '' : s.slice(minLeft, minLeft + minLen)
};
window를 hash로 구현하여 갯수를 담았다.
window의 시작 포인터, 종료 포인터 그리고 최소 부분 문자열의 시작 포인터를 정하여
t 문자열의 유니크한 문자의 갯수와 window에 포함된 유니크 갯수가 동일할 때,
window의 시작 포인터를 이동하면서 window의 최소 길이를 갱신해줬다.
기존에 풀던 방식이랑 얼추 비슷한데, 변수명을 정확히 설정하고 이해하니 더 가독성이 향상된 코드가 되었다.
'Leetcode' 카테고리의 다른 글
[Leetcode] 54. Spiral Matrix - JS (0) | 2024.10.10 |
---|---|
[Leetcode] 36. Valid Sudoku - JS (0) | 2024.10.09 |
[Leetcode] 30. Substring with Concatenation of All Words - JS (7) | 2024.10.07 |
[Leetcode] 3. Longest Substring Without Repeating Characters - JS (1) | 2024.10.06 |
[Leetcode] 209. Minimum Size Subarray Sum - JS (1) | 2024.10.05 |