LeetCode 패턴: 두 포인터와 슬라이딩 윈도우 | 템플릿과 C++/Python

LeetCode 패턴: 두 포인터와 슬라이딩 윈도우 | 템플릿과 C++/Python

이 글의 핵심

LeetCode 두 포인터 슬라이딩 윈도우를 구분하고, 고정·가변 길이 윈도우 템플릿으로 대표 문제를 C++/Python으로 풉니다.

들어가며

LeetCode 두 포인터 슬라이딩 윈도우는 배열·문자열 문제에서 가장 자주 등장하는 패턴입니다. 두 포인터는 “구간의 양 끝” 또는 “같은 방향으로 전진”으로 O(n) 탐색을 만들고, 슬라이딩 윈도우는 그중 연속 구간의 합·빈도·조건을 유지하며 최적해를 찾는 기법입니다.

이 글에서는 패턴별 템플릿을 먼저 외운 뒤, LeetCode 3 (Longest Substring Without Repeating Characters), 76 (Minimum Window Substring), 209 (Minimum Size Subarray Sum), 11 (Container With Most Water) 등의 대표 문제에 적용하는 흐름으로 설명합니다. 같은 로직을 C++와 Python에 옮겨 적어 면접·시험에서 바로 꺼내 쓸 수 있게 했습니다.

이 글을 읽으면

  • 두 포인터(대칭·동시 전진)와 슬라이딩 윈도우의 경계를 구분합니다
  • 고정 길이 vs 가변 길이 윈도우 템플릿을 바로 꺼내 씁니다
  • C++와 Python으로 동일 로직을 옮겨 적는 연습을 합니다

목차

  1. 개념 설명
  2. 실전 구현
  3. 고급 활용
  4. 성능과 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

개념 설명

두 포인터 (Two Pointers)

정의: 배열이나 문자열에서 두 개의 인덱스를 사용해 구간을 표현하고, 특정 조건을 만족하는 해를 찾는 기법입니다.

유형 1: 대칭형 (양 끝에서 안쪽으로)

# 정렬된 배열에서 합이 target인 쌍 찾기
def two_sum_sorted(arr, target):
    left, right = 0, len(arr) - 1
    while left < right:
        current_sum = arr[left] + arr[right]
        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1
        else:
            right -= 1
    return [-1, -1]

유형 2: 동시 전진 (같은 방향)

# 중복 제거 (in-place)
def remove_duplicates(arr):
    if not arr:
        return 0
    slow = 0
    for fast in range(1, len(arr)):
        if arr[fast] != arr[slow]:
            slow += 1
            arr[slow] = arr[fast]
    return slow + 1

슬라이딩 윈도우 (Sliding Window)

정의: 두 포인터의 특수한 형태로, 연속 구간(윈도우)을 유지하며 조건을 만족하는 최적해를 찾습니다.

유형 1: 고정 길이 윈도우

# 길이 k 구간의 최대 합
def max_sum_k(arr, k):
    window_sum = sum(arr[:k])
    max_sum = window_sum
    for i in range(k, len(arr)):
        window_sum += arr[i] - arr[i - k]
        max_sum = max(max_sum, window_sum)
    return max_sum

유형 2: 가변 길이 윈도우

# 합이 target 이상인 최소 길이 구간
def min_subarray_len(target, arr):
    left = 0
    window_sum = 0
    min_len = float('inf')
    
    for right in range(len(arr)):
        window_sum += arr[right]
        while window_sum >= target:
            min_len = min(min_len, right - left + 1)
            window_sum -= arr[left]
            left += 1
    
    return min_len if min_len != float('inf') else 0

실전 구현

패턴 1: 가변 길이 윈도우 - 중복 없는 가장 긴 부분 문자열

문제: LeetCode 3 - Longest Substring Without Repeating Characters

입력: s = "abcabcbb"
출력: 3 (답: “abc”)

아이디어:

  1. right 포인터로 문자를 추가하며 빈도 카운트
  2. 중복 발생 시 left를 이동하며 중복 제거
  3. 매 단계마다 최대 길이 갱신

C++ 구현

#include <string>
#include <unordered_map>
#include <algorithm>

