任意の関数のBigO表記を計算する方法を自分に教えようとしています。教科書でこの機能を見つけました。この本は、関数がO(n2)。これはなぜなのかを説明していますが、私は従うのに苦労しています。なぜこれがそうなのか、背後にある数学を誰かが私に示すことができるのだろうかと思います。基本的に、私はそれがO(n3)、しかし私は独立してO(n2)
A、B、Cの3つの数値シーケンスが与えられたとします。個々のシーケンスには重複する値が含まれていないと仮定しますが、2つまたは3つのシーケンスにいくつかの数値がある場合があります。 3ウェイセットの素性の問題は、3つのシーケンスの交差が空であるかどうか、つまり、x∈A、x∈B、x∈Cのような要素xがないことを決定することです。
ちなみに、これは私にとって宿題の問題ではありません-その船は何年も前に出航しました:)、私がもっと賢くなろうとしているだけです。
def disjoint(A, B, C):
"""Return True if there is no element common to all three lists."""
for a in A:
for b in B:
if a == b: # only check C if we found match from A and B
for c in C:
if a == c # (and thus a == b == c)
return False # we found a common value
return True # if we reach this, sets are disjoint
[編集]教科書によると:
改良版では、運が良ければ時間を節約するだけではありません。ディスジョイントの最悪の実行時間はO(n2)。
私が従うのに苦労している本の説明はこれです:
全体的な実行時間を明らかにするために、コードの各行の実行に費やされた時間を調べます。 Aでのforループの管理にはO(n)時間を必要とします。Bでのforループの管理には合計O(n2)時間。ループはn回実行されるため。テストa == bが評価されますO(n2)回。残りの時間は、一致する(a、b)ペアがいくつ存在するかによって異なります。前述のように、このようなペアは最大でn個あり、Cを介したループの管理とそのループの本体内のコマンドは、最大でO(n2)時間。費やされた合計時間はO(n2)。
(そして適切な信用を与えるために...)本は:Pythonのデータ構造とアルゴリズム、Michael T. Goodrichらによる、Wiley Publishing、pg。135
[編集]正当化。以下は、最適化前のコードです。
def disjoint1(A, B, C):
"""Return True if there is no element common to all three lists."""
for a in A:
for b in B:
for c in C:
if a == b == c:
return False # we found a common value
return True # if we reach this, sets are disjoint
上記では、これがO(n3)、各ループを最大限に実行する必要があるためです。この本は、(最初に与えられた)簡略化された例では、3番目のループはO(n2)、したがって、複雑さの方程式はk + O(n2)+ O(n2)最終的にO(n2)。
私はこれが事実であることを証明することはできませんが(したがって質問)、簡略化されたアルゴリズムの複雑さは少なくとも元のアルゴリズムよりも少ないことに読者は同意できます。
[編集]そして、簡略化されたバージョンが二次であることを証明するために:
if __name__ == '__main__':
for c in [100, 200, 300, 400, 500]:
l1, l2, l3 = get_random(c), get_random(c), get_random(c)
start = time.time()
disjoint1(l1, l2, l3)
print(time.time() - start)
start = time.time()
disjoint2(l1, l2, l3)
print(time.time() - start)
収量:
0.02684807777404785
0.00019478797912597656
0.19134306907653809
0.0007600784301757812
0.6405444145202637
0.0018095970153808594
1.4873297214508057
0.003167390823364258
2.953308343887329
0.004908084869384766
2番目の差は等しいので、単純化された関数は確かに2次です。
[編集]そしてさらにさらなる証拠:
最悪の場合(A = B!= C)を想定すると、
if __name__ == '__main__':
for c in [10, 20, 30, 40, 50]:
l1, l2, l3 = range(0, c), range(0,c), range(5*c, 6*c)
its1 = disjoint1(l1, l2, l3)
its2 = disjoint2(l1, l2, l3)
print(f"iterations1 = {its1}")
print(f"iterations2 = {its2}")
disjoint2(l1, l2, l3)
収量:
iterations1 = 1000
iterations2 = 100
iterations1 = 8000
iterations2 = 400
iterations1 = 27000
iterations2 = 900
iterations1 = 64000
iterations2 = 1600
iterations1 = 125000
iterations2 = 2500
2番目の差分検定を使用すると、最悪の場合の結果は正確に2次になります。
本は確かに正しい、そしてそれは良い議論を提供します。タイミングはアルゴリズムの複雑さの信頼できる指標ではないことに注意してください。タイミングは特別なデータ分布のみを考慮するか、テストケースが小さすぎる可能性があります。アルゴリズムの複雑さは、リソースの使用状況またはランタイムが適切な大きな入力サイズを超えてスケーリングする方法を記述するだけです。
この本では、if a == b
ブランチは最大n回しか入力されないため、複雑度はO(n²)であると主張しています。ループは依然としてネストされたものとして記述されているため、これは自明ではありません。それを抽出すると、より明確になります。
def disjoint(A, B, C):
AB = (a
for a in A
for b in B
if a == b)
ABC = (a
for a in AB
for c in C
if a == c)
for a in ABC:
return False
return True
このバリアントは、中間結果を表すためにジェネレーターを使用します。
AB
には最大n要素があり(入力リストに重複が含まれないことが保証されているため)、ジェネレータを生成しますO(n²)の複雑さを取ります。ABC
の生成には、長さnのジェネレータAB
および長さnのC
のループが含まれます。そのアルゴリズムの複雑さもO(n²)になるようにします。入力リストのペアは順次チェックできるため、任意の数のリストが互いに素であるかどうかをO(n²)時間で判断できるということになります。
この分析は、すべてのリストが同じ長さであると想定しているため、不正確です。より正確には、AB
は最大でmin(| A |、| B |)の長さを持ち、生成には複雑さO(| A |•| B |)があると言えます。 ABC
の生成には複雑さO(min(| A |、| B |)•| C |)があります。全体の複雑さは、入力リストがどのように並べられるかに依存します。 | A | ≤| B | ≤| C |最悪の場合のO(| A |•| C |)の合計が得られます。
入力コンテナーがすべての要素を反復処理する必要があるのではなく、高速のメンバーシップテストを許可している場合、効率が向上する可能性があることに注意してください。これは、バイナリ検索を実行できるように並べ替えられている場合や、ハッシュセットである場合などです。明示的にネストされたループがない場合、これは次のようになります。
for a in A:
if a in B: # might implicitly loop
if a in C: # might implicitly loop
return False
return True
またはジェネレーターベースのバージョンでは:
AB = (a for a in A if a in B)
ABC = (a for a in AB if a in C)
for a in ABC:
return False
return True
想定されている各リストですべての要素が異なる場合は、Aの各要素に対してCを1回だけ反復できます(Bに等しい要素がある場合)。したがって、内部ループは合計O(n ^ 2)です。
個々のシーケンスには重複が含まれていないと想定します。
非常に重要な情報です。
それ以外の場合、AとBが等しく、n回複製された1つの要素が含まれている場合、最適化されたバージョンの最悪の場合でもO(n³)になります。
i = 0
def disjoint(A, B, C):
global i
for a in A:
for b in B:
if a == b:
for c in C:
i+=1
print(i)
if a == c:
return False
return True
print(disjoint([1] * 10, [1] * 10, [2] * 10))
出力:
...
...
...
993
994
995
996
997
998
999
1000
True
したがって、基本的に、著者はO(n³)の最悪のケースは発生しないはずであると想定し(なぜですか?)、最悪のケースがO(n²)であることを「証明」します。
実際の最適化は、O(1)への包含をテストするためにセットまたはディクテーションを使用することです。その場合、disjoint
はすべての入力に対してO(n)になります。
本が使用する用語に物事を入れるには:
a == b
は最悪の場合のO(n2)。
3番目のループの最悪のケースでは、a
のすべてのA
がB
に一致するため、3番目のループは毎回呼び出されます。 a
がC
に存在しない場合は、C
セット全体を実行します。
つまり、a
ごとに1回、c
ごとに1回、つまりn * nです。オン2)
したがって、O(n2)+ O(n2)あなたの本が指摘すること。
最適化された方法の秘訣は、手抜きをすることです。 aとbが一致する場合のみ、cは一見の価値があります。これで、最悪の場合でも各cを評価する必要があることを理解できます。本当じゃない。
おそらく最悪のケースは、a == bのすべてのチェックが一致を返すため、a == bのすべてのチェックがCで実行されるということです。しかし、これの条件は矛盾しているので、これは不可能です。これを機能させるには、同じ値を含むAとBが必要です。それらの順序は異なる場合がありますが、Aの各値はBの一致する値を持つ必要があります。
これがキッカーです。これらの値を整理する方法はないため、各aについて、一致を見つける前にすべてのbを評価する必要があります。
A: 1 2 3 4 5
B: 1 2 3 4 5
一致する1が両方のシリーズの最初の要素であるため、これはすぐに実行されます。どうですか
A: 1 2 3 4 5
B: 5 4 3 2 1
これはAの最初の実行で機能します。Bの最後の要素のみがヒットします。しかし、Bの最後のスポットが既に1で占められているため、Aを介した次の反復はすでに高速である必要があります。実際、今回は4回の反復しかかかりません。そして、これは次の反復ごとに少し良くなります。
今は数学者ではないので、これがO(n2))になることを証明することはできませんが、下駄でそれを感じることができます。