크래프톤 정글/정글에서 문제풀기

[정글 베이직 3] 회문 판별

cedis 2026. 3. 8. 02:58

 

처음에 코드를 쓰다가 멈췄다.

논리는 맞는 것 같은데, 실행하면 이상한 결과가 나왔다.
"A man, a plan, a canal: Panama"를 넣었더니 False가 나왔다.
회문이 맞는데 왜 False가 나오지?

원인을 찾는 데 생각보다 시간이 걸렸다. 딱 한 글자 차이였다.


문제 소개

문자열이 주어진다. 이 문자열이 회문인지 판별하는 함수를 작성하는 것.
단, 대소문자는 구분하지 않고, 알파벳과 숫자가 아닌 문자는 무시한다.

항목 내용
입력 임의의 문자열 s
출력 True 또는 False
핵심 도구 isalnum(), lower(), 리스트 컴프리헨션, [::-1]

예제 입출력은 이렇다.

# 입력 / 출력
"A man, a plan, a canal: Panama"  →  True
"race a car"                      →  False
"Was it a car or a cat I saw?"    →  True
"Madam"                           →  True

일단 뼈대부터 채웠다

주어진 뼈대 코드는 이랬다.

def is_palindrome(s):
    pass
    cleand = "".join([c.lower() for c in s if s.isalnum()])
    pass
    return cleand == cleand[::-1]

논리 흐름은 읽혔다.
알파벳·숫자만 남기고 → 소문자로 변환 → 뒤집어서 비교.
근데 실행하면 모든 입력에 True가 나왔다.


버그가 4개 있었다

하나씩 찾았다.

① pass 두 줄

pass는 "아무것도 안 함" 키워드다. 코드가 없는 블록에 임시로 채워두는 자리표시자.
구현이 다 됐으면 지우면 된다. 동작에 영향은 없지만, 남겨두면 읽는 사람이 혼란스럽다.

② s.isalnum() → c.isalnum() (핵심 버그)

이게 모든 입력에 True가 나온 원인이었다.

# 문제의 코드
[c.lower() for c in s if s.isalnum()]
#                        ^
#                    s 전체를 검사하고 있다

for c in s는 문자열을 한 글자씩 꺼내서 c에 담는다.
근데 조건에서 s.isalnum()을 쓰면 문자열 전체를 검사한다.

코드 검사 대상 "A man!" 에서 결과
s.isalnum() 문자열 전체 False (공백·! 포함)
c.isalnum() 현재 문자 하나 "A"→True, " "→False, "!"→False

s.isalnum()이 False면 조건이 항상 False다.
리스트에 아무것도 안 담긴다 → cleaned가 빈 문자열 """" == ""는 True.
그래서 어떤 입력을 넣어도 True가 나왔던 거다.

③ 변수명 오타 — cleand → cleaned

뼈대 코드에 cleand로 적혀 있었다. cleaned로 통일해야 NameError가 안 난다.


최종 코드

BEFORE와 AFTER를 나란히 놓으면 이렇다.

BEFORE — 버그 있는 코드
def is_palindrome(s):
    pass
    cleand = "".join(
        [c.lower()
         for c in s
         if s.isalnum()])
    pass
    return cleand == cleand[::-1]
AFTER — 수정된 코드
def is_palindrome(s):
    cleaned = "".join(
        [c.lower()
         for c in s
         if c.isalnum()])
    return cleaned == cleaned[::-1]

바뀐 건 딱 두 가지다.
s.isalnum()c.isalnum(), 그리고 pass 삭제, 변수명 통일.


코드가 하는 일을 한 줄씩 보면

s = "A man, a plan, a canal: Panama"

# 1단계 — 알파벳·숫자만 + 소문자로
# c="A" → isalnum=True  → "a"
# c=" " → isalnum=False → 제외
# c="m" → isalnum=True  → "m"
# c="," → isalnum=False → 제외
# ...
cleaned = "".join([c.lower() for c in s if c.isalnum()])
# "amanaplanacanalpanama"

# 2단계 — 뒤집어서 비교
cleaned[::-1]  # "amanaplanacanalpanama"

return cleaned == cleaned[::-1]  # True

[ : : -1] 이 뭔가

파이썬 슬라이싱은 text[start:end:step] 형태다.
step-1로 주면 뒤에서 앞으로 한 글자씩 이동한다. 문자열 전체가 뒤집힌다.

"python"[::-1]   # "nohtyp"
"racecar"[::-1]  # "racecar"  ← 회문이니까 동일

return cleaned == cleaned[::-1] 의 반환 타입

==은 항상 bool을 반환한다.
if cleaned == cleaned[::-1]: return True 같이 쓸 필요 없다. 비교 결과 자체를 바로 return하면 된다.


전체 테스트

tests = [
    ("A man, a plan, a canal: Panama", True),
    ("race a car",                     False),
    ("Was it a car or a cat I saw?",   True),
    ("Madam",                          True),
]

for s, expected in tests:
    result = is_palindrome(s)
    status = "PASS" if result == expected else "FAIL"
    print(f'{status} | "{s}" → {result}')

# PASS | "A man, a plan, a canal: Panama" → True
# PASS | "race a car" → False
# PASS | "Was it a car or a cat I saw?" → True
# PASS | "Madam" → True

시간복잡도

단계 복잡도 이유
정제 (컴프리헨션) O(n) s 전체를 한 번 순회
뒤집기 ([::-1]) O(n) cleaned 길이만큼 복사
비교 (==) O(n) 두 문자열 앞→뒤 비교
전체 O(n)  

[::-1]은 뒤집은 문자열을 새로 만드니까 O(n) 공간도 추가로 쓴다.
투 포인터로 짜면 O(1) 공간으로 줄일 수 있다. 다음 풀이에서 해볼 것 같다.


마무리

버그의 원인은 sc 한 글자 차이였다.

for c in s라고 쓴 순간, 지금 다루는 건 c다.
조건도, 변환도, 모두 c에 적용해야 한다.
s를 건드리는 순간 한 글자가 아니라 문자열 전체를 상대하게 된다.

루프 변수가 무엇인지 항상 의식하면서 쓰는 것.
이번 문제에서 가져가는 습관이다.

다음 문제로 넘어갔다.

🌿