従来のクイックソートアルゴリズムを実行していました。いくつかの場所でパーティションアルゴリズムを調べましたが、実装の違いは非常に微妙でした。ここに2つのアプローチがあります:アプローチ1:ピボットは最後の要素です
partition (arr[], low, high)
{
// pivot (Element to be placed at right position)
pivot = arr[high];
i = (low - 1) // Index of smaller element
for (j = low; j <= high- 1; j++)
{
// If current element is smaller than or
// equal to pivot
if (arr[j] <= pivot)
{
i++; // increment index of smaller element
swap arr[i] and arr[j]
}
}
swap arr[i + 1] and arr[high])
return (i + 1)
}
アプローチ2:ピボットは最初の要素です
Partition(a[], l, h)
{
pivot = a[l];
i - l; j = ;h
while(i < j)
{
while(a[i] <= pivot)
i++;
while(a[i] > pivot)
j--;
if(i < j)
swap(a[i], a[j]);
}
swap(a[l], a[j]);
return j;
}
アプローチ1ではforループは1回しか使用されないため、線形時間と呼ばれますが、アプローチ2ではネストされたループがあります。しかし、私の疑いによると、これらのネストされたループによって実行されるタスクは、線形時間以上のものは発生しません。
これに光を当ててください。前もって感謝します。
ネストされたループは線形時間の複雑さを持つことができますか?
はい。この関数を考えてみましょう:
def linear_time(n):
for i in range(sqrt(n)):
for j in range(sqrt(n)):
something_that_takes_constant_time()
時間の複雑さはO(sqrt(n) * sqrt(n)
)= O(n
)です。
覚えておいてください:
def foo(x):
part_a(x)
part_b(x)
# if part_a(x) has a time complexity of O(f(x))
# and if part_b(x) has a time complexity of O(g(x))
# then foo(x) has a time complexity of O(f(x) + g(x))
def foo(x):
loop f(x) times:
part_of_loop(x)
# if part_of_loop(x) has a time complexity of O(g(x))
# then foo(x) has a time complexity of O(f(x) * g(x))
このアルゴリズムを調べてみましょう:
Partition(a[], l, h)
{
pivot = a[l]; # O(1)
i = l; j = h; # O(1)
while(i < j) # runs ??? times
{
while(a[i] <= pivot) # runs O(h - l) times
i++; # O(1)
while(a[j] > pivot) # runs O(h - l) times
j--; # O(1)
if(i < j) # O(1)
swap(a[i], a[j]); # O(1)
}
swap(a[l], a[j]); # O(1)
return j; # O(1)
}
したがって、ループの内部の時間の複雑さはO((h-l)* 1 +(h-l)* 1 + 1 * 1)= O(h-l)です。しかし、ループ全体の時間の複雑さは何ですか? (i <j)がtrueである場合と同じ頻度で実行されます。
while(a[i] <= pivot)
の後のポスト条件はa[i] > pivot
であり、while(a[j] > pivot)
の後はa[j] <= pivot
になります。 i < j
の場合(私たちが気にする唯一の場合、そうでなければループはとにかく終了します!)、2つのインデックスの値が交換されます。つまり、a[i] <= pivot
とa[j] > pivot
がもう一度trueになります。つまり、ピボットの反対側にある左端と右端のインデックスを見つけて、すべてが正しく分割されるまで入れ替えます。では、そのループはどのくらいの頻度で実行されますか?本当に言うことはできません、それはピボットの「間違った」側にある要素の数に依存します。最初のループi
とj
の後のすべてのループは、少なくとも1ステップ互いに近づくため、上限は(h-l)/ 2 +になります。 1は複雑度O(h-l)です。
今、私は何かをスキップしました:外側のループが実行される回数は、内側のループが実行される回数に依存しています。内側のループの実行回数はO(h-l)に向かう傾向があるため、外側のループの実行回数は1に向かう傾向があり、その逆も同様です。彼らは一種のお互いの実行を食べています。前の段落で述べた上限は、内部ループが毎回1回だけ実行され、その複雑さがO(1)になるときに得られるものです。そして、内側のループがそれぞれa
およびb
回実行されるたびに、外側のループがO((h - l) / (a + b)
で実行される回数が判明しました。言い換えると、外側のループの内容の複雑度はO(a + b)
であり、外側のループの実行回数はO((h - l) / (a + b)
であり、ループ全体の時間の複雑度はO((h - l) / (a + b) * (a + b)) = O(h - l)
、したがって、このパーティションは線形時間で動作します。
(私はここにバグがあると思います:a[i] <= a[l]
がすべてl <= i <= h
の場合はどうなりますか?)
ネストされたループは、実際にはOとは無関係です。重要なのは、アイテムを処理する回数です。 generalでは、ループがネストを定義しますが、これは、掘り下げるとループが急速に小さくなる特殊なケースです。 (各反復で何らかの方法で非常に悪いピボットを選択した場合、ループは速く縮小せず、O(n ^ 2)ランタイムになります。リストの最初の項目を毎回選択して、すでにソートしようとしていることに注意してください-ソートされたリストがこれを引き起こします。)