この関数の最悪の場合はなぜO(n ^ 2)ですか?


44

私は、任意の関数のBigO表記を計算する方法を自分自身に教えようとしています。この機能は教科書で見つけました。この本は、関数がO(n 2)であると断言しています。これがなぜなのかを説明していますが、私はそれに従うのに苦労しています。これがなぜそうなのか、誰かが数学を教えてくれるのではないかと思います。基本的に、O(n 3)より小さいことを理解していますが、O(n 2)に単独で着陸することはできませんでした

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(n 2)であると主張します。

私が従うのに苦労している本の説明はこれです:

全体の実行時間を説明するために、各コード行の実行に費やされた時間を調べます。Aに対するforループの管理にはO(n)時間を必要とします。Bに対するforループの管理は、そのループがn回実行されるため、合計O(n 2)時間を占めます。テストa == bはO(n 2)回評価されます。残りの時間は、一致する(a、b)ペアの数に依存します。前述したように、このようなペアは最大でn個あるため、Cを介したループの管理、およびそのループの本体内のコマンドは、最大でO(n 2)時間を使用します。費やされた合計時間はO(n 2)です。

(そして、適切な信用を与えるために...)本は、マイケル・T・グッドリッチらによるPythonのデータ構造とアルゴリズムです。すべて、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(n 3)であることが明確にわかります。本は、(最初​​に与えられた)単純化された例では、3番目のループはO(n 2)の複雑さだけであると断言するので、複雑さの方程式はk + O(n 2)+ O(n 2) O(n 2)。

これが事実であることを証明することはできませんが(質問)、簡略化されたアルゴリズムの複雑さは少なくとも元のものよりも小さいことに読者は同意できます。

[編集]そして、簡易版が2次であることを証明するために:

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次です。

ここに画像の説明を入力してください


6
本が間違っているか、転記が間違っています。
candied_orange

6
いや。引用の程度にかかわらず、間違っています。ビッグO分析を行うときに、これらの方法が最悪の方法であると単純に仮定できない理由を説明するか、得られた結果を受け入れます。
candied_orange

8
@candied_orange; 私は自分の能力を最大限に生かした正当化を追加しました-私の強いスーツではありません。私は、あなたが本当に間違っているかもしれないという可能性を再び考慮に入れるようお願いします。あなたはあなたの主張を正当にとった。
SteveJ

8
乱数は最悪のケースではありません。それは何も証明しません。
Telastyn

7
ああ。はい。「何のシーケンスは、重複する値を持っていません」変更C以来最悪の場合できる唯一のトリガー不満については申し訳ありません任意のA.に一回行います-私は土曜日に遅くstackexchange上にあるために得るものです:D
Telastyn

回答:


63

この本は確かに正しいものであり、良い議論を提供します。タイミングはアルゴリズムの複雑さの信頼できる指標ではないことに注意してください。タイミングは特別なデータ分散のみを考慮するか、テストケースが小さすぎる可能性があります。アルゴリズムの複雑さは、リソース使用量またはランタイムが適切に大きい入力サイズを超えてスケ​​ーリングする方法のみを示します。

この本は、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発電にわたるループを伴うAB長さのNとにわたってC長さのN、そのアルゴリズムの複雑さであるようにO(n²)も同様です。
  • これらの操作はネストされていませんが、独立して実行されるため、合計の複雑さはO(n²+n²)= 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

4
この魔法のn変数を廃止し、実際の変数について話をした場合、これは非常に明確になります。
アレクサンダー

15
@code_dreddいいえ、そうではありません。コードに直接接続していません。これlen(a) == len(b) == len(c)は、時間の複雑さの分析のコンテキストでは真実ですが、会話を混乱させる傾向があることを想定した抽象化です。
アレクサンダー

10
おそらく、OPのコードは最悪のケースの複雑さを持っていると言っているO(| A |•| B | + min(| A |、| B |)•| C |)は理解を促すのに十分でしょうか?
パブロH

3
タイミングテストに関するもう1つのこと:あなたが知ったように、それらは何が起こっているのかを理解するのに役立ちませんでした。一方、彼らはあなたが本が明らかに間違っているというさまざまな間違っているが、強制的に述べられた主張に立ち向かうことに追加の自信を与えたようですので、それは良いことです、そして、この場合、あなたのテストは直感的な手を振っています。理解するために、より効果的なテスト方法は、各ループのエントリでブレークポイントを使用してデバッガーで実行する(または変数の値の出力を追加する)ことです。
sdenham

4
「タイミングはアルゴリズムの複雑さの有用な指標ではないことに注意してください。」「有用」ではなく「厳密」または「信頼できる」と言った方が正確だと思います。
累積

7

想定される各リストですべての要素が異なる場合、Aの各要素に対してCを1回だけ反復できることに注意してください(Bに等しい要素がある場合)。したがって、内側のループはO(n ^ 2)合計です


3

重複を含む個々のシーケンスはないと想定します。

は非常に重要な情報です。

それ以外の場合、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)になります。


あなたの最後のコメントは非常に興味深く、そのことを考えていませんでした。これは、3つのO(n)操作を連続して実行できるためだと示唆していますか?
SteveJ

2
入力要素ごとに少なくとも1つのバケットを持つ完全なハッシュを取得しない限り、O(1)への包含をテストできません。ソートされたセットには通常、O(log n)ルックアップがあります。平均コストについて話しているのでない限り、それは問題の対象ではありません。それでも、バランスのとれたバイナリセットがハードO(n log n)になるのは簡単です。
Jan Dorniak

