TIL_20250317_시저암호

Kim jisu·2025년 3월 17일

TIL

목록 보기
19/43

문제를 읽은 순간 아스키 코드를 활용해야겠다는 생각이 들긴 했다.
근데, string을 char로 변환하고, 공백 처리, z->a로 변환할 엄두가 안 났다. switch문을 이용하기에는 너무 노가다....
생각이 구체화가 안되어서 결국 외부 찬스.......
접근은 맞았는데 가장 뒤 문자에서 현재 문자를 빼 범위를 한정시킬 생각을 못 했다.
아직 이런 아이디어적인 부분이 부족하다.

일단 오늘 내용 정리!


🔍 문제 분석

  • 대소문자 알파벳을 n만큼 오른쪽으로 밀어 새로운 문자로 변환.
  • 공백은 그대로 유지.
  • 문자의 범위를 벗어나면 원형 순환 (z → a, Z → A).
  • 최대 입력 길이: 8,000 → O(N) 알고리즘이면 충분히 해결 가능.

1️⃣ 기본적인 mod 연산 활용 (O(N))

class Solution {
    public String solution(String s, int n) {
        StringBuilder answer = new StringBuilder();

        for (char c : s.toCharArray()) {
            if (Character.isUpperCase(c)) {
                answer.append((char) ((c - 'A' + n) % 26 + 'A'));
            } else if (Character.isLowerCase(c)) {
                answer.append((char) ((c - 'a' + n) % 26 + 'a'));
            } else {
                answer.append(c); // 공백 유지
            }
        }

        return answer.toString();
    }
}

📌 문자 범위 설정 자세한 설명

이 코드에서는 대문자 (A-Z)와 소문자 (a-z)의 아스키 코드 범위를 직접 비교하여 문자 변환을 수행.
이를 이해하려면 아스키 코드(ASCII) 값과 mod 연산에 대한 개념이 필요.


1️⃣ 아스키 코드(ASCII) 범위

문자 유형아스키 코드 범위예제
대문자 (A-Z)65 ~ 90'A' = 65, 'Z' = 90
소문자 (a-z)97 ~ 122'a' = 97, 'z' = 122
공백 (' ')32' ' = 32

이 정보를 활용하여, char 값을 if 문을 사용해 알파벳 범위 내에서 처리.


2️⃣ 대소문자 변환 원리

✅ 대문자 (A-Z) 변환

if (c >= 'A' && c <= 'Z') { 
    answer.append((char) ((c - 'A' + n) % 26 + 'A'));
}

작동 원리
1. 현재 문자를 A 기준으로 0부터 시작하도록 조정

  • 'A' = 65, 'Z' = 90
  • c - 'A' → 'A' - 'A' = 0, 'B' - 'A' = 1, ..., 'Z' - 'A' = 25
  1. n만큼 이동 후, mod 26 적용

    • (c - 'A' + n) % 26
    • 예제: 'Y' (89) → (89 - 65 + 3) % 26 = (24 + 3) % 26 = 1 → 'B' (66)
  2. 다시 'A'를 더해 원래 문자 범위로 복원

    • + 'A'
    • 예제: 1 + 'A' = 66 → 'B'

✅ 소문자 (a-z) 변환

if (c >= 'a' && c <= 'z') { 
    answer.append((char) ((c - 'a' + n) % 26 + 'a'));
}

작동 원리

  • c - 'a' → 소문자를 0부터 시작하는 인덱스로 변환.
  • 이후 n만큼 이동 후 mod 26을 적용하여 범위 내 유지.
  • 마지막으로 다시 'a'를 더해 문자 복원.

💡 핵심 개념:

  • 'A', 'a'를 기준으로 0부터 시작하도록 조정.
  • n만큼 이동 후 mod 26을 적용하여 알파벳 범위를 유지.
  • 다시 'A', 'a'를 더해 원래 문자로 복원.

✅ 특징

  • mod 26을 이용하여 문자 범위 벗어남 방지.
  • StringBuilder를 사용하여 성능 최적화.
  • 가장 직관적이고 빠른 방식 (O(N)).

2️⃣ Map<Character, Character>를 사용한 문자 매핑 (O(N))

import java.util.*;

