Programmers Code Problem: Phone Number List

Code Test
Algorithm Test
저자

Kwangmin Kim

공개

2025년 09월 13일

1 문제 정보

  • 제목: 전화번호 목록
  • 링크: https://school.programmers.co.kr/learn/courses/30/lessons/42577
  • 난이도: 2
  • 유형: Hash
  • 풀이 시간: 10분

2 문제 설명

전화번호부에 적힌 전화번호 중, 한 번호가 다른 번호의 접두어인 경우가 있는지 확인하려 합니다. 전화번호가 다음과 같을 경우, 구조대 전화번호는 영석이의 전화번호의 접두사입니다.

  • 구조대 : 119
  • 박준영 : 97 674 223
  • 지영석 : 11 9552 4421

전화번호부에 적힌 전화번호를 담은 배열 phone_book 이 solution 함수의 매개변수로 주어질 때, 어떤 번호가 다른 번호의 접두어인 경우가 있으면 false를, 그렇지 않으면 true를 return 하도록 solution 함수를 작성해주세요.

2.1 제한 사항

phone_book의 길이는 1 이상 1,000,000 이하입니다. 각 전화번호의 길이는 1 이상 20 이하입니다. 같은 전화번호가 중복해서 들어있지 않습니다.

2.2 입출력 예제

예시 phone_book return
예시 1 [“119”, “97674223”, “1195524421”] false
예시 2 [“123”,“456”,“789”] true
예시 3 [“12”,“123”,“1235”,“567”,“88”] false

2.3 입출력 예 설명

입출력 예 #1 앞에서 설명한 예와 같습니다.

입출력 예 #2 한 번호가 다른 번호의 접두사인 경우가 없으므로, 답은 true입니다.

입출력 예 #3 첫 번째 전화번호, “12”가 두 번째 전화번호 “123”의 접두사입니다. 따라서 답은 false입니다.

2.4 알림

2021년 3월 4일, 테스트 케이스가 변경되었습니다. 이로 인해 이전에 통과하던 코드가 더 이상 통과하지 않을 수 있습니다.

3 문제 해설

3.1 문제 핵심 이해

이 문제의 핵심은 한 전화번호가 다른 전화번호의 접두어인지 확인하는 것

  • 입력: 전화번호 목록 배열
  • 출력: 접두어 관계가 있으면 false, 없으면 true
  • 목표: 효율적으로 접두어 관계 탐지

3.2 중요한 조건들

  1. 접두어 정의: 한 번호가 다른 번호의 시작 부분과 완전히 일치
  2. 대용량 데이터: 최대 1,000,000개의 전화번호 처리 필요
  3. 효율성 요구: O(n²) 방식으로는 시간 초과 발생 가능

3.3 해결 방법별 분석

3.3.1 알고리즘 요약

3.3.1.1 핵심 아이디어

사전순 정렬 후 인접한 번호끼리만 비교하여 O(n log n) 시간에 모든 접두어 관계를 효율적으로 탐지

3.3.1.2 알고리즘 동작

  1. 사전순 정렬: 전화번호를 문자열 기준으로 정렬
  2. 인접 비교: 정렬된 배열에서 인접한 두 번호만 접두어 관계를 확인
  3. 조기 종료: 접두어 관계를 발견하면 즉시 false를 반환

3.3.1.3 왜 효율적인가?

정렬의 특성상 접두어 관계가 있다면 반드시 인접한 위치에 배치되므로, 모든 쌍을 비교할 필요가 없음

3.3.1.4 핵심 포인트

“정렬된 상태에서 접두어는 반드시 인접하다”는 특성을 활용하여 O(n²) → O(n log n)로 시간복잡도를 획기적으로 개선

3.3.2 정렬 방법 (권장)

시간복잡도: O(n log n)
공간복잡도: O(log n)

def solution(phone_book):
    # 사전순 정렬 - O(n log n) 시간, O(log n) 공간(정렬 알고리즘 스택)
    phone_book.sort()
    
    # 인접한 번호끼리만 접두어 관계 확인 - O(n) 시간, O(1) 공간
    for i in range(len(phone_book) - 1):  # O(n) 반복
        # startswith 비교 - O(m) 시간 (m: 짧은 번호의 길이)
        if phone_book[i+1].startswith(phone_book[i]):
            return False  # O(1) 시간, O(1) 공간
    
    return True  # O(1) 시간, O(1) 공간

# 전체 시간복잡도: O(n log n) + O(n * m) = O(n log n) (m << log n 일반적)
# 전체 공간복잡도: O(log n) (정렬 알고리즘의 스택 공간)