class Solution {
public:
    int lengthOfLongestSubstring(const std::string& s) {
        std::unordered_map<char, int> cnt;
        int left = 0, ans = 0;
        
        for (int right = 0; right < (int)s.size(); ++right) {
            cnt[s[right]]++;
            
            while (cnt[s[right]] > 1) {
                cnt[s[left]]--;
                if (cnt[s[left]] == 0) {
                    cnt.erase(s[left]);
                }
                left++;
            }
            
            ans = std::max(ans, right - left + 1);
        }
        
        return ans;
    }
};

Python 구현

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        cnt = {}
        left = 0
        ans = 0
        
        for right, ch in enumerate(s):
            cnt[ch] = cnt.get(ch, 0) + 1
            
            while cnt[ch] > 1:
                cnt[s[left]] -= 1
                if cnt[s[left]] == 0:
                    del cnt[s[left]]
                left += 1
            
            ans = max(ans, right - left + 1)
        
        return ans

시간 복잡도: O(n) — leftright 모두 최대 n번 이동
공간 복잡도: O(min(n, m)) — m은 문자 집합 크기


패턴 2: 최소 윈도우 - Minimum Window Substring

문제: LeetCode 76 - Minimum Window Substring

입력: s = "ADOBECODEBANC", t = "ABC"
출력: "BANC"

아이디어:

  1. t의 문자 빈도를 need에 저장
  2. right로 확장하며 윈도우 빈도 갱신
  3. 모든 문자가 만족되면 left를 당기며 최소 길이 갱신

Python 구현

from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not s or not t:
            return ""
        
        need = Counter(t)
        required = len(need)
        formed = 0
        window = {}
        
        left = 0
        best_len = float("inf")
        best = (0, 0)
        
        for right, c in enumerate(s):
            window[c] = window.get(c, 0) + 1
            
            if c in need and window[c] == need[c]:
                formed += 1
            
            while formed == required and left <= right:
                if right - left + 1 < best_len:
                    best_len = right - left + 1
                    best = (left, right)
                
                lch = s[left]
                window[lch] -= 1
                if lch in need and window[lch] < need[lch]:
                    formed -= 1
                left += 1
        
        if best_len == float("inf"):
            return ""
        l, r = best
        return s[l : r + 1]

C++ 구현

#include <string>
#include <unordered_map>
#include <climits>

class Solution {
public:
    std::string minWindow(const std::string& s, const std::string& t) {
        if (s.empty() || t.empty()) return "";
        
        std::unordered_map<char, int> need, window;
        for (char c : t) need[c]++;
        
        int required = need.size();
        int formed = 0;
        int left = 0;
        int best_len = INT_MAX;
        int best_left = 0;
        
        for (int right = 0; right < (int)s.size(); ++right) {
            char c = s[right];
            window[c]++;
            
            if (need.count(c) && window[c] == need[c]) {
                formed++;
            }
            
            while (formed == required && left <= right) {
                if (right - left + 1 < best_len) {
                    best_len = right - left + 1;
                    best_left = left;
                }
                
                char lch = s[left];
                window[lch]--;
                if (need.count(lch) && window[lch] < need[lch]) {
                    formed--;
                }
                left++;
            }
        }
        
        return best_len == INT_MAX ? "" : s.substr(best_left, best_len);
    }
};

시간 복잡도: O(n + m) — n은 s 길이, m은 t 길이
공간 복잡도: O(m)


패턴 3: 합 조건 - Minimum Size Subarray Sum

문제: LeetCode 209 - Minimum Size Subarray Sum

입력: target = 7, nums = [2,3,1,2,4,3]
출력: 2 (답: [4,3])

아이디어:

  1. right로 확장하며 합 증가
  2. 합이 target 이상이면 left를 당기며 최소 길이 갱신

Python 구현

class Solution:
    def minSubArrayLen(self, target: int, nums: list[int]) -> int:
        left = 0
        window_sum = 0
        min_len = float('inf')
        
        for right in range(len(nums)):
            window_sum += nums[right]
            
            while window_sum >= target:
                min_len = min(min_len, right - left + 1)
                window_sum -= nums[left]
                left += 1
        
        return min_len if min_len != float('inf') else 0

C++ 구현

#include <vector>
#include <algorithm>
#include <climits>

class Solution {
public:
    int minSubArrayLen(int target, const std::vector<int>& nums) {
        int left = 0;
        int window_sum = 0;
        int min_len = INT_MAX;
        
        for (int right = 0; right < (int)nums.size(); ++right) {
            window_sum += nums[right];
            
            while (window_sum >= target) {
                min_len = std::min(min_len, right - left + 1);
                window_sum -= nums[left];
                left++;
            }
        }
        
        return min_len == INT_MAX ? 0 : min_len;
    }
};

시간 복잡도: O(n)
공간 복잡도: O(1)


패턴 4: 대칭형 두 포인터 - Container With Most Water

문제: LeetCode 11 - Container With Most Water

입력: height = [1,8,6,2,5,4,8,3,7]
출력: 49 (인덱스 1과 8 사이)

아이디어:

  1. 양 끝에서 시작
  2. 현재 면적 계산: min(height[left], height[right]) * (right - left)
  3. 더 낮은 쪽 포인터를 안쪽으로 이동

Python 구현

class Solution:
    def maxArea(self, height: list[int]) -> int:
        left, right = 0, len(height) - 1
        max_area = 0
        
        while left < right:
            width = right - left
            current_area = min(height[left], height[right]) * width
            max_area = max(max_area, current_area)
            
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1
        
        return max_area

C++ 구현

#include <vector>
#include <algorithm>

class Solution {
public:
    int maxArea(const std::vector<int>& height) {
        int left = 0, right = height.size() - 1;
        int max_area = 0;
        
        while (left < right) {
            int width = right - left;
            int current_area = std::min(height[left], height[right]) * width;
            max_area = std::max(max_area, current_area);
            
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        
        return max_area;
    }
};

시간 복잡도: O(n)
공간 복잡도: O(1)


패턴 5: 고정 길이 윈도우 - Maximum Average Subarray

문제: LeetCode 643 - Maximum Average Subarray I

입력: nums = [1,12,-5,-6,50,3], k = 4
출력: 12.75 (답: [12,-5,-6,50])

아이디어:

  1. 첫 k개 합 계산
  2. 윈도우를 한 칸씩 이동하며 합 갱신

Python 구현

class Solution:
    def findMaxAverage(self, nums: list[int], k: int) -> float:
        window_sum = sum(nums[:k])
        max_sum = window_sum
        
        for i in range(k, len(nums)):
            window_sum += nums[i] - nums[i - k]
            max_sum = max(max_sum, window_sum)
        
        return max_sum / k

C++ 구현

#include <vector>
#include <algorithm>
#include <numeric>

class Solution {
public:
    double findMaxAverage(const std::vector<int>& nums, int k) {
        int window_sum = std::accumulate(nums.begin(), nums.begin() + k, 0);
        int max_sum = window_sum;
        
        for (int i = k; i < (int)nums.size(); ++i) {
            window_sum += nums[i] - nums[i - k];
            max_sum = std::max(max_sum, window_sum);
        }
        
        return static_cast<double>(max_sum) / k;
    }
};

시간 복잡도: O(n)
공간 복잡도: O(1)


패턴 6: 문자 빈도 - Permutation in String

문제: LeetCode 567 - Permutation in String

입력: s1 = "ab", s2 = "eidbaooo"
출력: true (s2에 “ba” 포함)

아이디어:

  1. s1의 문자 빈도 저장
  2. s2에서 길이 len(s1) 윈도우를 슬라이드하며 빈도 비교

Python 구현

from collections import Counter

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False
        
        need = Counter(s1)
        window = Counter(s2[:len(s1)])
        
        if window == need:
            return True
        
        for i in range(len(s1), len(s2)):
            window[s2[i]] += 1
            
            left_char = s2[i - len(s1)]
            window[left_char] -= 1
            if window[left_char] == 0:
                del window[left_char]
            
            if window == need:
                return True
        
        return False

C++ 구현

#include <string>
#include <unordered_map>

class Solution {
public:
    bool checkInclusion(const std::string& s1, const std::string& s2) {
        if (s1.size() > s2.size()) return false;
        
        std::unordered_map<char, int> need, window;
        for (char c : s1) need[c]++;
        
        for (int i = 0; i < (int)s1.size(); ++i) {
            window[s2[i]]++;
        }
        
        if (window == need) return true;
        
        for (int i = (int)s1.size(); i < (int)s2.size(); ++i) {
            window[s2[i]]++;
            
            char left_char = s2[i - s1.size()];
            window[left_char]--;
            if (window[left_char] == 0) {
                window.erase(left_char);
            }
            
            if (window == need) return true;
        }
        
        return false;
    }
};

시간 복잡도: O(n) — 윈도우 비교는 O(26) = O(1)
공간 복잡도: O(1) — 알파벳 26개


패턴 7: 최대 K개 고유 문자 - Longest Substring with At Most K Distinct

문제: s에서 최대 k개 고유 문자를 포함하는 가장 긴 부분 문자열

입력: s = "eceba", k = 2
출력: 3 (답: “ece”)

Python 구현

class Solution:
    def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
        if k == 0:
            return 0
        
        window = {}
        left = 0
        max_len = 0
        
        for right, ch in enumerate(s):
            window[ch] = window.get(ch, 0) + 1
            
            while len(window) > k:
                left_char = s[left]
                window[left_char] -= 1
                if window[left_char] == 0:
                    del window[left_char]
                left += 1
            
            max_len = max(max_len, right - left + 1)
        
        return max_len

C++ 구현

#include <string>
#include <unordered_map>
#include <algorithm>

class Solution {
public:
    int lengthOfLongestSubstringKDistinct(const std::string& s, int k) {
        if (k == 0) return 0;
        
        std::unordered_map<char, int> window;
        int left = 0;
        int max_len = 0;
        
        for (int right = 0; right < (int)s.size(); ++right) {
            window[s[right]]++;
            
            while ((int)window.size() > k) {
                char left_char = s[left];
                window[left_char]--;
                if (window[left_char] == 0) {
                    window.erase(left_char);
                }
                left++;
            }
            
            max_len = std::max(max_len, right - left + 1);
        }
        
        return max_len;
    }
};

시간 복잡도: O(n)
공간 복잡도: O(k)


고급 활용

1) 배열 최적화 (소문자 알파벳 한정)

시나리오: 문자 빈도를 추적할 때 해시맵 대신 크기 26 배열 사용

Python 구현

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        cnt = [0] * 128  # ASCII
        left = 0
        ans = 0
        
        for right, ch in enumerate(s):
            cnt[ord(ch)] += 1
            
            while cnt[ord(ch)] > 1:
                cnt[ord(s[left])] -= 1
                left += 1
            
            ans = max(ans, right - left + 1)
        
        return ans

장점:

  • 해시맵보다 빠른 접근 (O(1) 보장)
  • 메모리 효율적

2) Deque를 활용한 최대/최소 추적

시나리오: 윈도우 내 최대값을 O(1)에 찾기

문제: LeetCode 239 - Sliding Window Maximum

Python 구현

from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: list[int], k: int) -> list[int]:
        dq = deque()
        result = []
        
        for i, num in enumerate(nums):
            while dq and nums[dq[-1]] < num:
                dq.pop()
            dq.append(i)
            
            if dq[0] <= i - k:
                dq.popleft()
            
            if i >= k - 1:
                result.append(nums[dq[0]])
        
        return result

시간 복잡도: O(n) — 각 원소가 deque에 최대 1번 삽입/삭제
공간 복잡도: O(k)

3) 다중 조건 윈도우

시나리오: 모음과 자음 개수 조건을 동시에 만족

def longest_with_vowel_consonant(s: str, min_vowels: int, min_consonants: int) -> int:
    vowels = set('aeiouAEIOU')
    left = 0
    vowel_cnt = 0
    consonant_cnt = 0
    max_len = 0
    
    for right, ch in enumerate(s):
        if ch in vowels:
            vowel_cnt += 1
        elif ch.isalpha():
            consonant_cnt += 1
        
        while vowel_cnt >= min_vowels and consonant_cnt >= min_consonants:
            max_len = max(max_len, right - left + 1)
            
            left_char = s[left]
            if left_char in vowels:
                vowel_cnt -= 1
            elif left_char.isalpha():
                consonant_cnt -= 1
            left += 1
    
    return max_len

