두 리스트 간 교집합 찾기

방법 1 - 리스트 순환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random
import time

# 문제 : 각 2만개의 랜덤한 값을 가진 리스트에서 겹치는(intersection) 원소 찾기
list_a = [str(random.randint(0, 10000)) for _ in range(20000)]
list_b = [str(random.randint(0, 10000)) for _ in range(20000)]

# 대조군 : list 순환
start = time.time()
intersection_1 = []
for b in list_b:
    if b in list_a:
        intersection_1.append(b)
print(time.time() - start)
1
3.3787968158721924

방법 2 - set 으로 변환 후 & 연산

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import random
import time

# 문제 : 각 2만개의 랜덤한 값을 가진 리스트에서 겹치는(intersection) 원소 찾기
list_a = [str(random.randint(0, 10000)) for _ in range(20000)]
list_b = [str(random.randint(0, 10000)) for _ in range(20000)]

# 실험군 : set(집합) 으로 변환한 뒤 & 연산
# 원리 : set 연산은 hash 연산을 사용하여 빠른 연산이 가능하다.
start = time.time()
intersection_2 = list(set(list_a) & set(list_b))
print(time.time() - start)

# 원소가 동일한지 확인
print(set(intersection_1) == set(intersection_2))

1
2
0.006021976470947266
True

원리

  • (1) set은 hash table 기반으로 비교하므로 빠르며
  • (2) & 연산은 각 원소를 “더 작은 set 에서 순회하며” hash lookup으로 검사하기 때문에 빠름
  • (3) 또한 Python의 for-loop 구조에서는 내부적으로 수행되는 작업이 있는데, 이를 수행하지 않아도 돼서 빠르다.

방법3 - set 의 intersection 메서드 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random
import time

# 문제 : 각 2만개의 랜덤한 값을 가진 리스트에서 겹치는(intersection) 원소 찾기
list_a = [str(random.randint(0, 10000)) for _ in range(20000)]
list_b = [str(random.randint(0, 10000)) for _ in range(20000)]

# 실험군 2 : set의 intersection 메서드 사용
start = time.time()
intersection_3 = set(list_a).intersection(set(list_b))
print(time.time() - start)

# 원소가 동일한지 확인
print(set(intersection_1) == set(intersection_3))
1
2
0.004326936173957672
True
  • 원리는 & 연산과 동일 (내부적으로 동일한 C 함수를 호출)

방법 2 원리 설명

set은 hash table 기반으로 비교해 빠르다

  • x in list_a 와 같이 리스트에서 접근하는 경우, 앞에서부터 하나씩 비교하며 O(n) 시간복잡도
  • x in set(list_a) 와 같이 집합에서 접근하는 경우, hash(x) 계산을 통해 바로 위치로 접근해 O(1) 시간복잡도

더 작은 set에서 순회

  • 파이썬에서 & 연산을 할 때에는 크기가 더 작은 쪽에서 순회를 하는 특징이 있다.
1
2
len(A) = 1000000
len(B) = 10
  • 위 코드에서 A & B 연산을 하면 Python은 아래와 같이 동작한다.
  • (1) 작은 쪽(B)를 순회한다 – 총 10회
  • (2) 각 원소를 A에서 lookup 한다 – O(1)
  • 결과적으로 10 * O(1) = O(10) 의 시간복잡도

반면에, 큰 쪽(A)에서 순회를 한다고 가정하면
1000000 회 * O(1) = O(1000000) 의 시간복잡도가 발생한다.

hash lookup

  • 값을 하나씩 비교하지 않고, hash(value) 로 계산한 위치에 바로 가서 존재 여부를 확인하는 방식
  • 반면에 list lookup은 앞에서부터 하나씩 값을 비교해가는 작동 방식이다.
  • 따라서 hash lookup 은 O(1) 의 시간복잡도 를 가지며
  • list lookup 은 O(n) 의 시간복잡도 를 가진다.
  • Python에서 hash lookup을 하는 자료구조는 set, dict 등이 있다.
1
2
3
4
5
6
7
8
# hash lookup
x in set(list_a)

# 동작 방식  
h = hash(x)
index = h % table_size # 그 칸에 x가 있는지만 확인

# -> O(1)
1
2
3
4
5
6
7
8
9
10
# list lookup
x in list_a

# 동작 방식
list_a[0] == x ?
list_a[1] == x ?
...
list_a[n] == x ?

# -> O(n)

Python의 for-loop 구조를 스킵

1
2
3
for x in list_a:
    if x in list_b:
        ...
  • Python 에서는 for-loop 이 수행될 때 아래와 같은 작업이 동반된다.
  • (1) 바이트코드 해석
  • (2) 객체 타입 검사
  • (3) 함수 호출
  • (4) 조건 분기
  • 이 모든 단계에 Python 인터프리터가 개입하므로, 수행되는 작업이 많다.
  • 이러한 for-loop 구조를 스킵하면, 작업 시간을 크게 줄일 수 있다.

Comments