Intersection of Two Arrays
정수 배열 두 개 nums1과 nums2가 주어집니다. 두 배열에 모두 나타나는 모든 값을 오름차순으로 정렬해 반환하세요. 각 공통 값은 어느 배열에서든 몇 번 반복되든 답에 한 번만 나타납니다.
함수
- nums1integer-array
- 첫 번째 정수 목록
- nums2integer-array
- 두 번째 정수 목록
- 반환값integer-array
- 두 목록 모두에 있는 값들을 각각 한 번씩, 오름차순으로
제약 조건
1 ≤ nums1.length, nums2.length ≤ 5000-105 ≤ nums1[i], nums2[i] ≤ 105- 두 배열 모두에 적어도 하나의 값이 있습니다.
예제
- 입력
- nums1 = [6, 2, 9, 2, 4]nums2 = [4, 4, 1, 6]
- 출력
- [4, 6]
- 설명
4와6은 두 배열 모두에 있습니다.4는nums2에 두 번 나타나지만 한 번만 나열되고,2와9는nums2에 전혀 나타나지 않습니다.
- 입력
- nums1 = [-3, 0, 7]nums2 = [7, -3, -3, 5]
- 출력
- [-3, 7]
- 설명
-3과7은 두 배열 모두에 있습니다. 오름차순으로는nums2에서7이 먼저 나오지만,-3이 먼저 옵니다.
제출 시 숨은 테스트 +16개
후속 질문
nums1에 값이 10개 있고 nums2에 이미 정렬된 값이 백만 개 있다면 어떨까요? 어떤 접근 방식을 선택하시겠어요? 이진 검색이 전체를 순회하는 것보다 빠를 수 있을까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
nums1의 각 값에 대해nums2전체를 훑어볼 수 있습니다. 각 배열에 값이 5000개씩 있다면 비교 횟수는 최대2.5 × 10^7회입니다. 어떤 질문을 계속 반복해서 하고 있나요?반복되는 질문은 "이 값이 다른 배열에 있는가?"입니다. 한 배열로 만든 해시 집합을 사용하면 평균적으로 상수 시간에 이 질문에 답할 수 있습니다.
nums1에서 집합을 만드세요.nums2를 순회하며 값이 집합에 있으면 답에 추가하고 집합에서 제거하세요. 그러면 나중에 같은 값이 나와도 다시 추가되지 않습니다. 반환하기 전에 답을 정렬하세요.
풀이
이 문제를 결정하는 두 가지 세부 사항이 있습니다. 양쪽에 모두 반복해서 나타나는 값도 정답에는 한 번만 들어가며, 정답은 정렬된 상태여야 합니다. 모든 쌍을 비교하는 방법도 있지만 n × m번 비교해야 하며, 두 배열에 각각 5000개의 값이 있을 때는 2.5 × 10^7번 비교합니다. 두 배열을 정렬하면 두 포인터가 순서대로 공통 값을 찾을 수 있고, 한 배열의 해시 집합을 사용하면 상수 시간에 "이 값이 nums1에 있나요?"에 답할 수 있습니다.
모든 쌍 비교
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
nums1의 각 값을 가져와 nums2에서 해당 값을 찾습니다. 처음 일치하는 항목에서 검색을 멈추고, 이미 답에 있는 값은 건너뛰므로 [8, 8, 8, 8]과 [8, 8]을 비교하면 8이 네 개가 아니라 하나만 나옵니다. 마지막에 답을 정렬합니다.
nums1에 있는 어떤 값의 복사본이 nums2에서 일치하는 항목을 찾을 때만 그 값이 답에 들어가고, 건너뛰기 처리로 중복해서 들어가지 않으므로 이 방법은 정확합니다.
nums1의 각 값이 nums2 전체를 검색할 수 있으므로 이 방법은 느립니다. 각 배열에 값이 5000개 있으면 비교 횟수는 최대 2.5 × 10^7회이며, 큰 테스트에서는 대부분의 값이 일치하는 항목을 찾지 못하므로 대부분의 검색이 끝까지 진행됩니다.
알고리즘
- 빈 답변 목록으로 시작합니다.
nums1의 각 값a에 대해, 이미 답변에 있으면 건너뜁니다.- 그렇지 않으면
nums2를 탐색합니다.a와 같은 첫 번째 값에서a를 답변에 추가하고 탐색을 중단합니다. - 답변을 오름차순으로 정렬한 뒤 반환합니다.
def intersection(nums1, nums2):
result = []
for a in nums1:
if a in result:
continue
# Look for a anywhere in nums2.
for b in nums2:
if a == b:
result.append(a)
break
result.sort()
return result두 배열을 모두 정렬한 다음, 두 개의 포인터를 사용해 순회하세요
핵심 아이디어
정렬하면 예제 1은 [2, 2, 4, 6, 9]와 [1, 4, 4, 6]이 됩니다. 첫 번째 배열의 시작 부분에 포인터 i를, 두 번째 배열의 시작 부분에 j를 둡니다. 더 작은 값의 포인터를 앞으로 이동합니다. 다른 배열에서는 모든 값이 그 값 이상이므로, 그 값은 다른 배열의 더 뒤쪽에 있는 어떤 값과도 일치할 수 없습니다. 두 포인터가 같은 값을 가리키면 그 값은 양쪽에 모두 있으므로 결과에 추가하고 두 포인터를 모두 이동합니다.
예제에서는 2 > 1이므로 j를 이동하고, 두 2 모두 4보다 작으므로 i를 이동합니다. 4 = 4에서는 4를 추가하고, 두 번째 4는 6보다 작으므로 j를 이동합니다. 6 = 6에서는 6을 추가합니다. [2, 2, 3]과 [2, 2]의 2처럼 양쪽에 여러 번 나타나는 값은 한 번 이상 일치합니다. 마지막으로 추가한 값과 비교하면 중복 항목 하나를 유지할 수 있습니다. 별도의 단계 없이 결과가 정렬된 상태로 만들어집니다.
정렬에는 O(n log n + m log m)의 비용이 들고, 각 단계에서 포인터가 하나 이상 이동하므로 배열을 훑는 데는 O(n + m)의 비용이 듭니다. 대부분의 구현은 복사본을 정렬하므로 O(n + m)의 메모리가 필요합니다. 입력을 재배열해도 된다면 C 코드처럼 제자리에서 정렬하면 되고, 추가 메모리는 결과에 필요한 것뿐입니다.
알고리즘
- 두 배열을 정렬합니다.
i = 0과j = 0으로 설정합니다.- 두 포인터가 모두 배열 안에 있는 동안 더 작은 값을 가리키는 포인터를 이동합니다.
- 값이 같으면, 마지막으로 추가한 값과 같지 않은 경우 해당 값을 추가한 다음 두 포인터를 모두 이동합니다.
- 답을 반환합니다.
def intersection(nums1, nums2):
a = sorted(nums1)
b = sorted(nums2)
i, j = 0, 0
result = []
while i < len(a) and j < len(b):
if a[i] < b[j]:
i += 1
elif a[i] > b[j]:
j += 1
else:
# A shared value: keep it once, even if it repeats.
if not result or result[-1] != a[i]:
result.append(a[i])
i += 1
j += 1
return result첫 번째 배열의 해시 집합
핵심 아이디어
nums1의 모든 값을 해시 집합에 넣습니다. 예제 1에서 집합은 {6, 2, 9, 4}입니다. 중복된 2는 집합에 들어갈 때 하나로 합쳐집니다. 그런 다음 nums2를 순회하며 각 값이 집합에 있는지 상수 시간에 확인합니다. 첫 번째 4는 집합에 있으므로 결과에 추가합니다. 두 번째 4는 추가하면 안 되므로 일치하는 순간 집합에서 해당 값을 제거합니다. 1은 집합에 없고 6은 있으므로 결과는 [4, 6]입니다.
일치하는 값을 제거하면 각 값이 한 번만 포함됩니다. 첫 번째로 일치한 뒤에는 해당 값이 집합에서 사라지므로, nums2의 이후 중복 항목은 집합에서 값을 찾지 못합니다. 추가되는 모든 값은 두 배열에 모두 있고, 두 배열에 공통으로 있는 모든 값은 nums2에서 처음 등장할 때 추가됩니다.
집합을 만들고 순회하는 데 평균 O(n + m)이 걸립니다. 결과는 nums2의 순서대로 나오므로 마지막에 정렬합니다. 결과에는 k ≤ min(n, m)개의 값이 들어 있으므로 정렬에는 O(k log k)가 걸립니다. C에는 내장 집합이 없으므로 C 코드는 value + 10^5를 인덱스로 사용하는 플래그 배열을 씁니다. 값의 범위가 제한되어 있기 때문에 이 방법을 사용할 수 있습니다.
알고리즘
nums1로 해시 집합first를 만듭니다.nums2의 각 값이first에 있으면 정답에 추가하고first에서 제거합니다.- 정답을 오름차순으로 정렬합니다.
- 정답을 반환합니다.
def intersection(nums1, nums2):
first = set(nums1)
result = []
for num in nums2:
if num in first:
result.append(num)
# Remove it so a repeat in nums2 is not added twice.
first.remove(num)
result.sort()
return result
함정과 경계 사례
여기서 오답이 나오는 주된 원인은 중복된 값과 출력 순서입니다.
- 값이 일치할 때마다 추가하는 경우.
[2, 2, 3, 3, 3]과[3, 2, 2]에는 공통된 값이 두 개 있으므로, 정답은[3, 2, 2]가 아니라[2, 3]입니다. - 값을 찾은 순서대로 반환하는 경우. 해시 집합을 순회하면
nums2의 순서를 따르므로,[7, -3]도[-3, 7]로 정렬해야 합니다. - 숫자를 텍스트처럼 정렬하는 경우. 비교 함수 없이 JavaScript의
sort()를 사용하면 문자열을 비교하므로,[100000, 99]는 그 순서 그대로 유지됩니다.(x, y) => x - y를 전달하세요. - 집합의 교집합을 구한 뒤 순서를 잊는 경우. Python의
set(nums1) & set(nums2)는 순서가 정해지지 않은 상태로 올바른 값들을 찾으므로,sorted로 감싸세요. - 원래 값을 플래그 배열의 인덱스로 사용하는 경우.
-3은 유효한 인덱스가 아니므로, 먼저 모든 값에10^5를 더해 이동시키세요.
자주 묻는 질문4
두 배열의 교집합의 시간 복잡도는 무엇인가요?
해시 집합을 사용하면 공통 값을 찾는 데 평균적으로 O(n + m)이 걸리고, 답에 해당하는 k개의 값을 정렬하는 데 O(k log k)가 추가됩니다. 집합은 O(n)의 공간을 사용합니다. 두 배열을 정렬한 뒤 두 포인터를 사용해 순회하면 O(n log n + m log m)이 걸립니다. 모든 쌍을 비교하면 O(n × m)이 걸립니다.
해시 집합을 사용해야 할까요, 아니면 투 포인터를 사용해야 할까요?
배열이 정렬되지 않았고 메모리를 사용할 수 있다면 해시 집합을 사용하세요. 작업량이 가장 적습니다. 두 배열이 이미 정렬된 상태로 주어지거나 메모리가 부족해 제자리에서 정렬해도 된다면 투 포인터를 사용하세요. 순회에는 집합이 필요 없고 결과도 순서대로 생성됩니다.
교집합에서 중복된 값을 어떻게 유지하나요?
값이 두 배열에 나타나는 횟수만큼 결과에 포함되어야 한다면, 즉 [3, 1, 3, 3]과 [3, 3]이 [3, 3]을 반환해야 한다면, 집합을 개수 맵으로 바꾸세요. nums1의 값의 개수를 세고, 개수가 0보다 큰 nums2의 각 값은 결과에 추가한 다음 그 개수를 줄이세요. 두 포인터를 사용하는 순회에서는 마지막으로 추가한 값과 비교하는 검사를 제거하세요.
배열 하나가 메모리에 담기에는 너무 클 때 교집합은 어떻게 찾나요?
적합한 배열로 해시 집합을 만들고, 더 큰 배열은 조각별로 읽으면서 각 값을 집합과 대조하고 일치하면 제거합니다. 메모리 사용량은 더 작은 배열의 크기로 유지됩니다. 어느 배열도 메모리에 들어가지 않으면 둘 다 디스크에서 정렬한 다음, 정렬된 파일을 대상으로 투 포인터 방식으로 순회합니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def intersection(nums1, nums2):
# 여기에 코드를 작성하세요케이스 1
케이스 2
입력
nums1 = [6, 2, 9, 2, 4] nums2 = [4, 4, 1, 6]
기대값
[4, 6]