Sort Colors
すべての値が 0、1、または 2 である配列 nums が与えられます。これらを赤、白、青などの3色だと考えてください。配列を並べ替えて、すべての 0 が先頭に来て、その次にすべての 1 が続き、最後にすべての 2 が来るようにし、それを返してください。
ライブラリのソート関数を使わずに解いてください。大切なのは、値について分かっていることを活用することです。
関数
- numsinteger-array
- 色はそれぞれ0、1、または2
- 戻り値integer-array
- 同じ値を、まずすべての0、次にすべての1、最後にすべての2の順に並べます
制約
1 ≤ nums.length ≤ 1.5 × 104- すべての
nums[i]は0、1、または2です。 - 色が欠けている場合があり、配列には色が1つだけ含まれている場合があります。
例
- 入力
- nums = [2, 1, 0, 2, 0, 1, 1]
- 出力
- [0, 0, 1, 1, 1, 2, 2]
- 説明
- 配列には0が2つ、1が3つ、2が2つ含まれているため、結果もそのとおり、0が2つ、次に1が3つ、そして2が2つです。
- 入力
- nums = [2, 0, 2]
- 出力
- [0, 2, 2]
- 説明
- 1はまったくありません。1つの0が先頭に移動し、その後に2つの2が続きます。
- 入力
- nums = [1]
- 出力
- [1]
- 説明
- 単一の値はすでに順序どおりなので、配列は変更されずに返されます。
提出時に隠しテスト+17件
発展問題
配列の長さよりもかなり小さい k 色の場合、3 色の代わりに何を変更しますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
異なる値は3つしか現れません。一般的なソートではできない、どのようなことができますか?
0、1、2の個数を数えて配列を書き換える方法は、2回の走査で行えます。1回の走査では、3つの領域が同時に拡大していく様子を思い浮かべてください。先頭に0、末尾に2、その間に1が並びます。
3つのインデックスを保持します:
low、mid、high。nums[mid]を読み取ります。0ならlowと交換し、2ならhighと交換し、1ならそのままにします。highとの交換後は、同じ位置をもう一度読み取ります。
解説
どのような並べ替えでも正しい順序になります。つまり、本当の問いは、3つの値によってどの処理を省けるかです。現れるのが0、1、2だけなので、それらを数えて、2回の走査で配列を書き換えられます。0の並びの終わりと2の並びの始まりを示す3つのポインターを使えば、1回の走査ですべての値を正しい位置に置くこともできます。この1回の走査で行う分割が、オランダ国旗アルゴリズムです。
手作業でバブルソート
正しいが、最大のテストでは終わらない
考え方
ライブラリのソートを使えば O(n log n) で済みますが、問題の条件でそれは禁止されています。面接官は、値が3種類しかないという事実を踏まえてどうするかを見たいからです。そこで基本となるのは、自分で書くソートです。正しく実装するのが最も簡単なのはバブルソートです。配列を順に見ていき、隣り合う2つの値の順序が逆なら入れ替えます。
1回の走査で、そのときに見つけた最大値が泡のように上昇して最後まで移動します。1回目の走査後には最後の位置が確定し、2回目の走査後には最後の2つの位置が確定するため、n-1 回走査すれば配列全体が整列します。[2, 1, 0]の場合、1回目の走査で2が最後まで移動し、[1, 0, 2]になります。2回目の走査では1と0が入れ替わります。
遅いのは、各走査でまだ位置が確定していないすべてのペアを比較するからです。合計の比較回数はおよそ n²/2 回です。n = 1.5 × 10^4 では比較回数が 10^8 回を超え、さらに最初に順序が逆になっているペアごとに入れ替えも発生します。その作業のどれも、値が3種類しかないという事実を活用していません。
アルゴリズム
- 配列に対して n-1 回のパスを実行します。
- 各パスで、まだ確定していない隣り合う要素の組
nums[j]とnums[j + 1]をすべて比較し、左側の要素のほうが大きければ入れ替えます。 - 番号が
doneのパス(0 から数えます)の後は、末尾のdone + 1個の位置に最終的な値が入るため、次のパスではその手前まで処理します。 numsを返します。
def sortColors(nums):
n = len(nums)
for done in range(n - 1):
# One pass: the largest value left so far bubbles to index n-1-done.
for j in range(n - 1 - done):
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
return numsそれぞれの色を数えてから、書き直しましょう
考え方
バブルソートは隣り合う要素の比較にすべての時間を費やしますが、どの値があるかはすでにわかっています。配列に0が2個、1が3個、2が2個あるなら、何も動かす前から答えは決まっています。0が2個、1が3個、2が2個です。重要なのは個数だけです。
そこで、配列を一度読み取り、それぞれの値を数えます。次に、先頭から配列を上書きします。count[0]個の0、続いてcount[1]個の1、そしてcount[2]個の2です。これが計数ソートです。等しい値は互いに入れ替えても同じなので、ここでは安全に使えます。1はどれも1なので、元の順序を保つ必要はありません。
これで2回の走査と3つのカウンターを使い、時間計算量はO(n)、空間計算量はO(1)です。条件を満たしており、色が多い場合には自然な解法です。この問題でよく知られている追加の問いは、配列を一度だけ読み取りながら同じことができるかどうかです。
アルゴリズム
- 3つのカウンターを作成し、すべて0にします。
- すべての値を読み取り、対応するカウンターを1増やします。
- 先頭から
count[0]個の0を書き、その後にcount[1]個の1、さらにcount[2]個の2を書きます。 numsを返します。
def sortColors(nums):
count = [0, 0, 0] # how many 0s, 1s and 2s
for x in nums:
count[x] += 1
i = 0
for color in range(3):
for _ in range(count[color]):
nums[i] = color
i += 1
return nums3つのポインターを使った1回の走査(オランダ国旗問題)
考え方
読み進めながら3つの領域を作ります。先頭に0、続いて1、末尾に2を置き、1と2の間に未読部分を残します。3つのインデックスで境界を示します。lowより前はすべて0、lowからmidの直前まではすべて1、highより後ろはすべて2で、nums[mid]からnums[high]まではまだ未読です。
nums[mid]を読みます。1はすでに自分の領域にあるので、midを進めます。0は先頭に属するので、nums[low]と交換し、lowとmidの両方を進めます。lowから戻ってくる値は1(または、まだ1が見つかっていない場合は同じ0)なので、すでに正しい位置にあります。2は末尾に属するので、nums[high]と交換してhighを戻しますが、highから来た値はまだ読んでいないため、midはそのままにします。
各ステップでmidを進めるかhighを戻すため、未読部分は毎回1セルずつ小さくなり、ループはnステップ後に終了します。[2, 0, 2]を追ってみましょう。最初の2を最後の2と交換し、highは1になります。インデックス0にはまだ2があり、これを0と交換するとhighは0になります。インデックス0には今度は0があり、これはそのままなので、結果は[0, 2, 2]になります。
アルゴリズム
low = 0、mid = 0を設定し、highを最後のインデックスに設定します。mid ≤ highの間、nums[mid]を読み取ります。- 値が0なら、
nums[low]と入れ替え、lowとmidを1つ右に進めます。 - 値が1なら、
midを1つ右に進めます。 - 値が2なら、
nums[high]と入れ替え、highを1つ左に進めます。midはそのままにします。 numsを返します。
def sortColors(nums):
# nums[:low] are 0s, nums[low:mid] are 1s, nums[high + 1:] are 2s.
low, mid, high = 0, 0, len(nums) - 1
while mid <= high:
if nums[mid] == 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
elif nums[mid] == 1:
mid += 1
else:
# The value swapped in from high is unread, so mid stays.
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
return nums
落とし穴と境界ケース
1回の走査で行うバージョンは短く、その中のほぼすべてのバグは、本来動くべきでないときにポインターが動くことが原因です。
highとの交換後にmidを前に進める。移動してきた値は未読です。[1, 2, 0]では、2が0と交換され、0を読み飛ばすと[1, 0, 2]が返されます。highが最後の未読インデックスであるのに、mid < highの間ループする。2つが一致したとき、そのセルはまだ未読です。[1, 0]では、ループは0を読む前に停止し、[1, 0]を返します。- 符号なしインデックスで
highが0未満になるのを許す。[2]のような2だけの配列では、highが-1になります。インデックスがusizeであるRustでは、Rustのコードが行っているように、代わりにhighを未読部分の1つ先に保ちます。 - すべての色が現れると仮定する。
[2, 0, 2]には1がなく、配列が1色だけを含むこともあります。ポインターの規則はどちらの場合も特別扱いなしで対処するため、特別な処理を追加しないでください。
よくある質問4
オランダ国旗問題とは何ですか?
エドガー・ダイクストラが提起した問題です。3色のオブジェクトが一列に並んでいるとき、オランダ国旗の赤、白、青を、交換だけを使って1回の走査で色ごとにまとめます。「Sort Colors」は、数値0、1、2を使った同じ問題です。彼の解法は、low、mid、highを使う3ポインターによる分割です。
Sort Colors の時間計算量と空間計算量は何ですか?
1 回の走査による解法は、各ステップで未読部分を 1 セルずつ縮小するため、O(n) 時間で実行されます。追加の領域は O(1) です。3 つのインデックスと、交換に使う一時値を使用します。計数ソートも同じ計算量ですが、配列を 2 回読み取ります。
high と入れ替えた後、なぜ mid は動かないのですか?
highから戻ってきた値はまだ読み取られていないため、0、1、または2の可能性があります。それを越えてmidを進めると、中央に0または2が残ることになります。lowとの交換は異なります。lowとmidの間はすべて1なので、戻ってくる値はわかっており、midを先に進めることができます。
Sort Colors の回答として計数ソートは許容されますか?
時間計算量 O(n)、空間計算量 O(1) の条件を満たし、多くの面接官は最初の回答としてこれを受け入れます。次に、1回の走査で処理する方法を尋ねられることを想定しておきましょう。それが3ポインターによる分割です。色が多い場合は、カウントする方法のほうが適しています。分割では3つのグループにしか分けられないためです。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
Python
def sortColors(nums):
# ここにコードを書いてくださいケース1
ケース2
ケース3
入力
nums = [2, 1, 0, 2, 0, 1, 1]
期待値
[0, 0, 1, 1, 1, 2, 2]