문자열 처리
1. 문자열 처리 문제
문자열 처리는 코딩테스트에서 자주 출제되는 유형입니다. Python의 문자열 메서드를 잘 활용하면 빠르게 해결할 수 있습니다.
1.1 문자열 문제의 특징
- 파싱(Parsing): 문자열에서 필요한 정보 추출
- 변환(Transformation): 특정 규칙에 따라 문자열 변경
- 검증(Validation): 패턴 일치, 조건 확인
- 정규표현식: 복잡한 패턴 매칭에 활용
2. 필수 문자열 메서드
2.1 기본 메서드
s = "Hello World"
# 대소문자 변환
print(s.lower()) # "hello world"
print(s.upper()) # "HELLO WORLD"
print(s.swapcase()) # "hELLO wORLD"
print(s.capitalize()) # "Hello world"
print(s.title()) # "Hello World"
# 검색
print(s.find("o")) # 4 (첫 번째 위치, 없으면 -1)
print(s.rfind("o")) # 7 (마지막 위치)
print(s.index("o")) # 4 (없으면 ValueError)
print(s.count("o")) # 2 (개수)
# 시작/끝 확인
print(s.startswith("Hello")) # True
print(s.endswith("ld")) # True
# 공백 처리
s2 = " hello "
print(s2.strip()) # "hello" (양쪽 공백 제거)
print(s2.lstrip()) # "hello " (왼쪽 공백 제거)
print(s2.rstrip()) # " hello" (오른쪽 공백 제거)s = "Hello World"
# 대소문자 변환
print(s.lower()) # "hello world"
print(s.upper()) # "HELLO WORLD"
print(s.swapcase()) # "hELLO wORLD"
print(s.capitalize()) # "Hello world"
print(s.title()) # "Hello World"
# 검색
print(s.find("o")) # 4 (첫 번째 위치, 없으면 -1)
print(s.rfind("o")) # 7 (마지막 위치)
print(s.index("o")) # 4 (없으면 ValueError)
print(s.count("o")) # 2 (개수)
# 시작/끝 확인
print(s.startswith("Hello")) # True
print(s.endswith("ld")) # True
# 공백 처리
s2 = " hello "
print(s2.strip()) # "hello" (양쪽 공백 제거)
print(s2.lstrip()) # "hello " (왼쪽 공백 제거)
print(s2.rstrip()) # " hello" (오른쪽 공백 제거)2.2 분리와 결합
# split: 문자열 분리
s = "apple,banana,cherry"
print(s.split(",")) # ["apple", "banana", "cherry"]
s2 = "hello world python"
print(s2.split()) # ["hello", "world", "python"] (공백 기준)
print(s2.split(" ", 1)) # ["hello", "world python"] (최대 1번 분리)
# join: 문자열 결합
words = ["hello", "world"]
print(" ".join(words)) # "hello world"
print("-".join(words)) # "hello-world"
print("".join(words)) # "helloworld"
# splitlines: 줄 단위 분리
text = "line1\nline2\nline3"
print(text.splitlines()) # ["line1", "line2", "line3"]# split: 문자열 분리
s = "apple,banana,cherry"
print(s.split(",")) # ["apple", "banana", "cherry"]
s2 = "hello world python"
print(s2.split()) # ["hello", "world", "python"] (공백 기준)
print(s2.split(" ", 1)) # ["hello", "world python"] (최대 1번 분리)
# join: 문자열 결합
words = ["hello", "world"]
print(" ".join(words)) # "hello world"
print("-".join(words)) # "hello-world"
print("".join(words)) # "helloworld"
# splitlines: 줄 단위 분리
text = "line1\nline2\nline3"
print(text.splitlines()) # ["line1", "line2", "line3"]2.3 치환과 정렬
# replace: 문자열 치환
s = "hello world"
print(s.replace("world", "python")) # "hello python"
print(s.replace("l", "L", 1)) # "heLlo world" (최대 1번)
# 정렬
s = "hello"
print(s.center(10)) # " hello "
print(s.ljust(10)) # "hello "
print(s.rjust(10)) # " hello"
print(s.zfill(10)) # "00000hello"# replace: 문자열 치환
s = "hello world"
print(s.replace("world", "python")) # "hello python"
print(s.replace("l", "L", 1)) # "heLlo world" (최대 1번)
# 정렬
s = "hello"
print(s.center(10)) # " hello "
print(s.ljust(10)) # "hello "
print(s.rjust(10)) # " hello"
print(s.zfill(10)) # "00000hello"2.4 문자 종류 확인
# 문자 종류 확인
print("abc".isalpha()) # True (알파벳만)
print("123".isdigit()) # True (숫자만)
print("abc123".isalnum()) # True (알파벳 + 숫자)
print(" ".isspace()) # True (공백만)
print("ABC".isupper()) # True (대문자만)
print("abc".islower()) # True (소문자만)
# 아스키 코드
print(ord('A')) # 65
print(ord('a')) # 97
print(ord('0')) # 48
print(chr(65)) # 'A'
print(chr(97)) # 'a'# 문자 종류 확인
print("abc".isalpha()) # True (알파벳만)
print("123".isdigit()) # True (숫자만)
print("abc123".isalnum()) # True (알파벳 + 숫자)
print(" ".isspace()) # True (공백만)
print("ABC".isupper()) # True (대문자만)
print("abc".islower()) # True (소문자만)
# 아스키 코드
print(ord('A')) # 65
print(ord('a')) # 97
print(ord('0')) # 48
print(chr(65)) # 'A'
print(chr(97)) # 'a'3. 자주 사용하는 문자열 패턴
3.1 회문(팰린드롬) 검사
def is_palindrome(s):
"""회문인지 확인 (대소문자 구분 없이, 알파벳만)"""
# 알파벳만 추출하고 소문자로 변환
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")) # Falsedef is_palindrome(s):
"""회문인지 확인 (대소문자 구분 없이, 알파벳만)"""
# 알파벳만 추출하고 소문자로 변환
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")) # False3.2 애너그램 검사
def is_anagram(s1, s2):
"""두 문자열이 애너그램인지 확인"""
# 정렬 비교
return sorted(s1.lower()) == sorted(s2.lower())
def is_anagram_counter(s1, s2):
"""Counter 사용"""
from collections import Counter
return Counter(s1.lower()) == Counter(s2.lower())
print(is_anagram("listen", "silent")) # True
print(is_anagram("hello", "world")) # Falsedef is_anagram(s1, s2):
"""두 문자열이 애너그램인지 확인"""
# 정렬 비교
return sorted(s1.lower()) == sorted(s2.lower())
def is_anagram_counter(s1, s2):
"""Counter 사용"""
from collections import Counter
return Counter(s1.lower()) == Counter(s2.lower())
print(is_anagram("listen", "silent")) # True
print(is_anagram("hello", "world")) # False3.3 괄호 검증
def is_valid_parentheses(s):
"""괄호 쌍이 올바른지 확인"""
stack = []
pairs = {')': '(', '}': '{', ']': '['}
for char in s:
if char in '({[':
stack.append(char)
elif char in ')}]':
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()
return len(stack) == 0
print(is_valid_parentheses("()[]{}")) # True
print(is_valid_parentheses("(]")) # False
print(is_valid_parentheses("([)]")) # False
print(is_valid_parentheses("{[]}")) # Truedef is_valid_parentheses(s):
"""괄호 쌍이 올바른지 확인"""
stack = []
pairs = {')': '(', '}': '{', ']': '['}
for char in s:
if char in '({[':
stack.append(char)
elif char in ')}]':
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()
return len(stack) == 0
print(is_valid_parentheses("()[]{}")) # True
print(is_valid_parentheses("(]")) # False
print(is_valid_parentheses("([)]")) # False
print(is_valid_parentheses("{[]}")) # True3.4 문자열 압축
def compress_string(s):
"""연속된 문자 압축 (예: "aabccc" → "a2bc3")"""
if not s:
return ""
result = []
count = 1
for i in range(1, len(s)):
if s[i] == s[i - 1]:
count += 1
else:
result.append(s[i - 1])
if count > 1:
result.append(str(count))
count = 1
# 마지막 문자 처리
result.append(s[-1])
if count > 1:
result.append(str(count))
return ''.join(result)
print(compress_string("aabcccccaaa")) # "a2bc5a3"
print(compress_string("abc")) # "abc"def compress_string(s):
"""연속된 문자 압축 (예: "aabccc" → "a2bc3")"""
if not s:
return ""
result = []
count = 1
for i in range(1, len(s)):
if s[i] == s[i - 1]:
count += 1
else:
result.append(s[i - 1])
if count > 1:
result.append(str(count))
count = 1
# 마지막 문자 처리
result.append(s[-1])
if count > 1:
result.append(str(count))
return ''.join(result)
print(compress_string("aabcccccaaa")) # "a2bc5a3"
print(compress_string("abc")) # "abc"4. 문자열 실전 문제
4.1 유효한 사용자명 만들기
import re
def make_valid_username(username):
"""
사용자명을 규칙에 맞게 변환합니다.
규칙:
1. 소문자로 변환
2. 알파벳, 숫자, 하이픈, 언더스코어, 마침표만 허용
3. 연속된 마침표를 하나로
4. 처음과 끝의 마침표 제거
5. 빈 문자열이면 "a"
6. 15자 초과시 자르고 끝 마침표 제거
7. 2자 이하면 마지막 문자 반복
"""
# 1단계: 소문자로 변환
answer = username.lower()
# 2단계: 허용 문자만 남기기
answer = re.sub(r'[^a-z0-9\-_.]', '', answer)
# 3단계: 연속된 마침표를 하나로
answer = re.sub(r'\.+', '.', answer)
# 4단계: 처음과 끝의 마침표 제거
answer = answer.strip('.')
# 5단계: 빈 문자열이면 "a"
if not answer:
answer = "a"
# 6단계: 15자 초과시 자르고 끝 마침표 제거
if len(answer) > 15:
answer = answer[:15].rstrip('.')
# 7단계: 2자 이하면 마지막 문자 반복
while len(answer) < 3:
answer += answer[-1]
return answer
print(make_valid_username("...!@BaT#*..y.abcdefghijklm"))
# "bat.y.abcdefghi"import re
def make_valid_username(username):
"""
사용자명을 규칙에 맞게 변환합니다.
규칙:
1. 소문자로 변환
2. 알파벳, 숫자, 하이픈, 언더스코어, 마침표만 허용
3. 연속된 마침표를 하나로
4. 처음과 끝의 마침표 제거
5. 빈 문자열이면 "a"
6. 15자 초과시 자르고 끝 마침표 제거
7. 2자 이하면 마지막 문자 반복
"""
# 1단계: 소문자로 변환
answer = username.lower()
# 2단계: 허용 문자만 남기기
answer = re.sub(r'[^a-z0-9\-_.]', '', answer)
# 3단계: 연속된 마침표를 하나로
answer = re.sub(r'\.+', '.', answer)
# 4단계: 처음과 끝의 마침표 제거
answer = answer.strip('.')
# 5단계: 빈 문자열이면 "a"
if not answer:
answer = "a"
# 6단계: 15자 초과시 자르고 끝 마침표 제거
if len(answer) > 15:
answer = answer[:15].rstrip('.')
# 7단계: 2자 이하면 마지막 문자 반복
while len(answer) < 3:
answer += answer[-1]
return answer
print(make_valid_username("...!@BaT#*..y.abcdefghijklm"))
# "bat.y.abcdefghi"4.2 단위별 문자열 압축
def min_compressed_length(s):
"""
문자열을 여러 단위로 압축해보고 가장 짧은 길이를 반환합니다.
압축 규칙:
- 연속으로 반복되는 부분 문자열을 "반복횟수+문자열"로 표현
- 예: "aabbaccc" → "2a2ba3c" (1단위), 길이 7
s: 압축할 문자열
"""
if len(s) == 1:
return 1
min_length = len(s)
# 1개 ~ len(s)//2개 단위로 압축 시도
for unit in range(1, len(s) // 2 + 1):
compressed = []
prev = s[:unit]
count = 1
for i in range(unit, len(s), unit):
curr = s[i:i + unit]
if curr == prev:
count += 1
else:
if count > 1:
compressed.append(str(count))
compressed.append(prev)
prev = curr
count = 1
# 마지막 처리
if count > 1:
compressed.append(str(count))
compressed.append(prev)
result = ''.join(compressed)
min_length = min(min_length, len(result))
return min_length
print(min_compressed_length("aabbaccc")) # 7 ("2a2ba3c")
print(min_compressed_length("ababcdcdababcdcd")) # 9 ("2ababcdcd")
print(min_compressed_length("abcabcdede")) # 8 ("2abcdede")def min_compressed_length(s):
"""
문자열을 여러 단위로 압축해보고 가장 짧은 길이를 반환합니다.
압축 규칙:
- 연속으로 반복되는 부분 문자열을 "반복횟수+문자열"로 표현
- 예: "aabbaccc" → "2a2ba3c" (1단위), 길이 7
s: 압축할 문자열
"""
if len(s) == 1:
return 1
min_length = len(s)
# 1개 ~ len(s)//2개 단위로 압축 시도
for unit in range(1, len(s) // 2 + 1):
compressed = []
prev = s[:unit]
count = 1
for i in range(unit, len(s), unit):
curr = s[i:i + unit]
if curr == prev:
count += 1
else:
if count > 1:
compressed.append(str(count))
compressed.append(prev)
prev = curr
count = 1
# 마지막 처리
if count > 1:
compressed.append(str(count))
compressed.append(prev)
result = ''.join(compressed)
min_length = min(min_length, len(result))
return min_length
print(min_compressed_length("aabbaccc")) # 7 ("2a2ba3c")
print(min_compressed_length("ababcdcdababcdcd")) # 9 ("2ababcdcd")
print(min_compressed_length("abcabcdede")) # 8 ("2abcdede")4.3 괄호 짝 검증
def is_valid_parentheses(s):
"""
'('와 ')'로만 이루어진 문자열이 올바른 괄호 쌍인지 확인합니다.
올바른 괄호:
- 모든 '('에 대응하는 ')'가 있음
- ')'가 '('보다 먼저 나오면 안 됨
"""
count = 0
for char in s:
if char == '(':
count += 1
else:
count -= 1
if count < 0: # ')'가 먼저 나온 경우
return False
return count == 0
print(is_valid_parentheses("()()")) # True
print(is_valid_parentheses("(())()")) # True
print(is_valid_parentheses(")()(")) # Falsedef is_valid_parentheses(s):
"""
'('와 ')'로만 이루어진 문자열이 올바른 괄호 쌍인지 확인합니다.
올바른 괄호:
- 모든 '('에 대응하는 ')'가 있음
- ')'가 '('보다 먼저 나오면 안 됨
"""
count = 0
for char in s:
if char == '(':
count += 1
else:
count -= 1
if count < 0: # ')'가 먼저 나온 경우
return False
return count == 0
print(is_valid_parentheses("()()")) # True
print(is_valid_parentheses("(())()")) # True
print(is_valid_parentheses(")()(")) # False4.4 점수 계산기
import re
def calculate_score(score_string):
"""
점수 문자열을 파싱하여 총점을 계산합니다.
형식: 숫자(0~10) + 보너스(S/D/T) + 옵션(*/#)
- S: 1제곱, D: 2제곱, T: 3제곱
- *: 해당 점수와 이전 점수 2배
- #: 해당 점수 마이너스
예: "1S2D*3T" → 1^1*2 + 2^2*2 + 3^3 = 2 + 8 + 27 = 37
"""
# 정규표현식으로 파싱
pattern = r'(\d+)([SDT])([*#]?)'
matches = re.findall(pattern, score_string)
scores = []
power = {'S': 1, 'D': 2, 'T': 3}
for num, bonus, option in matches:
score = int(num) ** power[bonus]
if option == '*':
score *= 2
if scores:
scores[-1] *= 2
elif option == '#':
score *= -1
scores.append(score)
return sum(scores)
print(calculate_score("1S2D*3T")) # 37
print(calculate_score("1D2S#10S")) # 9
print(calculate_score("1D2S0T")) # 3import re
def calculate_score(score_string):
"""
점수 문자열을 파싱하여 총점을 계산합니다.
형식: 숫자(0~10) + 보너스(S/D/T) + 옵션(*/#)
- S: 1제곱, D: 2제곱, T: 3제곱
- *: 해당 점수와 이전 점수 2배
- #: 해당 점수 마이너스
예: "1S2D*3T" → 1^1*2 + 2^2*2 + 3^3 = 2 + 8 + 27 = 37
"""
# 정규표현식으로 파싱
pattern = r'(\d+)([SDT])([*#]?)'
matches = re.findall(pattern, score_string)
scores = []
power = {'S': 1, 'D': 2, 'T': 3}
for num, bonus, option in matches:
score = int(num) ** power[bonus]
if option == '*':
score *= 2
if scores:
scores[-1] *= 2
elif option == '#':
score *= -1
scores.append(score)
return sum(scores)
print(calculate_score("1S2D*3T")) # 37
print(calculate_score("1D2S#10S")) # 9
print(calculate_score("1D2S0T")) # 35. 정규표현식 기초
복잡한 문자열 패턴을 처리할 때 유용합니다.
5.1 기본 패턴
import re
text = "Hello123World456"
# 숫자만 추출
print(re.findall(r'\d+', text)) # ['123', '456']
# 알파벳만 추출
print(re.findall(r'[a-zA-Z]+', text)) # ['Hello', 'World']
# 패턴 치환
print(re.sub(r'\d+', 'X', text)) # "HelloXWorldX"
# 패턴 분리
print(re.split(r'\d+', text)) # ['Hello', 'World', '']import re
text = "Hello123World456"
# 숫자만 추출
print(re.findall(r'\d+', text)) # ['123', '456']
# 알파벳만 추출
print(re.findall(r'[a-zA-Z]+', text)) # ['Hello', 'World']
# 패턴 치환
print(re.sub(r'\d+', 'X', text)) # "HelloXWorldX"
# 패턴 분리
print(re.split(r'\d+', text)) # ['Hello', 'World', '']5.2 자주 사용하는 정규표현식
import re
# 이메일 추출
text = "연락처: test@example.com, admin@test.co.kr"
emails = re.findall(r'[\w.-]+@[\w.-]+\.\w+', text)
print(emails) # ['test@example.com', 'admin@test.co.kr']
# 전화번호 추출
text = "연락처: 010-1234-5678, 02-123-4567"
phones = re.findall(r'\d{2,3}-\d{3,4}-\d{4}', text)
print(phones) # ['010-1234-5678', '02-123-4567']
# HTML 태그 제거
html = "<p>Hello</p><div>World</div>"
clean = re.sub(r'<[^>]+>', '', html)
print(clean) # "HelloWorld"
# 공백 정리 (연속 공백을 하나로)
text = "Hello World Python"
clean = re.sub(r'\s+', ' ', text)
print(clean) # "Hello World Python"import re
# 이메일 추출
text = "연락처: test@example.com, admin@test.co.kr"
emails = re.findall(r'[\w.-]+@[\w.-]+\.\w+', text)
print(emails) # ['test@example.com', 'admin@test.co.kr']
# 전화번호 추출
text = "연락처: 010-1234-5678, 02-123-4567"
phones = re.findall(r'\d{2,3}-\d{3,4}-\d{4}', text)
print(phones) # ['010-1234-5678', '02-123-4567']
# HTML 태그 제거
html = "<p>Hello</p><div>World</div>"
clean = re.sub(r'<[^>]+>', '', html)
print(clean) # "HelloWorld"
# 공백 정리 (연속 공백을 하나로)
text = "Hello World Python"
clean = re.sub(r'\s+', ' ', text)
print(clean) # "Hello World Python"5.3 정규표현식 주요 메타문자
| 패턴 | 설명 | 예시 |
|---|---|---|
\d | 숫자 | \d+ → "123" |
\w | 알파벳+숫자+_ | \w+ → "hello_123" |
\s | 공백 | \s+ → " " |
. | 모든 문자 | a.c → "abc", "a1c" |
* | 0개 이상 | ab* → "a", "ab", "abb" |
+ | 1개 이상 | ab+ → "ab", "abb" |
? | 0개 또는 1개 | ab? → "a", "ab" |
[] | 문자 클래스 | [aeiou] → 모음 |
^ | 시작 | ^Hello |
$ | 끝 | World$ |
6. 문자열 팁
문자열 문제 체크리스트
- 문자열은 불변: 수정할 때 새 문자열 생성됨
- 리스트로 변환: 자주 수정해야 할 때
list(s)후''.join() - 슬라이싱 활용:
s[::-1]뒤집기,s[::2]짝수 인덱스 - Counter 활용: 문자 빈도 계산
- 정규표현식: 복잡한 패턴은
re모듈 활용
7. 연습문제
- (문자열) 알파벳 뒤집기: https://pyalgo.co.kr/?page=2
- (문자열) 카멜케이스: https://pyalgo.co.kr/?page=4
- (문자열) 문자열 쪼개기: https://100.pyalgo.co.kr/?page=33
- (문자열) 반복 문자열: https://100.pyalgo.co.kr/?page=35
- (문자열) 대소문자 바꾸기: https://100.pyalgo.co.kr/?page=63