LabHub

블로그

FAANG 코딩 인터뷰 완전 정복: 배열과 문자열 핵심 패턴

한국어English日本語

FAANG 코딩 인터뷰 완전 정복: 배열과 문자열 핵심 패턴

FAANG(Facebook/Meta, Apple, Amazon, Netflix, Google) 코딩 인터뷰에서 배열과 문자열 문제는 전체 출제 비중의 40% 이상을 차지합니다. 이 글에서는 실제 인터뷰에서 자주 등장하는 6가지 핵심 패턴을 실전 코드와 함께 완전히 정복합니다.


1. Two Pointers 패턴

개념 및 언제 사용하는가

Two Pointers 패턴은 정렬된 배열이나 연결 리스트에서 두 개의 포인터를 사용해 선형 시간에 문제를 해결하는 기법입니다.

사용 조건:

핵심 아이디어: O(n^2) 브루트 포스를 O(n)으로 줄일 수 있습니다.


문제 1: Two Sum II (LeetCode 167)

정렬된 배열에서 두 수의 합이 target이 되는 인덱스를 찾아라.

def twoSum(numbers: list[int], target: int) -> list[int]:
    left, right = 0, len(numbers) - 1

    while left < right:
        current_sum = numbers[left] + numbers[right]
        if current_sum == target:
            return [left + 1, right + 1]  # 1-indexed
        elif current_sum < target:
            left += 1
        else:
            right -= 1

    return []  # 문제 조건상 항상 해가 존재

# 예시
# Input: numbers = [2,7,11,15], target = 9
# Output: [1,2]

시간 복잡도: O(n) - 각 포인터가 최대 n번 이동 공간 복잡도: O(1) - 추가 공간 불필요

FAANG 팁: 정렬되지 않은 배열이라면 HashMap을 사용한 O(n) 풀이를 먼저 제시하고, 정렬된 경우 Two Pointers가 더 공간 효율적임을 설명하세요.


문제 2: 3Sum (LeetCode 15)

배열에서 합이 0인 세 원소를 모두 찾아라 (중복 없이).

def threeSum(nums: list[int]) -> list[list[int]]:
    nums.sort()
    result = []

    for i in range(len(nums) - 2):
        # 중복 건너뛰기
        if i > 0 and nums[i] == nums[i - 1]:
            continue

        left, right = i + 1, len(nums) - 1

        while left < right:
            total = nums[i] + nums[left] + nums[right]

            if total == 0:
                result.append([nums[i], nums[left], nums[right]])
                # 중복 건너뛰기
                while left < right and nums[left] == nums[left + 1]:
                    left += 1
                while left < right and nums[right] == nums[right - 1]:
                    right -= 1
                left += 1
                right -= 1
            elif total < 0:
                left += 1
            else:
                right -= 1

    return result

# 예시
# Input: nums = [-1, 0, 1, 2, -1, -4]
# Output: [[-1, -1, 2], [-1, 0, 1]]

시간 복잡도: O(n^2) - 정렬 O(n log n) + 이중 루프 O(n^2) 공간 복잡도: O(1) ~ O(n) (정렬 알고리즘에 따라)

FAANG 팁: Meta 인터뷰에서 자주 출제됩니다. 중복 처리 로직을 빠뜨리지 않도록 주의하세요.


문제 3: Container With Most Water (LeetCode 11)

높이 배열이 주어질 때 가장 많은 물을 담을 수 있는 컨테이너의 용량을 구하라.

def maxArea(height: list[int]) -> int:
    left, right = 0, len(height) - 1
    max_water = 0

    while left < right:
        # 현재 물의 양 = 너비 * 더 낮은 높이
        width = right - left
        current_water = width * min(height[left], height[right])
        max_water = max(max_water, current_water)

        # 더 낮은 쪽 포인터를 이동 (더 높은 값을 찾기 위해)
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1

    return max_water

# 예시
# Input: height = [1,8,6,2,5,4,8,3,7]
# Output: 49

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

왜 이 방식이 최적인가? 더 낮은 높이의 벽을 이동하는 이유는, 더 높은 벽을 이동하면 너비도 줄고 높이도 줄거나 같아서 절대 더 많은 물을 담을 수 없기 때문입니다.


문제 4: Trapping Rain Water (LeetCode 42)

고난도 문제로 빗물을 가두는 총량을 구하라.

def trap(height: list[int]) -> int:
    if not height:
        return 0

    left, right = 0, len(height) - 1
    left_max, right_max = height[left], height[right]
    water = 0

    while left < right:
        if left_max <= right_max:
            left += 1
            left_max = max(left_max, height[left])
            # 현재 위치에서 담을 수 있는 물 = 왼쪽 최대 - 현재 높이
            water += left_max - height[left]
        else:
            right -= 1
            right_max = max(right_max, height[right])
            water += right_max - height[right]

    return water

# 예시
# Input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
# Output: 6

시간 복잡도: O(n) 공간 복잡도: O(1) - prefix max 배열 없이 투 포인터로 해결

FAANG 팁: Google, Amazon 인터뷰 단골 문제입니다. DP 방식(O(n) space)도 알아두고, 투 포인터가 왜 더 효율적인지 설명할 수 있어야 합니다.


2. Sliding Window 패턴

개념

연속된 부분 배열/문자열에서 최적해를 찾는 패턴입니다. 윈도우를 슬라이딩하며 전체를 한 번만 순회합니다.

Fixed Window: 윈도우 크기가 고정된 경우 Variable Window: 조건에 따라 윈도우 크기가 변하는 경우


문제 5: Longest Substring Without Repeating Characters (LeetCode 3)

def lengthOfLongestSubstring(s: str) -> int:
    char_set = set()
    left = 0
    max_length = 0

    for right in range(len(s)):
        # 중복 문자가 있으면 왼쪽 포인터 이동
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1

        char_set.add(s[right])
        max_length = max(max_length, right - left + 1)

    return max_length

# 최적화 버전 (HashMap 사용)
def lengthOfLongestSubstringOptimized(s: str) -> int:
    char_index = {}
    left = 0
    max_length = 0

    for right, char in enumerate(s):
        if char in char_index and char_index[char] >= left:
            left = char_index[char] + 1

        char_index[char] = right
        max_length = max(max_length, right - left + 1)

    return max_length

# 예시
# Input: "abcabcbb"
# Output: 3 ("abc")

시간 복잡도: O(n) 공간 복잡도: O(min(n, charset_size))


문제 6: Minimum Window Substring (LeetCode 76)

문자열 s에서 t의 모든 문자를 포함하는 최소 윈도우를 찾아라.

from collections import Counter

def minWindow(s: str, t: str) -> str:
    if not t or not s:
        return ""

    need = Counter(t)
    missing = len(t)  # 아직 포함하지 못한 문자 수

    start = 0
    best_start = 0
    best_end = float('inf')

    left = 0
    for right, char in enumerate(s):
        # 필요한 문자라면 missing 감소
        if need[char] > 0:
            missing -= 1
        need[char] -= 1

        # 모든 문자를 포함했을 때 윈도우 최소화
        if missing == 0:
            # 왼쪽에서 불필요한 문자 제거
            while need[s[left]] < 0:
                need[s[left]] += 1
                left += 1

            # 현재 윈도우가 더 작으면 업데이트
            if right - left < best_end - best_start:
                best_start = left
                best_end = right

            # 윈도우 축소 (다음 더 짧은 윈도우 탐색)
            need[s[left]] += 1
            missing += 1
            left += 1

    if best_end == float('inf'):
        return ""
    return s[best_start:best_end + 1]

# 예시
# Input: s = "ADOBECODEBANC", t = "ABC"
# Output: "BANC"

시간 복잡도: O(|s| + |t|) 공간 복잡도: O(|s| + |t|)


문제 7: Sliding Window Maximum (LeetCode 239)

크기 k인 슬라이딩 윈도우에서 각 위치의 최대값을 구하라.

from collections import deque

