Skip to main content

Command Palette

Search for a command to run...

[LeetCode] 125. Valid Palindrome

Updated
2 min read

LINK : 125. Valid Palindrome

문제 설명

주어진 문자열 s가 회문인지 확인하시오. 단, 영숫자 문자만을 고려하며, 대소문자는 무시합니다. 공백, 기호 등은 무시합니다.

예시:

  • "A man, a plan, a canal: Panama" → true

  • "race a car" → false


접근 방식

이 문제는 문자열의 양 끝에서 시작하여 중앙으로 향하면서 문자가 서로 일치하는지 확인하면 해결할 수 있습니다. 하지만 이때 주의할 점은 아래와 같습니다:

  • 영숫자(letter 또는 digit)만 비교

  • 대소문자 구분 없음

따라서 문자열을 처음부터 끝까지 순회하며 다음을 수행합니다:

  1. 왼쪽 포인터(left)와 오른쪽 포인터(right)를 사용하여 각 문자를 검사합니다.

  2. 두 포인터가 가리키는 문자가 영숫자가 아니라면, 해당 포인터를 건너뜁니다.

  3. 두 문자가 모두 영숫자이고 서로 대소문자 구분 없이 같다면 다음 비교로 넘어갑니다.

  4. 하나라도 다르다면 회문이 아니므로 false를 반환합니다.


구현 코드 (Kotlin)

1차 시도

// 1차 시도
class Solution {
    fun isPalindrome(s: String): Boolean {
        val tmp = s.lowercase().replace("[^a-z0-9]".toRegex(), "")

        for (i in 0 until tmp.length / 2) {
            if (tmp[i] != tmp[tmp.length - i - 1]) return false
        }

        return true
    }
}

정규식을 사용한 풀이 방법 입니다. 해당 수준으로도 문제를 해결할 수 있어서 간단했던 것 같습니다.

2차 시도

class Solution {
    fun isPalindrome(s: String): Boolean {
        val queue = ArrayDeque<Char>()
        for (c in s) {
            if (c.isDigit() || c.isLetter()) queue.add(c.uppercaseChar())
        }

        for (i in 0 until queue.size / 2) {
            val first = queue.removeFirst()
            val last = queue.removeLast()

            if (first != last) return false
        }
        return true
    }
}

2차에는 deque를 사용한 방법을 사용했습니다. 그래서 앞뒤로 하나씩 빼서 확인하는 방식입니다. 다만 이것도 공간복잡도를 많이 잡아먹는 문제가 있습니다.

3차 시도

class Solution {
    fun isPalindrome(s: String): Boolean {
        var left = 0
        var right = s.length - 1

        while (left < right) {
            while (left < right && !s[left].isDigit() && !s[left].isLetter()) {
                left++
            }

            while (left < right && !s[right].isDigit() && !s[right].isLetter()) {
                right--
            }

            if (left == right) break
            if (s[left].lowercaseChar() != s[right].lowercaseChar()) {
                return false
            } else {
                left++
                right--
            }
        }

        return true
    }
}

로직은 복잡하지만 공간복잡도까지 고려한 풀이 방법입니다. 다만 성능은 개선되었지만 가독성이 많이 안좋아진 것 같아서 조금 맘에 들지는 않듭니다.

핵심 포인트

  • 투 포인터(Two Pointer) 방식을 사용하여 시간복잡도는 O(n)입니다.

  • Kotlin의 isLetterOrDigit(), lowercaseChar()를 이용하여 간결하게 문자를 정규화할 수 있습니다.

  • left < right 조건을 통해 불필요한 비교를 방지하고 조기 종료도 가능합니다.


결론

회문 문제는 문자열 처리에서 자주 나오는 기초 알고리즘 문제입니다. 이 문제에서 중요한 점은 입력 전처리, 즉 불필요한 문자의 제거대소문자 처리입니다. 이 과정만 잘 이해하고 나면 투 포인터 방식으로 쉽게 해결할 수 있습니다.

느낀점

단순한 방식으로 문제를 푸는 것이라면 정규식을 이용해서 푸는 것이 더 빨랐습니다. 난이도는 쉬웠지만 성능을 요구하는 문제라면 고민해야할 부분이나 알아야할 수 있는 부분이 많은 문제였다고 느껴집니다.

More from this blog

LangChain, LangGraph, LangSmith — 첫 AI 에이전트를 만들기 전에 알아야 하는 세 도구의 역할

처음으로 AI 에이전트를 만들어 보려는 백엔드 개발자를 대상으로 합니다. 이름이 비슷한 세 도구 — LangChain, LangGraph, LangSmith — 가 각각 어떤 문제를 풀려고 등장했는지, 어떤 관계로 묶여 있는지, 그리고 어디서부터 손을 대야 하는지를 한 흐름으로 정리합니다. 함께 자주 등장하는 단어(Tool, ReAct, Function C

May 14, 202613 min read

Spring @Transactional 동작 원리 — 프록시, 트랜잭션 매니저, 전파와 롤백의 진실

메서드 위에 @Transactional 한 줄을 붙이면 Spring은 그 호출을 가로채 트랜잭션을 시작하고, 예외가 나면 롤백하고, 정상 종료되면 커밋합니다. 이 글은 그 한 줄 뒤에서 일어나는 일을 코드 흐름과 함께 풀어냅니다. 프록시가 어떻게 메서드를 가로채는지, TransactionInterceptor가 어떤 순서로 매니저를 호출하는지, 전파 속성과

May 14, 202612 min read

끄적끄적 테크 블로그

47 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