web-dev-qa-db-ja.com

ネストされたループの複雑さ

序文:

  • この投稿では、O(n)Theta(n)の間の一般的な混乱を複雑さの表記として説明します。
  • 自分の好みに合った表記法を使用して、アルゴリズムについて説明する疑似コードを記述します。

私は知っていますが、ネストされたループと複雑さについてのもう1つの質問です。

最近、ネストされたアルゴリズムを作成しました。これは、より複雑なアプリケーションのボトルネックとして機能します。それはどういうわけかこのように見えました:

_while(firstLoopRunning) {
    foreach(element in myArray) {
        doSomeAction(element);
    }
}
_

DoSomeActionはO(1)にあるため、アルゴリズムはO(n²)にある必要があるという結論に私は鋭い目を向けました。

ただし、ネストされたループは異なる複雑さを持つ可能性があることを知っています。たとえば、以下はO(n.log(n))にあります。

_for(i in 0..n) // n is constant in this loop
    for(j in 0..i) // j goes up to i and not to a constant
        print(j);
_

次に、myArrayには最大長があることを知っています。この特定のケースでは、my knowの仕様により、myArrayに8要素を超えることはないため、アルゴリズムは次のようになります。

_while(firstLoopRunning) {
    // Let's do this one in Java, myArray would actually be an ArrayList
    try {
        doSomeAction(myArray.get(0));
        doSomeAction(myArray.get(1));
        doSomeAction(myArray.get(2));
        doSomeAction(myArray.get(3));
        doSomeAction(myArray.get(4));
        doSomeAction(myArray.get(5));
        doSomeAction(myArray.get(6));
        doSomeAction(myArray.get(7));
    } catch(ArrayOutOfBoundsException ex) {
       // Just a lazy way for me to avoid checking if the index exists
    }
}
_

そして、ほら!これはO(n)にあります!

さらに、コンパイラは通常、次のようなfakeループを変換するという事実を知っています。

_for(i in 0..8) // not really a loop
    someCall(i);
_

呼び出しのシーケンスに。

これは私に最初の結論をもたらしました:

長さが既知の有限の上限を持つ配列の反復はO(1)にあります。

私はこれについて正しいと思います(別名:間違っている場合は修正してください)。

一方、我々は有限のデータで作業しており、複雑性理論の要点は理論的に無限のデータで作業することです。

それでは、別の古典的な例を使用してみましょう。グリッド(=〜2次元配列)の正方形を反復処理しているので、O(n²)アルゴリズムは明確です。しかし、すべての正方形を1つのリストに入れて、これを変更するとどうなるでしょうか。

_// O(n²) algorithm over myGrid[][]
for(i in myGrid)
    for(j in myGrid[i])
        yieldAction(myGrid[i][j])
_

これに

_// O(n) algorithm over myFlatGrid[]
for(i in myFlatGrid)
    yieldAction(myFlatGrid[i mod rowLength][j])
_

基本的には同じことですが複雑さが異なりますが、実際にはループはありません。グリッドは両方の次元で拡大できるため、変数は実際には2次ですが、ある意味では、線形の方法で扱うことができます(それは価値がありませんが)。

しかし、私は何かを逃しているに違いありません。データを少しひねると、まったく同じ数の操作が実行されても、理論的な観点からアルゴリズムの複雑さを変更できるとはどういう意味ですか?

3
Pierre Arlaud

アルゴリズムの複雑さはネストされたループの数にリンクしているとお考えでしょう。そうではありません。次のコードはO(1)です。

for i in [1.. 10^15]:
    for j in [1.. 10^15]:
        for k in [1.. 10^15]:
            dosomethingO1()

複雑さは、操作数の増加率に関連しています。ループを展開してもそれは変わりません。そう:

foreach(element in myArray) {
    doSomeAction(element);
}

あります0(1) myArrayの要素数に制限がある場合。複雑さは、whileループの反復回数によって決まります。

グリッドの例について:nがセルの数であり、グリッドがネストされた配列として表されている場合でも、これらの配列をループすると、O(n)複雑になります。

10
Simon Bergot

nO(n)に何があるかについて明確な理解がないようで、困惑しています。

DoSomeActionはO(1)にあるため、アルゴリズムはO(n²)でなければならないという結論に私は鋭く目を向けました。

これは、両方のループがO(n)にある場合にのみ当てはまりますが、ループに複雑性がある場合O(p)およびO(q)の場合プログラム全体は複雑ですO(pq)など。あなたの例では、外側のループがn回実行され、長さp8以下のループをトリガーしているため、全体的にループはO(8n O(n))==によって制限された複雑さを持っています。

内部ループの複雑さが一定でない場合は、さらに細かい近似を行う必要があります。

ただし、ネストされたループは異なる複雑さを持つ可能性があることを知っています。たとえば、以下はO(n.log(n))にあります

そうではない。それを思い出します

1 + 2 + ... + n = n(n+1)/2 = O(n²)

ループの複雑さはO(n²)であり、O(n.log(n))ではありません。

7
user40989

はい。しかし、データを変更すると、nも変更されます。最初の例では、nは実際には定数です。そのため、アルゴリズム全体が実際に一定であるよりも意味があります。

2番目の例では、O(n^2)アルゴリズムをO(n)アルゴリズムに変換できますが、データ量はnから_n^2_に増加するため、最後に、全体の複雑さは同じままです。

Simonが示唆したように、O表記はnの絶対値ではなく、nの変更方法に基づいてアルゴリズムの時間がどのように変更されるかを示します。 O(n)アルゴリズムでは、時間が線形に増加するため、nが2倍に増加すると、時間は2倍に増加します。 O(n^2)を使用すると、時間は2乗で増加するため、nが2倍に増加すると、時間は4倍増加します。

重要なのは、データを変更することです。nの増加率も変わる可能性があります。グリッドの例のように。 sideの長さを2だけ長くすると、アルゴリズムは何があってもその4倍の時間がかかります。 O(n^2)では、nは辺の長さであり、O(n)では、nは辺の2乗に等しい合計アイテム数です。

2
Euphoric

Nとグリッドを取得してO(n ^ 2)を取得する場合、代わりに、たとえばN列とN行のグリッドについて説明する必要があります。

これで、Nが2倍になり、タイルが4倍になったことがわかります。

リストに追加したり、表記を変更したりしても、問題は常にO(n ^ 2)の複雑さになります。

0
Pieter B