동작 원리: - 사전순 정렬 후 인접한 번호끼리만 비교 - 정렬되어 있으면 접두어 관계는 반드시 인접한 위치에 존재 - 예: [“119”, “1195524421”, “97674223”] 정렬 시 “119”와 “1195524421”이 인접

3.3.3 해시셋(Set) 방법

시간복잡도: O(n×m²) (n: 번호 개수, m: 평균 번호 길이)
공간복잡도: O(n×m)

def solution(phone_book):
    # 모든 번호를 해시셋에 저장 - O(n) 시간, O(n * m) 공간
    phone_set = set(phone_book)  # n개 번호, 각 번호 평균 m자리
    
    # 각 번호의 모든 접두어가 해시셋에 있는지 확인 - O(n * m) 시간
    for phone in phone_book:  # O(n) 반복
        for i in range(1, len(phone)):  # O(m) 반복 (m: 전화번호 길이)
            # 슬라이싱 O(i), 해시셋 검색 O(1) 평균 → O(m) 최대
            if phone[:i] in phone_set:
                return False  # O(1) 시간, O(1) 공간
    
    return True  # O(1) 시간, O(1) 공간

# 전체 시간복잡도: O(n) + O(n * m^2) = O(n * m^2)
# 전체 공간복잡도: O(n * m) (해시셋 저장)

동작 원리: - 모든 전화번호를 해시셋에 저장 - 각 번호의 모든 가능한 접두어를 생성하여 해시셋에서 검색

3.3.4 한 줄 해답 (정렬 + any 함수)

def solution(phone_book):
    # 사전순 정렬 - O(n log n) 시간, O(log n) 공간
    phone_book.sort()
    
    # any() 함수로 접두어 관계 확인 - O(n * m) 시간, O(1) 공간
    return not any(  # any(): 최악의 경우 O(n) 반복
        phone_book[i+1].startswith(phone_book[i])  # startswith: O(m) 시간
        for i in range(len(phone_book) - 1)  # 제너레이터: O(1) 공간
    )

# 전체 시간복잡도: O(n log n) + O(n * m) = O(n log n) (m << log n 일반적)
# 전체 공간복잡도: O(log n) (정렬 알고리즘 스택) + O(1) (제너레이터) = O(log n)

3.4 복잡도 비교표

방법 시간복잡도 공간복잡도 특징
정렬 O(n log n) O(log n) 가장 효율적, 메모리 절약
해시셋 O(n×m²) O(n×m) 구현이 직관적
한 줄 해답 O(n log n) O(log n) 코드가 간결

3.5 왜 정렬 방법이 최적인가?

  1. 핵심 아이디어: 사전순 정렬 시 접두어 관계는 반드시 인접한 위치에 존재
  2. 시간 효율성: O(n log n) - 정렬 후 한 번의 순회만 필요
  3. 공간 효율성: O(log n) - 정렬 알고리즘의 스택 공간만 사용
  4. 단순함: 구현이 직관적이고 이해하기 쉬움

3.6 핵심 포인트

  • 정렬의 활용: 사전순 정렬로 비교 대상을 대폭 축소
  • 접두어 특성: A가 B의 접두어라면, 정렬 후 A는 B보다 앞에 위치
  • 최적화: 모든 쌍 비교(O(n²)) → 인접 비교(O(n))

이 문제의 핵심은 “정렬을 통한 비교 대상 축소로 효율성을 극대화하는 것”입니다.

3.7 예제별 동작 과정

3.7.1 예제 1: ["119", "97674223", "1195524421"] → 결과: false

정렬 방법: 1. 사전순 정렬: ["119", "1195524421", "97674223"] 2. 인접 비교: - “119”와 “1195524421”: "1195524421".startswith("119") → True - 접두어 관계 발견! → 답: false

3.7.2 예제 2: ["123", "456", "789"] → 결과: true

정렬 방법: 1. 사전순 정렬: ["123", "456", "789"] (이미 정렬됨) 2. 인접 비교: - “123”과 “456”: "456".startswith("123") → False - “456”과 “789”: "789".startswith("456") → False - 접두어 관계 없음 → 답: true

3.7.3 예제 3: ["12", "123", "1235", "567", "88"] → 결과: false

정렬 방법: 1. 사전순 정렬: ["12", "123", "1235", "567", "88"] 2. 인접 비교: - “12”와 “123”: "123".startswith("12") → True - 접두어 관계 발견! → 답: false

Subscribe

Enjoy this blog? Get notified of new posts by email: