DSA Interview Prep · 강의

문자열 인코딩, 뒤집기, 회문

제자리 단어 뒤집기와 런 길이 인코딩을 구현하고, 중심 확장 기법을 포함한 회문 판별을 학습합니다.

레슨 4/413개 단계

문자열 인코딩, 뒤집기, 회문은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

문자열 제자리 뒤집기

파이썬 문자열은 변경할 수 없으므로 '제자리' 뒤집기란 문자를 리스트로 변환하고, 두 포인터로 서로 바꾼 다음, join을 수행하는 것을 의미합니다. 고전적인 두 포인터 교환 방식에서는 left를 인덱스 0에, right를 마지막 인덱스에 둡니다. 문자를 교환하고 포인터를 안쪽으로 이동하다가 두 포인터가 교차하면 중단합니다. 문자 리스트에 필요한 공간을 포함해 시간 복잡도는 O(n), 공간 복잡도는 O(n)입니다(문자열이 변경 불가능하므로 줄일 수 없습니다).

def reverse_string(s):
    chars = list(s)
    left, right = 0, len(chars) - 1
    while left < right:
        chars[left], chars[right] = chars[right], chars[left]
        left  += 1
        right -= 1
    return ''.join(chars)

print(reverse_string('hello'))   # 'olleh'
print(reverse_string('Hannah'))  # 'hannaH'

# Pythonic shortcut (creates new string):
print('hello'[::-1])  # 'olleh'

문장에서 단어 뒤집기

여분의 공백을 제거하면서 단어의 순서를 뒤집습니다. 깔끔한 파이썬 해법은 다음과 같습니다. split은 여러 공백을 처리하고, 목록에 reverse와 join을 적용합니다. 문자 배열을 제자리에서 뒤집으려면 배열 전체를 reverse한 다음 각 단어를 개별적으로 reverse합니다. 이 두 단계 방식은 O(n) 시간과 O(n) 공간을 사용합니다(파이썬 문자열은 변경 불가능하므로 피할 수 없습니다).

def reverse_words(s):
    words = s.split()       # split and strip whitespace
    words.reverse()         # in-place reverse
    return ' '.join(words)  # single space between words

print(reverse_words('  hello   world  '))  # 'world hello'
print(reverse_words('a good example'))     # 'example good a'

# One-liner:
print(' '.join('  hello   world  '.split()[::-1]))

회문 판별: 기본 방법

문자열이 자신의 reverse 결과와 같으면 회문입니다. 파이썬에서 가장 빠른 확인 방법은 s == s[::-1]입니다. 대소문자를 구분하지 않고 영숫자만 사용하는 회문(면접에서 가장 흔한 변형)의 경우 먼저 문자열을 정규화합니다. 영숫자가 아닌 문자를 걸러 내고 소문자로 변환한 다음 비교합니다. 두 방법 모두 O(n)입니다.

def is_palindrome(s):
    # Filter and normalise
    cleaned = ''.join(c.lower() for c in s if c.isalnum())
    return cleaned == cleaned[::-1]

print(is_palindrome('A man, a plan, a canal: Panama'))  # True
print(is_palindrome('race a car'))                       # False
print(is_palindrome('Was it a car or a cat I saw?'))     # True

회문 판별: 두 포인터

추가 공간을 O(1)로 유지하려면 슬라이싱 대신 두 포인터로 회문을 확인합니다. left를 0에, right를 끝에 둡니다. 영숫자가 아닌 문자는 건너뛰고, 남은 문자를 대소문자를 구분하지 않고 비교하며, 일치하지 않으면 거짓을 반환합니다. 이 방법은 더 장황하지만 정제된 문자열을 아예 만들지 않으므로 메모리가 제한된 상황에서 중요합니다.

