WeniVooks

검색

코딩 테스트 에센셜 with 파이썬

문자열 처리

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"))  # False
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"))  # False
3.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"))    # False
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"))    # False
3.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("{[]}"))      # True
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("{[]}"))      # True
3.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(")()("))    # False
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(")()("))    # False
4.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"))    # 3
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"))    # 3

5. 정규표현식 기초

복잡한 문자열 패턴을 처리할 때 유용합니다.

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. 문자열 팁

문자열 문제 체크리스트

  1. 문자열은 불변: 수정할 때 새 문자열 생성됨
  2. 리스트로 변환: 자주 수정해야 할 때 list(s)''.join()
  3. 슬라이싱 활용: s[::-1] 뒤집기, s[::2] 짝수 인덱스
  4. Counter 활용: 문자 빈도 계산
  5. 정규표현식: 복잡한 패턴은 re 모듈 활용

7. 연습문제

4.5 구현과 시뮬레이션4.7 투 포인터와 슬라이딩 윈도우