어떤 자연수들의 list A가 주어진다고 가정해 봅시다. 이 list에는 어떤 수들이 존재하는지 확인하려면 어떻게 해야 할까요? 즉, 중복되는 것들을 제외하고 distinct한 원소들은 몇 개가 있을까요?
이 문제를 풀기 위해서는 다양한 방법들이 존재할 수 있습니다. 그 중에서 가장 먼저 생각해 볼 수 있는 방법이 distinct한 원소들만을 저장하는 list를 따로 만들어서 A의 원소들을 하나씩 그 list에 속해있는지 검사하면서 없으면 그 list에 추가시켜주는 방법이 있겠습니다. 하지만 이 방법은 distinct한 원소들을 저장하는 list가 커지면 커질수록 비효율적이 되는 알고리즘입니다. 왜냐하면 그만큼 뒤져야 하는 list가 커지면 비효율적이 되기 때문이죠. 이를 코드로 구현해보면,
def distinctElem_list(A):
distinct = [] #distinct한 원소들만 간직하는 list를 만들고,
for a in A: #A의 원소들을 하나씩 돌려가면서
if a not in distinct: #distinct에 없으면 추가합니다.
distinct.append(a)
return len(distinct) #마지막으로 해당 list의 크기를 리턴합니다.
잘 돌아가는지 확인하기 위해 간단하게 sample input을 만들어 보겠습니다.
>>> import random
>>> A = [random.randint(1, 5) for _ in range(10)]
>>> print A
[4, 2, 5, 2, 4, 5, 1, 4, 2, 5]
위에서 distinct한 원소는 [1, 2, 4, 5]로 4가 정답이겠죠? 그럼 잘 동작하는지 바로 확인해 보겠습니다.
>>> print distinctElem_list(A)
4
결과가 잘 나오네요. 그럼 성능은 어느정도 나올까요? 다음과 같이 테스트를 진행해 보겠습니다.
>>> bigA = [random.randint(1,20000) for _ in range(20000)]
>>> from time import time
>>> t0 = time()
>>> print distinctElem_list(bigA)
12668
>>> print "걸린시간 : %.2e" % (time()-t0)
걸린시간 : 2.31e+00
역시나 예상했던대로 조금 무겁네요… ㅠ 그럼 조금 더 빠르게 만들 수 있는 방법은 없을까요? 네. 꾸준히 연습을 하셨으면 느끼시겠지만, sorting을 활용한 trick을 사용하는 것입니다! 정렬을 하게 되면 무조건 다음 것하고만 비교하면 되기 때문에 $O(n\log n)$의 복잡도로 bounded되겠죠? 그럼 직접 구현해보고 성능을 확인해 봅시다.
def distinctElem_list_opt(A):
count = 0
A.sort() #일단 정렬을 하구요.
now = None
for a in A: #하나씩 빼면서 이웃하고만 비교하면 됩니다.
if a != now:
count += 1
now = a
return count
>>> t0 = time()
>>> print distinctElem_list_opt(bigA)
12668
>>> print "걸린시간 : %.2e" % (time()-t0)
걸린시간 : 1.11e-02
와우.. 거의 200배 차이가 나는 것을 보실 수 있죠?? 이는 $n$이 커지면서 더 심해진답니다. 하지만 우리는 여기서 만족할 수 없습니다. 오늘의 주인공인 set
자료구조를 활용하면 어떨까요? set
자료구조의 특징은 중복된 값이 없다는 것과, 그렇기 때문에 disjoint set operation(union, intersection, difference 등..)에 특화되어 있다는 장점이 있습니다. 나중에 set
의 진가를 활용할 날이 많이 있을것이니 일단은 굉장히 distinct한 element와 관련해서 효율적인 자료구조라는 것 정도만 알아둡시다 :) (참고: set
구조는 정렬이 되어있는 구조가 아닙니다! 따라서 list처럼 넣은 순서 그대로 있지 않습니다! 정렬하시고 싶으시면 sorted
함수를 사용하여 다시 list구조로 변환시키셔야 합니다!)
이제 이를 이용해서 같은 기능을 하는 코드를 짜 봅시다.
def distinctElem_set(A):
return len(set(A)) #코드는 완전 심플합니다! set에서 굉장히 편리한 constructor를 제공하기 때문이죠.
>>> t0 = time()
>>> print distinctElem_set(bigA)
12668
>>> print "걸린시간 : %.2e" % (time()-t0)
걸린시간 : 1.41e-03
앞서 우리가 크게 개선한 알고리즘보다 10배 가까이 빠른것을 확인하실 수 있죠? 이렇게 강력한 기능들을 가진 built-in 자료구조들을 잘 염두해 두고 적재적소에 잘 활용하도록 합시다 :)