@JanDorniak:素晴らしいコメント、ありがとう。少し厄介ですkey in dict。著者のように、の最悪のケースを無視しました。:-/私の防御では、重複した値を持つリストを作成するよりも、nキーとnハッシュの衝突を持つ辞書を見つけるのがはるかに難しいと思いnます。また、セットまたはディクテーションを使用すると、重複する値が実際に存在することもありません。したがって、最悪の場合は実際にはO(n²)です。回答を更新します。
エリックドゥミニル

2
@JanDorniak C ++の赤黒木とは対照的に、セットと辞書はPythonのハッシュテーブルだと思います。したがって、絶対最悪の場合は最悪で、検索では0(n)までですが、平均的な場合はO(1)です。C ++ wiki.python.org/moin/TimeComplexityの O(log n)とは対照的に。Pythonの質問であり、問​​題の領域が平均的なケースパフォーマンスの高い可能性につながることを考えると、O(1)の主張は貧弱なものではないと思います。
Baldrickk

3
私はここで問題を見ていると思う:著者が「個々のシーケンスには重複値が含まれないと仮定する」と言うとき、それは質問に答えるステップではない。それはむしろ、問題が対処される前提条件です。教育目的のために、これは興味のない問題をbig-Oに対する人々の直感に挑戦する問題に変えます-そして、O(n²)が間違っているに違いないと強く主張した人々の数から判断して、それで成功したようです。 ..また、ここでは意味がありませんが、1つの例でステップ数を数えることは説明ではありません。
sdenham

3

本が使用する用語に物事を入れるには:

チェックa == bが最悪の場合O(n 2)であることを理解しても問題ないと思います。

3番目のループの最悪の場合、すべてのain Aがin Bに一致するため、3番目のループが毎回呼び出されます。にa存在しない場合はCCセット全体を実行します。

言い換えれば、それは毎回a1回、毎回1回c、またはn * nです。O(n 2

したがって、あなたの本が指摘するO(n 2)+ O(n 2)があります。


0

最適化された方法の秘trickは、角を切ることです。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)で終わることを証明することはできませんが、下駄で感じることができます。


1
ここでは、要素の順序は役割を果たしません。重要な要件は、重複がないことです。引数は、ループを2つの別々のO(n^2)ループに変換できることです。全体を与えますO(n^2)(定数は無視されます)。
AnoE

@AnoE確かに、要素の順序は重要ではありません。これがまさに私が実証していることです。
マーティンマート

私はあなたがやろうとしていることを見て、あなたが書いていることは間違っていませんが、OPの観点から、あなたの答えは主に特定の思考の流れが無関係である理由を示しています。実際のソリューションに到達する方法を説明していません。OPは、これが注文に関連していると実際に考えていることを示すものではないようです。したがって、この回答がOPにどのように役立つかはわかりません。
AnoE

-1

最初は困惑しましたが、エイモンの答えは本当に役に立ちました。私は本当に簡潔なバージョンができるかどうかを見たいです:

ainの指定された値に対してA、関数はa可能なすべてのbin と比較しB、一度だけ実行します。したがって、指定された時間aに対してa == b正確に実行されますn

B重複が含まれていないため(リストに含まれていないため)、指定aされたものに対して最大で 1つの一致があります。(それが鍵です)。一致がある場合、a可能なすべてのに対して比較されます。cつまり、a == c正確にn回実行されます。一致a == cしないところはありません、まったく起こりません。

そのため、指定されたについて、比較または比較のaいずれかがあります。これはごとに発生するため、最良のケースは(n²)であり、最悪のケースは(2n²)です。n2na

TLDR:のすべての値aのすべての値と比較されたbとのすべての値に対してcではなく、すべてのに対して、組み合わせbc。2つの問題は加算されますが、増えません。


-3

このように考えてください。いくつかの数字は2つまたは3つのシーケンスにある場合がありますが、これの平均的なケースは、セットAの各要素に対して、bで徹底的な検索が実行されることです。セットAのすべての要素が繰り返されることが保証されますが、セットbの要素の半分未満が繰り返されることが暗示されます。

セットbの要素が反復されると、一致する場合に反復が発生します。つまり、この互いに素な関数の平均的なケースはO(n2)ですが、この最悪のケースはO(n3)になる可能性があります。本が詳細に入らなかった場合、おそらく答えとして平均的なケースが得られます。


4
この本は、O(n2)が最悪のケースであり、平均的なケースではないことをはっきりと示しています。
SteveJ

大きなO表記法による関数の説明は、通常、関数の成長率の上限のみを提供します。大きなO表記法には、記号o、Ω、ω、およびΘを使用して、漸近的成長率に関する他の種類の境界を記述するいくつかの関連表記法が関連付けられています。ウィキペディア-ビッグオー
candied_orange

5
「本が詳細に入らなかった場合、おそらく答えとして平均的なケースが得られるでしょう。」–オム、いや。明示的な資格がない場合、通常、RAMモデルの最悪のステップの複雑さについて話します。データ構造の操作について話し、コンテキストから明らかな場合、RAMモデルでの償却された最悪の場合のステップの複雑さについて話しているかもしれません。なければ、明示的な資格、我々は一般的になりません最良の場合、平均的なケース、予想される場合、時間の複雑さ、またはRAMを除く他のモデルについて話しています。
イェルクWミッターク
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.