문자열 인코딩, 뒤집기, 회문
제자리 단어 뒤집기와 런 길이 인코딩을 구현하고, 중심 확장 기법을 포함한 회문 판별을 학습합니다.
문자열 인코딩, 뒤집기, 회문은(는) 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 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 면접을 위한 Python 문자열 API
- 부분 문자열을 위한 슬라이딩 윈도우
- 애너그램과 문자 빈도 맵
- 문자열 인코딩, 뒤집기, 회문