"loop A x loop B 을 사용한 포함여부판별" 은 아래와 같이 개선될 수 있다.
zip 자체는 O(1), zip 에 따른 loop 사용은 O(N)
zip() 을 사용하여 이웃 간 묶음을 만들 수도 있다.
e.g.
# ["12","123","1235","567","88"]
list(zip(phoneBook, phoneBook[1:]))
=> [('12', '123'), ('123', '1235'), ('1235', '567'), ('567', '88')]
문제 설명
전화번호부에 적힌 전화번호 중, 한 번호가 다른 번호의 접두어인 경우가 있는지 확인하려 합니다.
전화번호가 다음과 같을 경우, 구조대 전화번호는 영석이의 전화번호의 접두사입니다.
구조대 : 119
박준영 : 97 674 223
지영석 : 11 9552 4421
전화번호부에 적힌 전화번호를 담은 배열 phone_book 이 solution 함수의 매개변수로 주어질 때, 어떤 번호가 다른 번호의 접두어인 경우가 있으면 false를 그렇지 않으면 true를 return 하도록 solution 함수를 작성해주세요.
제한 사항
phone_book의 길이는 1 이상 1,000,000 이하입니다.
각 전화번호의 길이는 1 이상 20 이하입니다.
같은 전화번호가 중복해서 들어있지 않습니다.
입출력 예제
phone_book return
["119", "97674223", "1195524421"] false
["123","456","789"] true
["12","123","1235","567","88"] false
입출력 예 설명
입출력 예 #1
앞에서 설명한 예와 같습니다.
입출력 예 #2
한 번호가 다른 번호의 접두사인 경우가 없으므로, 답은 true입니다.
입출력 예 #3
첫 번째 전화번호, “12”가 두 번째 전화번호 “123”의 접두사입니다. 따라서 답은 false입니다.
https://school.programmers.co.kr/learn/courses/30/lessons/42577
각 문자열에 대해 0~l 까지의 문자열을 포함하는지를 판별한다.
def solution(phone_book):
# len 기준으로 정렬
ph = sorted(phone_book,key=lambda phone_book: len(phone_book)) # O(N log N)
# 전화번호부 문자열 중 최대 길이
max_len = len(ph[-1])
# 전화번호부 문자열 중 최소 길이
min_len = len(ph[0])
# 최소~최대 길이의 substring 을 포함하는지 유무를 판별
for l in (min_len,max_len+1): # O(N)
# 포함유무를 임시저장하기 위한 dict
hashMap = dict()
for p in ph:
pOfLength = p[0:l]
# 만약 포함하지 않는다면 dict 에 추가 / 포함한다면 False 반환
if(hashMap.get(pOfLength) == None):
hashMap.setdefault(pOfLength,1)
else:
return False
return True
하지만 해당 접근은 의도대로 되지 않는다.
왜일까?
결론부터 말하자면 0~n 문자열을 0~m 문자열이 포함하는지를 따져야하기 때문에 해결되지 않는다.
0~n 문자열,0~m 문자열 모두 "12" 로 시작하는데 n,m 이 서로 일치하지 않는다면 다른 문자열이다.
(e.g "125", "1273")하지만 위 코드에서는 0~l 까지의 문자열 비교하므로 부분적인 문자열이 포함되어있기만 하는지 확인한다.
즉, 문자열 간 비교를 해야하는데 부분적 비교를 하므로 오류가 발생함
따라서 위 코드는 아래와 같은 케이스가 통과되지 않음
ph = ["123", "1197", "557713", "11987543"] # True
이제 그럼 단순하게 접근을 해보자.
이중 loop 를 통해 모든 원소 간 비교를 (1:1) 하여 포함관계인지를 확인하자
-- startswith()
/ __contains__()
를 사용하면 str 의 포함유무를 확인 할 수 있다.
-- 속도 측면에서는 __contains__()
가 조금 더 빠르다. __contains__()
는 정적 타입만을 지원하는 반면, startswith()
은 모든 동적 타입을 지원하기 때문이다.
stackoverflow : Why is string's startswith slower than in?
def solution(phone_book):
sortedPhoneBook = sorted(phone_book,key=lambda x : len(x))
for i in range(0,len(sortedPhoneBook)):
for j in range(0,len(sortedPhoneBook)):
if(i == j):
continue
else:
if(sortedPhoneBook[i].startswith(sortedPhoneBook[j])):
return False
return True
하지만 아쉽게도 시간복잡도를 초과한다.
왜냐하면 위 코드는 O(N^3) 이기 때문이다.
정렬 : O(NlogN) -- tim sort
loop : O(N^3) -- {이중 loop:O(N^2)} X {startswith():C}
* 길이가 1~20 의 문자열이므로 startswith()는 C로 무시할만하다.
따라서 제한조건인 1 이상 1,000,000 이하인 phone_book의 길이로 인해 최대 1,000,000 ^ 2 만큼의 시간복잡도를 가지게 된다
어떻게 하면 개선할 수 있을까??
이전 접근법의 문제는 간단하다.
"원소 A에 대한 loop X 원소 B에 대한 loop X 문자열 비교" 로 접근되었기 때문이다.
여기서 문자열 비교는 불가피하다.
그렇다면 loop 를 하나 제거해야만 한다. 어떻게 제거할 수 있을까?
바로 원소 간 1:1 비교가 아닌, hashMap(dict) 을 통해 존재여부 확인 하는 방법이다.
전체 원소에 대해 dict 을 정의한다.
loop 를 통해 각 원소를 substring, 해당 substring 이 dict 에 존재하는지 판단한다.
아래 예시를 살펴보자.
0 < N < M 길이 관계의 문자열이 존재한다고 보자.
0~N 문자열, 0~M 문자열 모두를 dict 에 저장한다.
이제 loop 를 통해 0~N 문자열, 0~M 문자열 각각에 접근한다.
이제 이 문자열들에 대해 0~1, 0~2 ,,, N,,, M 크기의 substring 을 하나씩 차근차근 만들게 된다.
이 때 0~M의 문자열의 substring 이 0~N 문자열이라고 가정해보자.
0~N 문자열은 dict 에 저장되어있으므로 해당 substring은 dict 에 존재한다.
따라서 substring 이 dict 에 있어 부분문자열이라고 판단, 비교로직을 종료하게 된다.
이제 코드를 살펴보자.
def solution(phone_book):
hashMap = {}
for nums in phone_book: # O(N)
hashMap[hash(nums)] = nums
for nums in phone_book:
arr = ""
for num in nums:
arr += num
if hash(arr) in hashMap and arr != nums:
return False
return True
접근법에 따라 하나씩 코드를 분석해보자.
hashMap = {}
for nums in phone_book: # O(N)
hashMap[hash(nums)] = nums
for nums in phone_book:
arr = ""
for num in nums:
arr += num
if hash(arr) in hashMap and arr != nums:
return False
이 접근법은 O(N) 시간복잡도이므로 훨씬 개선된 것을 볼 수 있다. -- 이전 : O(N^2)
전체 원소에 대해 dict 을 정의 : O(N)
substring 구한 뒤, dict 에 존재여부 판별 : O(1)
각 원소에 대한 loop : O(N)
원소의 각 index 에 대한 substring 계산 : C
* 각 원소의 길이가 1~20 이므로 거의 무시할만한 연산이다.
dict 에 존재하는지 판별 : O(1)
조금 더 영리하게 접근할 수 있다.
알파벳 순으로 정렬 이후 이웃 간 비교를 해주면 된다.
이에 따라 아래와 같이 접근할 수 있다.
phoneBook.sort()
for p1, p2 in zip(phoneBook, phoneBook[1:]):
for p1, p2 in zip(phoneBook, phoneBook[1:]):
if p2.startswith(p1):
return False
return True
위 코드는 O(N) 으로 인해 개선된 점을 알 수 있다.
zip()과 sort()를 통해 의존도는 높아졌지만, 코드도 간결해지고 더욱 빨라졌다.
zip() 을 통해 이웃 간 묶음 처리 : O(1)
묶음 처리에 대한 loop : O(N)
startswith() : C
* 각 문자열의 최대길이가 20이므로 무시할만하다.
Tistory : [프로그래머스] 전화번호 목록 문제 풀이(해시 Lv. 2) - 파이썬 Python
출처: https://coding-grandpa.tistory.com/86 [개발자로 취직하기:티스토리]
python-zip
stackoverflow : time-complexity-of-zip-in-python
stackoverflow : Why is string's startswith slower than in?