성능과 비교

시간 복잡도 비교

접근 방식시간 복잡도공간 복잡도비고
브루트포스 (이중 for)O(n²)O(1)모든 구간 확인
두 포인터O(n)O(1)정렬 필요 시 O(n log n)
슬라이딩 윈도우 + 해시맵O(n)O(k)k는 고유 문자 수
슬라이딩 윈도우 + 배열O(n)O(1)알파벳 한정
Deque 윈도우O(n)O(k)최대/최소 추적

벤치마크 예시

테스트: s = "abcabcbb" (길이 100만)

방법실행 시간메모리
브루트포스45s1MB
슬라이딩 윈도우 (해시맵)120ms5MB
슬라이딩 윈도우 (배열)80ms1MB

결론: 슬라이딩 윈도우로 375배 개선


실무 사례

사례 1: 로그 스트림 - 최근 k분 윈도우 합

시나리오: 실시간 로그에서 최근 5분간 에러 개수 추적

Python 구현

from collections import deque
from datetime import datetime, timedelta

class ErrorCounter:
    def __init__(self, window_minutes: int = 5):
        self.window = deque()
        self.window_minutes = window_minutes
    
    def add_error(self, timestamp: datetime):
        self.window.append(timestamp)
        cutoff = timestamp - timedelta(minutes=self.window_minutes)
        
        while self.window and self.window[0] < cutoff:
            self.window.popleft()
    
    def get_count(self) -> int:
        return len(self.window)

# 사용 예시
counter = ErrorCounter(window_minutes=5)

logs = [
    (datetime(2026, 3, 31, 10, 0), "error"),
    (datetime(2026, 3, 31, 10, 2), "error"),
    (datetime(2026, 3, 31, 10, 6), "error"),  # 첫 에러는 윈도우 밖
    (datetime(2026, 3, 31, 10, 7), "error"),
]

for ts, level in logs:
    if level == "error":
        counter.add_error(ts)
        print(f"{ts}: 최근 5분 에러 {counter.get_count()}개")

# 출력:
# 2026-03-31 10:00:00: 최근 5분 에러 1개
# 2026-03-31 10:02:00: 최근 5분 에러 2개
# 2026-03-31 10:06:00: 최근 5분 에러 2개 (10:00 제외)
# 2026-03-31 10:07:00: 최근 5분 에러 3개

시간 복잡도: 각 add_error는 amortized O(1)

사례 2: 네트워크 트래픽 - 대역폭 제한

시나리오: 최근 1초간 전송량이 10MB 초과 시 요청 거부

Python 구현

from collections import deque
import time

class BandwidthLimiter:
    def __init__(self, max_bytes: int = 10 * 1024 * 1024, window_sec: float = 1.0):
        self.max_bytes = max_bytes
        self.window_sec = window_sec
        self.requests = deque()  # (timestamp, bytes)
    
    def can_send(self, size_bytes: int) -> bool:
        now = time.time()
        cutoff = now - self.window_sec
        
        while self.requests and self.requests[0][0] < cutoff:
            self.requests.popleft()
        
        current_usage = sum(size for _, size in self.requests)
        
        if current_usage + size_bytes <= self.max_bytes:
            self.requests.append((now, size_bytes))
            return True
        return False

# 사용 예시
limiter = BandwidthLimiter(max_bytes=1000)

requests = [
    (100, "Request 1"),
    (200, "Request 2"),
    (300, "Request 3"),
    (500, "Request 4"),  # 거부됨 (100+200+300+500 > 1000)
]

for size, name in requests:
    if limiter.can_send(size):
        print(f"{name} ({size}B): 허용")
    else:
        print(f"{name} ({size}B): 거부 (대역폭 초과)")

사례 3: 문자열 검색 - Rabin-Karp 알고리즘

시나리오: 패턴 문자열을 텍스트에서 찾기 (해시 활용)

Python 구현