def maxSlidingWindow(nums: list[int], k: int) -> list[int]:
    dq = deque()  # 인덱스를 저장 (값의 감소 순서 유지)
    result = []

    for i, num in enumerate(nums):
        # 윈도우 밖으로 나간 인덱스 제거
        if dq and dq[0] < i - k + 1:
            dq.popleft()

        # 현재 값보다 작은 원소는 유용하지 않으므로 제거
        while dq and nums[dq[-1]] < num:
            dq.pop()

        dq.append(i)

        # 윈도우가 완성되면 결과 추가
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

# 예시
# Input: nums = [1,3,-1,-3,5,3,6,7], k = 3
# Output: [3,3,5,5,6,7]

시간 복잡도: O(n) - 각 원소가 deque에 한 번씩 추가/삭제 공간 복잡도: O(k)


문제 8: Fruit Into Baskets (LeetCode 904)

최대 2종류의 과일만 담을 수 있는 바구니로 연속해서 따는 과일의 최대 개수를 구하라.

from collections import defaultdict

def totalFruit(fruits: list[int]) -> int:
    basket = defaultdict(int)
    left = 0
    max_fruits = 0

    for right, fruit in enumerate(fruits):
        basket[fruit] += 1

        # 3종류 이상이면 왼쪽 포인터 이동
        while len(basket) > 2:
            basket[fruits[left]] -= 1
            if basket[fruits[left]] == 0:
                del basket[fruits[left]]
            left += 1

        max_fruits = max(max_fruits, right - left + 1)

    return max_fruits

# 예시
# Input: fruits = [1,2,1,2,3]
# Output: 4 ([1,2,1,2])

시간 복잡도: O(n) 공간 복잡도: O(1) - 최대 3가지 과일 종류만 저장


3. Binary Search 패턴

개념

정렬된 배열에서 O(log n) 시간에 원소를 탐색합니다. 표준 형태 외에도 다양한 변형이 FAANG 인터뷰에 등장합니다.

표준 Binary Search 템플릿:

def binarySearch(nums: list[int], target: int) -> int:
    left, right = 0, len(nums) - 1

    while left <= right:
        mid = left + (right - left) // 2  # 오버플로우 방지

        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return -1

문제 9: Search in Rotated Sorted Array (LeetCode 33)

회전된 정렬 배열에서 타겟을 찾아라.

def search(nums: list[int], target: int) -> int:
    left, right = 0, len(nums) - 1

    while left <= right:
        mid = left + (right - left) // 2

        if nums[mid] == target:
            return mid

        # 왼쪽 절반이 정렬된 경우
        if nums[left] <= nums[mid]:
            if nums[left] <= target < nums[mid]:
                right = mid - 1
            else:
                left = mid + 1
        # 오른쪽 절반이 정렬된 경우
        else:
            if nums[mid] < target <= nums[right]:
                left = mid + 1
            else:
                right = mid - 1

    return -1

# 예시
# Input: nums = [4,5,6,7,0,1,2], target = 0
# Output: 4

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


문제 10: Find Minimum in Rotated Sorted Array (LeetCode 153)

def findMin(nums: list[int]) -> int:
    left, right = 0, len(nums) - 1

    while left < right:
        mid = left + (right - left) // 2

        # mid가 right보다 크면 최소값은 오른쪽 절반에 있음
        if nums[mid] > nums[right]:
            left = mid + 1
        else:
            right = mid

    return nums[left]

# 예시
# Input: nums = [3,4,5,1,2]
# Output: 1

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


문제 11: Median of Two Sorted Arrays (LeetCode 4) - 고난도

def findMedianSortedArrays(nums1: list[int], nums2: list[int]) -> float:
    # nums1이 더 짧은 배열이 되도록 보장
    if len(nums1) > len(nums2):
        nums1, nums2 = nums2, nums1

    m, n = len(nums1), len(nums2)
    half_len = (m + n + 1) // 2

    left, right = 0, m

    while left <= right:
        i = (left + right) // 2  # nums1의 파티션
        j = half_len - i          # nums2의 파티션

        # i가 너무 작으면 오른쪽으로
        if i < m and nums1[i] < nums2[j - 1]:
            left = i + 1
        # i가 너무 크면 왼쪽으로
        elif i > 0 and nums1[i - 1] > nums2[j]:
            right = i - 1
        else:
            # 올바른 파티션 찾음
            if i == 0:
                max_left = nums2[j - 1]
            elif j == 0:
                max_left = nums1[i - 1]
            else:
                max_left = max(nums1[i - 1], nums2[j - 1])

            if (m + n) % 2 == 1:
                return float(max_left)

            if i == m:
                min_right = nums2[j]
            elif j == n:
                min_right = nums1[i]
            else:
                min_right = min(nums1[i], nums2[j])

            return (max_left + min_right) / 2.0