class Solution {
    public String solution(String s, int n) {
        Map<Character, Character> upperMap = new HashMap<>();
        Map<Character, Character> lowerMap = new HashMap<>();

        for (char c = 'A'; c <= 'Z'; c++) {
            upperMap.put(c, (char) ((c - 'A' + n) % 26 + 'A'));
        }
        for (char c = 'a'; c <= 'z'; c++) {
            lowerMap.put(c, (char) ((c - 'a' + n) % 26 + 'a'));
        }

        StringBuilder answer = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (upperMap.containsKey(c)) {
                answer.append(upperMap.get(c));
            } else if (lowerMap.containsKey(c)) {
                answer.append(lowerMap.get(c));
            } else {
                answer.append(c); // 공백 유지
            }
        }
        return answer.toString();
    }
}

✅ 특징

  • 사전 매핑을 통해 빠르게 변환 가능.
  • 조회 속도 O(1) → O(N)보다 빠를 가능성 있음.
  • 하지만 메모리 사용량 증가 (HashMap 사용).

3️⃣ Stream API 활용 (람다식)

import java.util.stream.Collectors;

class Solution {
    public String solution(String s, int n) {
        return s.chars()
                .mapToObj(c -> (char) (Character.isUpperCase(c) ? (c - 'A' + n) % 26 + 'A' :
                        Character.isLowerCase(c) ? (c - 'a' + n) % 26 + 'a' : c))
                .map(String::valueOf)
                .collect(Collectors.joining());
    }
}

✅ 특징

  • 함수형 스타일로 깔끔하게 작성 가능.
  • chars() → mapToObj() → collect()의 스트림 처리 방식 활용.
  • 하지만 성능이 미세하게 떨어질 수 있음 (O(N) 유지).

4️⃣ String.replaceAll() + 정규표현식

class Solution {
    public String solution(String s, int n) {
        return s.replaceAll("[A-Z]", m -> String.valueOf((char) ((m.group().charAt(0) - 'A' + n) % 26 + 'A')))
                .replaceAll("[a-z]", m -> String.valueOf((char) ((m.group().charAt(0) - 'a' + n) % 26 + 'a')));
    }
}

✅ 특징

  • replaceAll()과 람다식을 이용하여 한 번의 replaceAll()으로 변환 가능.
  • 정규식 매칭을 사용하므로 깔끔한 코드.
  • 정규식이므로 속도가 상대적으로 느릴 수 있음.

🎯 성능 비교

방법시간 복잡도장점단점
방법 1 (mod 연산 활용)O(N)가장 빠름, 직관적없음
방법 2 (해시맵 매핑)O(N) (조회 O(1))빠른 조회 가능메모리 사용량 증가
방법 3 (Stream API)O(N)함수형 스타일로 깔끔성능이 미세하게 떨어질 수 있음
방법 4 (replaceAll() + 정규식)O(N)replaceAll() 한 번으로 처리정규식 사용으로 가독성 감소

🚀 최종 결론

✅ 가장 빠른 방법: mod 26을 활용한 기본 연산 (방법 1)

  • O(N)으로 가장 빠르게 실행됨.
  • StringBuilder 사용으로 메모리 절약.
  • 문자 범위를 벗어나지 않도록 mod 26 적용.

✅ 특별한 상황에서는 다른 방법 고려

  1. 해시맵을 사용하면 조회 속도 O(1)이므로, 대량의 변환이 필요할 때 유리 (방법 2).
  2. Stream API를 활용하면 코드가 간결해짐 (방법 3).
  3. 정규식(replaceAll())을 사용하면 유지보수성이 높아질 수 있음 (방법 4).

📌 배운 점

✅ 문제를 해결하는 방법은 여러 가지가 있으며, 상황에 따라 최적의 방법이 다를 수 있다.
✅ 기본적인 mod 연산을 활용하면 빠르고 간단하게 해결할 수 있다.
✅ Stream API, replaceAll() 등 다양한 접근 방식이 존재하며, 유지보수성과 성능을 고려해야 한다.
✅ 해시맵을 활용하면 조회 속도가 빨라질 수 있지만, 메모리 사용량이 증가할 수 있다.

📌 "알고리즘을 풀 때, 다양한 방법을 고민하는 습관을 기르자!" 🚀

profile
Dreamer

0개의 댓글