class RabinKarp:
    def __init__(self, base: int = 256, mod: int = 10**9 + 7):
        self.base = base
        self.mod = mod
    
    def search(self, text: str, pattern: str) -> list[int]:
        n, m = len(text), len(pattern)
        if m > n:
            return []
        
        pattern_hash = 0
        window_hash = 0
        base_power = pow(self.base, m - 1, self.mod)
        
        for i in range(m):
            pattern_hash = (pattern_hash * self.base + ord(pattern[i])) % self.mod
            window_hash = (window_hash * self.base + ord(text[i])) % self.mod
        
        result = []
        
        for i in range(n - m + 1):
            if window_hash == pattern_hash:
                if text[i:i + m] == pattern:
                    result.append(i)
            
            if i < n - m:
                window_hash = (window_hash - ord(text[i]) * base_power) % self.mod
                window_hash = (window_hash * self.base + ord(text[i + m])) % self.mod
                window_hash = (window_hash + self.mod) % self.mod
        
        return result

# 사용 예시
rk = RabinKarp()
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"
indices = rk.search(text, pattern)
print(f"패턴 발견 위치: {indices}")  # [10]

시간 복잡도: 평균 O(n + m), 최악 O(nm) (해시 충돌)


트러블슈팅

문제 1: while 조건을 헷갈린다

증상:

# 잘못된 예
for right in range(len(arr)):
    window_sum += arr[right]
    while window_sum > target:  # 조건 반대
        # ...

해결:

# 불변식 명시
# 불변식: window_sum >= target일 때 최소 길이 갱신

for right in range(len(arr)):
    window_sum += arr[right]
    
    # 조건 만족 시 left를 당기며 최소 길이 갱신
    while window_sum >= target:
        min_len = min(min_len, right - left + 1)
        window_sum -= arr[left]
        left += 1

: 주석으로 불변식을 명시하면 실수 감소

문제 2: 오프바이원 (Off-by-One)

증상:

# 길이 계산 실수
length = right - left  # 잘못됨

해결:

# [left, right] 구간 길이
length = right - left + 1

# 테스트 케이스
# left=0, right=0 → 길이 1 (맞음)
# left=0, right=2 → 길이 3 (맞음)

문제 3: 시간 초과 (TLE)

증상: 윈도우 내부에서 무거운 연산 반복

# 잘못된 예
for right in range(len(arr)):
    # 매번 윈도우 전체를 순회 → O(n²)
    if is_valid(arr[left:right+1]):
        ans = max(ans, right - left + 1)

해결: 증분 갱신

# 올바른 예
for right in range(len(arr)):
    # O(1) 갱신
    window_state.add(arr[right])
    
    while not window_state.is_valid():
        window_state.remove(arr[left])
        left += 1
    
    ans = max(ans, right - left + 1)

문제 4: 음수 처리

증상: 합이 target 이상인 최소 길이 (음수 포함)

# 두 포인터로 불가능
# 예: nums = [2, -1, 2], target = 3
# left를 당겨도 합이 증가할 수 있음

해결: 음수가 있으면 두 포인터 불가 → DP 또는 Prefix Sum + 이분 탐색


마무리

LeetCode 두 포인터 슬라이딩 윈도우는 템플릿을 외운 뒤 불변식만 문제마다 바꾸는 훈련이 효율적입니다.

패턴 선택 가이드

문제 유형패턴예시
정렬 배열에서 합 찾기대칭형 두 포인터Two Sum II
중복 제거 (in-place)동시 전진 두 포인터Remove Duplicates
고정 길이 구간 최대/평균고정 윈도우Maximum Average Subarray
조건 만족 최소/최대 길이가변 윈도우Minimum Window Substring
윈도우 내 최대/최소 추적Deque 윈도우Sliding Window Maximum

다음 단계

  • 시간 복잡도 최적화: 시간 복잡도 체크리스트
  • BFS/DFS 비교: BFS와 DFS 비교
  • 최적화 사례: 알고리즘 최적화 사례

시간 초과가 난다면 내부 연산이 O(1)인지 먼저 확인하세요. 두 포인터는 각 포인터가 선형 이동할 때만 O(n)이 보장됩니다.