# 예시
# Input: nums1 = [1,3], nums2 = [2]
# Output: 2.0

시간 복잡도: O(log(min(m, n))) 공간 복잡도: O(1)


문제 12: Search a 2D Matrix (LeetCode 74)

def searchMatrix(matrix: list[list[int]], target: int) -> bool:
    if not matrix or not matrix[0]:
        return False

    rows, cols = len(matrix), len(matrix[0])
    left, right = 0, rows * cols - 1

    while left <= right:
        mid = left + (right - left) // 2
        # 1D 인덱스를 2D 좌표로 변환
        mid_val = matrix[mid // cols][mid % cols]

        if mid_val == target:
            return True
        elif mid_val < target:
            left = mid + 1
        else:
            right = mid - 1

    return False

# 예시
# Input: matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
# Output: True

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


4. Prefix Sum & Hash Map 패턴

개념

Prefix Sum은 누적합을 미리 계산해 구간 합 쿼리를 O(1)에 처리합니다. Hash Map과 결합하면 특정 조건을 만족하는 구간을 효율적으로 찾을 수 있습니다.


문제 13: Subarray Sum Equals K (LeetCode 560)

합이 k인 연속 부분 배열의 개수를 구하라.

from collections import defaultdict

def subarraySum(nums: list[int], k: int) -> int:
    # prefix_sum_count[s] = prefix sum이 s인 경우의 수
    prefix_sum_count = defaultdict(int)
    prefix_sum_count[0] = 1  # 빈 배열의 prefix sum = 0

    current_sum = 0
    count = 0

    for num in nums:
        current_sum += num
        # current_sum - k가 이전에 나온 prefix sum이면 해당 구간의 합 = k
        count += prefix_sum_count[current_sum - k]
        prefix_sum_count[current_sum] += 1

    return count

# 예시
# Input: nums = [1,1,1], k = 2
# Output: 2

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


문제 14: Longest Consecutive Sequence (LeetCode 128)

정렬되지 않은 배열에서 가장 긴 연속 수열의 길이를 구하라 (O(n) 요구).

def longestConsecutive(nums: list[int]) -> int:
    num_set = set(nums)
    max_length = 0

    for num in num_set:
        # 수열의 시작점인 경우만 처리 (num-1이 없는 경우)
        if num - 1 not in num_set:
            current = num
            length = 1

            while current + 1 in num_set:
                current += 1
                length += 1

            max_length = max(max_length, length)

    return max_length

# 예시
# Input: nums = [100,4,200,1,3,2]
# Output: 4 ([1,2,3,4])

시간 복잡도: O(n) - 각 원소는 최대 두 번 방문 공간 복잡도: O(n)


문제 15: Group Anagrams (LeetCode 49)

문자열 배열을 애너그램끼리 묶어라.

from collections import defaultdict

def groupAnagrams(strs: list[str]) -> list[list[str]]:
    anagram_map = defaultdict(list)

    for s in strs:
        # 정렬된 문자열을 키로 사용
        key = tuple(sorted(s))
        anagram_map[key].append(s)

    return list(anagram_map.values())

# 카운터를 키로 사용하는 최적화 버전
def groupAnagramsOptimized(strs: list[str]) -> list[list[str]]:
    anagram_map = defaultdict(list)

    for s in strs:
        # 26개 알파벳 카운트를 키로 사용
        count = [0] * 26
        for c in s:
            count[ord(c) - ord('a')] += 1
        anagram_map[tuple(count)].append(s)

    return list(anagram_map.values())

# 예시
# Input: strs = ["eat","tea","tan","ate","nat","bat"]
# Output: [["bat"],["nat","tan"],["ate","eat","tea"]]

시간 복잡도: O(n _ k) - n은 문자열 수, k는 최대 문자열 길이 공간 복잡도: O(n _ k)


문제 16: Top K Frequent Elements (LeetCode 347)

from collections import Counter
import heapq

def topKFrequent(nums: list[int], k: int) -> list[int]:
    # 방법 1: 힙 사용 O(n log k)
    count = Counter(nums)
    return heapq.nlargest(k, count.keys(), key=count.get)

def topKFrequentBucketSort(nums: list[int], k: int) -> list[int]:
    # 방법 2: 버킷 정렬 O(n)
    count = Counter(nums)

    # 빈도를 인덱스로 사용하는 버킷
    bucket = [[] for _ in range(len(nums) + 1)]
    for num, freq in count.items():
        bucket[freq].append(num)

    result = []
    for freq in range(len(bucket) - 1, 0, -1):
        for num in bucket[freq]:
            result.append(num)
            if len(result) == k:
                return result

    return result

# 예시
# Input: nums = [1,1,1,2,2,3], k = 2
# Output: [1,2]

시간 복잡도: O(n) with Bucket Sort 공간 복잡도: O(n)


5. FAANG 기업별 배열/문자열 출제 경향

Google

Google 인터뷰는 Arrays와 String Manipulation에 집중합니다.

Meta (Facebook)

Meta는 Sliding Window와 Two Pointer 패턴을 선호합니다.

Amazon

Amazon은 Hash Table과 Sorting 기반 문제를 선호합니다.

Microsoft

Microsoft는 Binary Search 변형과 배열 조작 문제를 자주 냅니다.


6. 인터뷰 실전 팁

문제 접근 프레임워크 (45분 기준)

1단계 - Clarify (5분)

면접관에게 반드시 확인할 사항들:

2단계 - Brute Force (5분)

먼저 O(n^2) 또는 O(n^3) 브루트 포스를 설명합니다. 이것이 베이스라인이 됩니다.

3단계 - Optimize (10분)

패턴 인식: Two Pointers? Sliding Window? Hash Map? 면접관과 트레이드오프를 논의합니다.

4단계 - Code (15분)

깔끔하게 코딩합니다. 변수명은 명확하게, 주석은 핵심만:

def solve(nums, target):
    # 투 포인터 초기화
    left, right = 0, len(nums) - 1

    while left < right:
        # ... 로직
        pass

5단계 - Test (10분)

직접 테스트 케이스를 만들어 검증합니다:


엣지 케이스 체크리스트

배열/문자열 문제에서 반드시 확인:


시간 분배 전략

단계시간핵심 포인트
Clarify5분면접관에게 질문하며 요구사항 확인
Brute Force5분단순한 해법 먼저 제시
Optimize10분패턴 파악, 복잡도 개선
Coding15분깔끔하고 버그 없이
Testing10분직접 트레이스, 엣지 케이스

7. 연습 문제 퀴즈

퀴즈 1: Two Pointers로 정렬된 배열에서 중복 제거하기

정렬된 배열 nums = [0,0,1,1,1,2,2,3,3,4]에서 in-place로 중복을 제거하고 고유 원소의 수를 반환하라. 추가 공간은 O(1)만 사용 가능.

정답: 5 (배열은 [0,1,2,3,4,...])

설명: Two Pointers를 사용합니다. 느린 포인터(slow)는 다음에 쓸 위치를, 빠른 포인터(fast)는 탐색 위치를 가리킵니다. nums[fast] != nums[slow]이면 slow를 증가시키고 값을 복사합니다.

def removeDuplicates(nums):
    if not nums:
        return 0
    slow = 0
    for fast in range(1, len(nums)):
        if nums[fast] != nums[slow]:
            slow += 1
            nums[slow] = nums[fast]
    return slow + 1
퀴즈 2: Sliding Window로 최대 연속 1의 개수 구하기

이진 배열 nums = [1,1,0,0,1,1,1,0,1]에서 최대 k=1개의 0을 1로 바꿀 수 있을 때 가장 긴 연속 1의 부분 배열 길이를 구하라.

정답: 6

설명: Variable Sliding Window를 사용합니다. 윈도우 내 0의 개수가 k를 초과하면 왼쪽 포인터를 이동합니다.

def longestOnes(nums, k):
    left = 0
    zeros = 0
    max_len = 0
    for right in range(len(nums)):
        if nums[right] == 0:
            zeros += 1
        while zeros > k:
            if nums[left] == 0:
                zeros -= 1
            left += 1
        max_len = max(max_len, right - left + 1)
    return max_len
퀴즈 3: Binary Search 응용 - 경계값 찾기

정렬된 배열 nums = [5,7,7,8,8,10]에서 target=8의 시작 인덱스와 끝 인덱스를 구하라.

정답: [3, 4]

설명: Binary Search를 두 번 실행합니다. 첫 번째는 가장 왼쪽 인덱스(leftmost), 두 번째는 가장 오른쪽 인덱스(rightmost)를 찾습니다.

def searchRange(nums, target):
    def findLeft(nums, target):
        left, right = 0, len(nums) - 1
        idx = -1
        while left <= right:
            mid = (left + right) // 2
            if nums[mid] == target:
                idx = mid
                right = mid - 1  # 왼쪽으로 계속 탐색
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return idx

    def findRight(nums, target):
        left, right = 0, len(nums) - 1
        idx = -1
        while left <= right:
            mid = (left + right) // 2
            if nums[mid] == target:
                idx = mid
                left = mid + 1  # 오른쪽으로 계속 탐색
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return idx

    return [findLeft(nums, target), findRight(nums, target)]
퀴즈 4: Prefix Sum으로 짝수 길이 구간의 합이 같은 경우 세기

배열 [1, -1, 1, -1, 1]에서 합이 0인 연속 부분 배열의 개수는?

정답: 4

설명: Prefix Sum + HashMap을 사용합니다. prefix sum이 같은 경우가 m번 나타나면 C(m,2)개의 구간이 합이 0입니다. prefix_sum_count를 이용해 count += prefix_sum_count[current_sum]으로 계산합니다.

from collections import defaultdict
def subarraySum(nums, k=0):
    prefix_sum_count = defaultdict(int)
    prefix_sum_count[0] = 1
    current_sum = 0
    count = 0
    for num in nums:
        current_sum += num
        count += prefix_sum_count[current_sum - k]
        prefix_sum_count[current_sum] += 1
    return count
# 결과: 4
퀴즈 5: 종합 문제 - Product of Array Except Self

배열 nums = [1,2,3,4]에서 각 위치에 대해 자기 자신을 제외한 나머지 원소의 곱을 구하라. 나눗셈 사용 금지, O(n) 시간, O(1) 추가 공간.

정답: [24, 12, 8, 6]

설명: 왼쪽 누적곱(prefix product)과 오른쪽 누적곱(suffix product)을 활용합니다. 출력 배열 자체를 재활용해 공간을 절약합니다.

def productExceptSelf(nums):
    n = len(nums)
    result = [1] * n

    # 왼쪽 누적곱
    prefix = 1
    for i in range(n):
        result[i] = prefix
        prefix *= nums[i]

    # 오른쪽 누적곱을 곱하기
    suffix = 1
    for i in range(n - 1, -1, -1):
        result[i] *= suffix
        suffix *= nums[i]

    return result

마무리

배열과 문자열 문제는 FAANG 인터뷰의 핵심입니다. 이 글에서 다룬 패턴들을 완전히 내재화하면 대부분의 문제를 빠르게 인식하고 최적 해법으로 접근할 수 있습니다.

다음 학습 단계:

  1. LeetCode에서 각 패턴별 문제 20개씩 풀기
  2. 시간을 재며 45분 안에 해결하는 연습
  3. 코드 리뷰: 다른 사람의 풀이와 비교
  4. 면접관 역할로 직접 문제 출제해보기

다음 편에서는 트리와 그래프 패턴을 다룹니다.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다