Intersection of Two Arrays
整数の配列 nums1 と nums2 が与えられます。両方の配列に含まれるすべての値を、昇順に並べて返してください。どちらかの配列に何回含まれていても、共通する各値は答えに1回だけ含めます。
関数
- nums1integer-array
- 最初の整数リスト
- nums2integer-array
- 2つ目の整数のリスト
- 戻り値integer-array
- 両方のリストに含まれる値を、それぞれ1回ずつ、昇順で
制約
1 ≤ nums1.length, nums2.length ≤ 5000-105 ≤ nums1[i], nums2[i] ≤ 105- 少なくとも1つの値が両方の配列に含まれています。
例
- 入力
- nums1 = [6, 2, 9, 2, 4]nums2 = [4, 4, 1, 6]
- 出力
- [4, 6]
- 説明
4と6は両方の配列に含まれています。4はnums2に2回現れますが、1回だけ記載されており、2と9はnums2に一度も現れません。
- 入力
- nums1 = [-3, 0, 7]nums2 = [7, -3, -3, 5]
- 出力
- [-3, 7]
- 説明
-3と7は両方の配列に含まれています。昇順では-3が先に来ますが、nums2では7が先に来ます。
提出時に隠しテスト+16件
発展問題
nums1に10個の値があり、nums2にすでにソートされた100万個の値がある場合はどうでしょうか?どの方法を選びますか?二分探索は全体を順に調べる方法より速くなるでしょうか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
nums1の各値について、nums2全体を調べることができます。各配列に5000個の値がある場合、比較の回数は最大で2.5 × 10^7になります。あなたはどんな質問を何度も繰り返しているのでしょうか?繰り返される質問は「この値はもう一方の配列に含まれているか」です。片方の配列から作成したハッシュセットを使えば、平均して定数時間で答えられます。
nums1から集合を作ります。nums2を順に調べ、値が集合に含まれていたら、それを答えに追加して集合から削除します。これにより、後に現れる同じ値が再度追加されることはありません。返す前に答えをソートします。
解説
この問題を決めるポイントは2つあります。両方の配列にある値は、答えには1回だけ含めます。また、答えはソートされていなければなりません。すべてのペアを比較する方法でも解けますが、比較回数はn × m回になり、両方の配列に5000個の値がある場合は2.5 × 10^7回です。両方の配列をソートすれば、2つのポインターで共通の値を順番に見つけられます。また、片方の配列をハッシュセットにすれば、「この値はnums1に含まれているか?」を定数時間で判定できます。
すべてのペアを比較する
正しいが、最大のテストでは終わらない
考え方
nums1の各値について、nums2を検索します。最初に一致したところで検索を止め、すでに答えに含まれている値はスキップするので、[8, 8, 8, 8]を[8, 8]と照合すると、4個ではなく8が1個得られます。最後に答えをソートします。
この方法が正しいのは、nums1内の値が、nums2内にあるその値のコピーのいずれかと一致した場合に限り、答えに追加されるためです。また、スキップすることで、同じ値が2回追加されるのを防ぎます。
この方法が遅いのは、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両方をソートし、その後2つのポインターで走査します
考え方
ソートすると、例1は[2, 2, 4, 6, 9]と[1, 4, 4, 6]になります。ポインターiを最初の配列の先頭に、jを2つ目の配列の先頭に置きます。小さい値を指すポインターを進めます。その値は、もう一方の配列のそれより後ろにあるどの値とも一致しません。そこにある値はすべて、それ以上の大きさだからです。両方のポインターが同じ値を指しているとき、その値は共通なので、それを追加して両方を進めます。
この例では、2 > 1のときjを進め、どちらの2も4より小さいのでiを進め、4 = 4のとき4を追加します。2つ目の4は6より小さいのでjを進め、6 = 6のとき6を追加します。[2, 2, 3]と[2, 2]にある2のように、両方に複数回現れる値は複数回一致します。直前に追加した値と比較することで、同じ値を1つだけ残します。結果は、追加の処理なしでソートされた状態になります。
ソートの計算量は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 はセットにあるので、答えに追加します。2つ目の 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]に共通する値は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
2つの配列の積集合の時間計算量はどれくらいですか?
ハッシュセットを使うと、共通する値の検索には平均で O(n + m) かかり、答えとなる k 個の値をソートするとさらに O(k log k) かかります。セットの使用領域は O(n) です。両方の配列をソートし、2つのポインターで走査すると O(n log n + m log m) かかります。すべてのペアを比較すると O(n × m) かかります。
ハッシュセットと二つのポインターのどちらを使うべきでしょうか?
配列がソートされておらず、メモリに余裕がある場合はハッシュセットを使いましょう。最も作業量が少なくて済みます。両方の配列がすでにソートされている場合、またはメモリが限られていて配列をその場でソートしてもよい場合は、二つのポインターを使いましょう。この方法ではセットが不要で、答えを順番どおりに得られます。
共通部分に重複する値を残すにはどうすればよいですか?
ある値を2つの配列で出現する回数だけ含める場合、つまり [3, 1, 3, 3] と [3, 3] から [3, 3] を得るには、集合をカウントマップに置き換えます。nums1 の値を数え、nums2 の各値について、そのカウントが0より大きければその値を追加してカウントを減らします。2つのポインターで走査する際は、最後に追加した値との比較を削除します。
一方の配列がメモリに収まらないほど大きい場合、共通部分をどのように見つけますか?
収まる方の配列からハッシュセットを作成し、大きい方を分割して読み込みながら、各値をセットと照合し、一致したら削除します。メモリ使用量は小さい方の配列のサイズに抑えられます。どちらの配列も収まらない場合は、ディスク上で両方をソートし、ソート済みファイルに対して二ポインタ法を実行します。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
Python
def intersection(nums1, nums2):
# ここにコードを書いてくださいケース1
ケース2
入力
nums1 = [6, 2, 9, 2, 4] nums2 = [4, 4, 1, 6]
期待値
[4, 6]