def is_palindrome_twoptr(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1; right -= 1
    return True

print(is_palindrome_twoptr('A man, a plan, a canal: Panama'))  # True

중심 확장으로 가장 긴 회문 찾기

중심 확장 기법은 O(n²) 시간과 O(1) 추가 공간으로 가장 긴 회문 부분 문자열을 찾습니다. 각 문자(홀수 길이 회문)와 문자 사이의 각 간격(짝수 길이 회문)을 중심으로 삼아, 문자가 일치하는 동안 바깥쪽으로 확장합니다. 지금까지 찾은 최선의 (시작, 끝) 쌍을 기록합니다. 중심은 2n-1개이며, 각 확장은 최악의 경우 O(n)입니다.

def longest_palindrome(s):
    best_start = best_end = 0

    def expand(left, right):
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1; right += 1
        return left + 1, right - 1  # last valid bounds

    for i in range(len(s)):
        l, r = expand(i, i)      # odd-length
        if r - l > best_end - best_start:
            best_start, best_end = l, r
        l, r = expand(i, i + 1)  # even-length
        if r - l > best_end - best_start:
            best_start, best_end = l, r

    return s[best_start:best_end+1]

print(longest_palindrome('babad'))    # 'bab' or 'aba'
print(longest_palindrome('cbbd'))     # 'bb'

마나커 알고리즘 미리 보기

마나커 알고리즘은 더 큰 회문 안에 있는 회문을 거울 위치에서 초기화할 수 있다는 통찰을 이용해 O(n) 시간으로 가장 긴 회문 부분 문자열을 찾습니다. 면접에서 직접 구현하라는 요구는 드물지만, 이런 알고리즘이 존재한다는 점은 알아 둘 가치가 있습니다. 대부분의 면접관은 O(n²) 중심 확장 방식을 '충분히 최적'인 해법으로 인정합니다. 후속 질문으로 더 나은 방법을 묻는다면 이론적인 O(n) 해법으로 마나커 알고리즘을 언급하십시오.

# Manacher's: O(n) longest palindromic substring
def manacher(s):
    # Transform s into '#a#b#a#' to handle even/odd uniformly
    t = '#' + '#'.join(s) + '#'
    n = len(t)
    P = [0] * n  # P[i] = palindrome radius at i
    center = right = 0
    for i in range(n):
        mirror = 2 * center - i
        if i < right:
            P[i] = min(right - i, P[mirror])
        while (i + P[i] + 1 < n and i - P[i] - 1 >= 0
               and t[i+P[i]+1] == t[i-P[i]-1]):
            P[i] += 1
        if i + P[i] > right:
            center, right = i, i + P[i]
    max_len = max(P)
    center_idx = P.index(max_len)
    start = (center_idx - max_len) // 2
    return s[start:start+max_len]

print(manacher('babad'))   # 'bab'

런 길이 인코딩

런 길이 인코딩(RLE)은 연속해서 반복되는 문자를 압축합니다. 'aaabbc'는 'a3b2c1'이 됩니다. 구현할 때는 빠른 포인터로 각 런의 끝을 찾고, 문자와 개수를 출력 목록에 기록한 다음 join합니다. 짧은 런에서는 입력이 인코딩된 출력보다 짧을 수 있으므로, 반환하기 전에 항상 인코딩한 버전이 더 짧은지 확인하십시오.

def encode_rle(s):
    if not s: return ''
    parts = []
    i = 0
    while i < len(s):
        char = s[i]
        j = i
        while j < len(s) and s[j] == char:
            j += 1
        count = j - i
        parts.append(char + (str(count) if count > 1 else ''))
        i = j
    encoded = ''.join(parts)
    return encoded if len(encoded) < len(s) else s

print(encode_rle('aaabbc'))    # 'a3b2c'
print(encode_rle('abc'))       # 'abc'  (no compression gain)

런 길이 인코딩 문자열 디코딩

RLE 디코딩은 문자와 그 뒤에 이어지는 숫자 시퀀스를 읽어 각 런을 확장합니다. 면접에서는 반복되는 부분 문자열에 k[encoded_string] 형식을 사용하는 LeetCode 변형을 제시하기도 합니다. 예를 들어 3[ab] → ababab입니다. 이 중첩 변형은 여러 단계의 중첩을 처리하기 위해 스택이 필요합니다.

def decode_rle(s):
    result = []
    i = 0
    while i < len(s):
        char = s[i]; i += 1
        num_str = ''
        while i < len(s) and s[i].isdigit():
            num_str += s[i]; i += 1
        count = int(num_str) if num_str else 1
        result.append(char * count)
    return ''.join(result)

print(decode_rle('a3b2c'))    # 'aaabbc'
print(decode_rle('a2b3c1'))   # 'aabbbc'

# Nested bracket decode (LeetCode 394)
def decode_bracket(s):
    stack = []
    for c in s:
        if c != ']':
            stack.append(c)
        else:
            chars = []
            while stack[-1] != '[':
                chars.append(stack.pop())
            stack.pop()  # remove '['
            k = int(stack.pop())
            stack.append(''.join(reversed(chars)) * k)
    return ''.join(stack)
print(decode_bracket('3[ab]'))  # 'ababab'

유효한 회문 II: 한 번 삭제 허용

문자열이 주어졌을 때, 문자를 최대 하나까지 삭제하여 회문으로 만들 수 있으면 참을 반환합니다. 두 포인터를 사용하다가 처음 불일치하는 지점에서 s[left+1:right+1] 또는 s[left:right] 중 하나가 회문인지 확인합니다(일치하지 않은 두 문자 각각을 건너뛰어 봅니다). 어느 한쪽이 회문이면 참을 반환합니다. 불일치한 문자를 건너뛰는 것만이 유효한 행동이므로 이 그리디 방식이 작동합니다.

def valid_palindrome(s):
    def is_pal(l, r):
        while l < r:
            if s[l] != s[r]: return False
            l += 1; r -= 1
        return True

    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            # Try skipping either character
            return is_pal(left+1, right) or is_pal(left, right-1)
        left += 1; right -= 1
    return True

print(valid_palindrome('aba'))    # True
print(valid_palindrome('abca'))   # True  (delete 'c')
print(valid_palindrome('abc'))    # False

회문 분할 I

문자열을 회문인 모든 부분 문자열로 분할합니다. 역추적을 사용하여 각 단계에서 남은 문자열의 모든 접두사를 시도하고, 접두사가 회문이면 나머지 부분에 대해 재귀 호출을 수행합니다. 구간 DP를 사용해 2차원 불리언 테이블 is_pal[i][j]을 미리 계산하면 회문 확인을 O(1)로 만들 수 있습니다. 그 결과 전체 역추적의 복잡도는 O(n² × 2^n)에서 O(n × 2^n)으로 줄어듭니다. 모든 분할을 생성하는 작업 자체가 본질적으로 지수 시간이므로 이는 허용 가능한 복잡도입니다.

def partition(s):
    n = len(s)
    dp = [[False]*n for _ in range(n)]
    for i in range(n):
        dp[i][i] = True
    for length in range(2, n+1):
        for i in range(n-length+1):
            j = i + length - 1
            if s[i] == s[j]:
                dp[i][j] = length == 2 or dp[i+1][j-1]

    result = []
    def backtrack(start, path):
        if start == n: result.append(path[:]); return
        for end in range(start, n):
            if dp[start][end]:
                path.append(s[start:end+1])
                backtrack(end+1, path)
                path.pop()
    backtrack(0, [])
    return result

print(partition('aab'))  # [['a','a','b'],['aa','b']]

가장 짧은 회문: 문자열 해싱

문자열의 앞에 문자를 추가하여 만들 수 있는 가장 짧은 회문을 찾습니다. 핵심 통찰은 문자열의 가장 긴 회문 접두사를 찾은 다음, 남은 접미사를 reverse한 결과를 앞에 붙이는 것입니다. 가장 긴 회문 접두사를 효율적으로 찾으려면 s + '#' + reverse(s) 문자열에 KMP의 실패 함수를 사용합니다. 실패 함수의 마지막 값이 가장 긴 회문 접두사의 길이를 나타냅니다.

def shortest_palindrome(s):
    rev = s[::-1]
    combined = s + '#' + rev  # '#' prevents overlap
    n = len(combined)
    kmp = [0] * n
    j = 0
    for i in range(1, n):
        while j > 0 and combined[i] != combined[j]:
            j = kmp[j-1]
        if combined[i] == combined[j]:
            j += 1
        kmp[i] = j
    # kmp[-1] = length of longest palindromic prefix
    to_add = rev[:len(s) - kmp[-1]]
    return to_add + s

print(shortest_palindrome('aacecaaa'))  # 'aaacecaaa'
print(shortest_palindrome('abcd'))      # 'dcbabcd'

빠른 확인

이 단원에서 배운 자료 구조 및 알고리즘 — 코딩 면접 준비 개념을 얼마나 이해했는지 테스트해 보십시오.

단원 요약

이 단원에서는 다음을 배웠습니다. 두 포인터를 사용한 회문 판별은 O(n) 시간과 O(1) 공간을 사용하므로 공간이 중요할 때는 뒤집은 복사본을 할당하는 대신 항상 인덱스 기반 확인을 우선해야 합니다. 중심 확장 방식은 2n-1개의 위치를 각각 잠재적인 회문 중심으로 취급하여 O(n²) 시간에 가장 긴 회문 부분 문자열을 찾습니다. 또한 런 길이 인코딩은 연속된 런을 O(n)에 압축하며, 중첩 괄호 변형을 디코딩하려면 스택이 필요합니다. 다음에는 버블 정렬과 삽입 정렬을 살펴봅니다.

무료로 시작

AI 튜터와 함께 Python을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
30
레슨
120

자주 묻는 질문

“문자열 인코딩, 뒤집기, 회문” 강의는 무료인가요?

네 — “문자열 인코딩, 뒤집기, 회문” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“문자열 인코딩, 뒤집기, 회문”에서 뭘 배우나요?

제자리 단어 뒤집기와 런 길이 인코딩을 구현하고, 중심 확장 기법을 포함한 회문 판별을 학습합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“문자열 인코딩, 뒤집기, 회문” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 면접을 위한 Python 문자열 API
  2. 부분 문자열을 위한 슬라이딩 윈도우
  3. 애너그램과 문자 빈도 맵
  4. 문자열 인코딩, 뒤집기, 회문
← DSA Interview Prep(으)로 돌아가기