문제10 - 문자열 압축 (Python)

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

문자열을 일정한 단위로 잘라 연속되는 중복을 찾아 압축하고, 그 중 가장 짧은 결과물의 길이를 반환하는 문제입니다. 완전 탐색을 기반으로 하되, 파이썬의 슬라이싱과 zip 함수를 활용하여 효율적으로 구현하는 방법을 학습할 수 있습니다.


1.  나의 풀이

def solution(s):
    answer = len(s)
    # 문자열 길이가 2 이하인 경우 압축 효율이 없으므로 조기 반환
    if len(s) <= 2:
        return len(s)

    # 1부터 문자열 절반 길이까지 단위를 늘려가며 확인
    for i in range(1, len(s) // 2 + 1):
        parts = []
        idx = 0
        
        # 설정된 단위 i만큼 문자열을 슬라이싱하여 parts 리스트에 저장
        while idx + i <= len(s):
            # 이전 조각과 동일하면 카운트 증가, 다르면 새로 추가
            if parts and parts[-1][0] == s[idx:idx + i]:
                parts[-1][1] += 1
            else:
                parts.append([s[idx:idx + i], 1])
            idx += i

        # 남은 잔여 문자열 길이 계산 (단위로 나누어 떨어지지 않는 뒷부분)
        compressed_len = len(s[idx:])
        
        # parts 리스트를 순회하며 최종 압축 길이 합산
        for part in parts:
            compressed_len += i
            if part[1] > 1:
                # 반복 횟수가 10회, 100회 이상일 경우를 대비하여 문자열 길이로 합산
                compressed_len += len(str(part[1]))
        
        answer = min(answer, compressed_len)

    return answer

2.  오늘 배운 점 및 복기 노트

데이터를 묶어주는 zip 함수 활용법
파이썬의 내장 함수인 zip은 여러 개의 반복 가능한(iterable) 객체를 인자로 받아, 각 객체가 담고 있는 원소를 튜플 형태로 차례로 접근할 수 있게 합니다.

기본적인 사용법은 다음과 같습니다.

names = ['A', 'B', 'C']
scores = [10, 20, 30]
print(list(zip(names, scores))) # [('A', 10), ('B', 20), ('C', 30)] 형식입니다

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# 결과: A: 10, B: 20, C: 30


이번 문제와 같은 인접 원소 비교 패턴에서는 zip을 활용하여 '현재 원소'와 '다음 원소'를 쌍으로 묶어 처리할 수 있습니다.

# 인접 원소 비교 예시
words = ['ab', 'ab', 'cd', 'cd']
shifted_words = words[1:] + [''] # ['ab', 'cd', 'cd', '']
# words[1:] + ['']를 통해 한 칸씩 밀린 리스트와 비교 대상을 맞춤
for curr, nxt in zip(words, shifted_words):
    if curr == nxt:
        print(f"{curr}는 다음 요소와 같습니다.")

인덱스 제어와 슬라이싱의 관계 while 문을 사용하여 idx를 수동으로 증가시키는 방식은 인덱스 범위를 직접 제어하기에 용이하지만, 파이썬의 range(start, stop, step)를 활용하면 코드가 더 간결해질 수 있음을 확인했습니다. 특히 슬라이싱 s[idx:idx+i]에서 범위를 벗어나는 인덱스가 입력되어도 에러가 발생하지 않고 남은 부분까지만 반환한다는 파이썬의 특성을 이용하면 조건문을 줄일 수 있습니다.

 

range의 step 인자를 활용한 문자열 분할
문자열을 특정 간격(length)으로 자를 때, while 문을 쓰지 않고 리스트 컴프리헨션과 range의 세 번째 인자인 step을 활용하면 훨씬 간결하게 분할할 수 있습니다.

# s = "abcabcdede", length = 3 일 때
words = [s[i:i + length] for i in range(0, len(s), length)]
# 0부터 끝인덱스까지 한 도막(3)만큼 간격으로 자른 각 도막의 앞 인덱스가 i에 담김
# 결과: ['abc', 'abc', 'ded', 'e']
# -> [s[0:3], s[3:6], s[6:9], s[9:12]]가 담긴것

이 방식의 장점은 파이썬 슬라이싱의 특성상 마지막에 남는 문자열이 length보다 작더라도 인덱스 에러 없이 남은 부분만 정확히 가져온다는 것입니다. 수동으로 idx += length를 관리하는 것보다 실수를 줄일 수 있는 안전한 방법입니다.

3. 프로그래머스 코딩 테스트 문제 풀이 전략 교재에서 배울만한 점

교재에서는 크게 두 가지 풀이 흐름을 제시하며, 각각의 최적화 포인트가 다릅니다.

[첫 번째 풀이: 순차 비교 방식의 효율성]
교재의 첫 번째 풀이는 문자열을 실제로 합치지 않고 길이를 계산하는 것에 집중합니다. comp_len이라는 변수를 두고, 반복되는 패턴이 끝날 때마다 숫자의 길이(len(str(cnt)))와 패턴의 길이(len(temp))를 더해나갑니다. 이는 파이썬에서 무거운 작업인 문자열 연산(+)을 최소화하여 성능을 높이는 전략입니다.

[두 번째 풀이: 구조적 접근과 예외 처리]
두 번째 풀이는 모든 단위를 먼저 리스트로 만든 뒤 zip으로 처리합니다. 이 과정에서 발생할 수 있는 치명적인 오류를 짚어줍니다.

  • 런타임 에러 방지: 문자열 s의 길이가 1일 때, range(1, len(s) // 2 + 1)은 빈 범위를 반환합니다. min() 함수에 빈 반복 가능 객체가 들어가면 에러가 발생하므로, if len(s) == 1: return 1과 같은 예외 처리가 반드시 선행되어야 함을 배울 수 있습니다.