web-dev-qa-db-ja.com

単純なforループの実行時間

私はアルゴリズムを読んでいて、そのほとんどを理解しています。それでも、多くの問題に対処できるのは、さまざまなforループでの実行時間と同じくらい単純なことです。私を除いて、だれもが簡単に思っているので、ここでヘルプを検索します。

私は現在、自分の本からいくつかの抜粋を行っています。異なる実行時間を把握するために、それらを完成させるための助けが必要です。

演習のタイトルは次のとおりです。「次の各コードフラグメントの実行時間の(Nの関数としての)成長の順序を与える」

a:

int sum = 0;
for (int n = N; n > 0; n /= 2)
  for(int i = 0; i < n; i++)
    sum++;

b:

int sum = 0;
for (int i = 1; i < N; i *= 2)
  for(int j = 0; j < i; j++)
    sum++;

c:

int sum = 0;
for (int i = 1; i < N; i *= 2)
  for(int j = 0; j < N; j++)
    sum++;

N、n ^ 2、n ^ 3、Log N、N Log Nなど、さまざまな種類の実行時間/成長の順序を学習しました。しかし、forループのように、forループが異なる場合、どちらを選択するかを理解できません。 "n、n ^ 2、n ^ 3"は問題ではありませんが、これらのforループの実行時間が何であるかはわかりません。

ここに何かの試みがあります。y軸は「N」値を表し、x軸は外側のループが実行された時間を表します。左側の図は次のとおりです。右側の矢印=外部ループ、円=内部ループ、および現在のN値。それからそれを見るためだけにいくつかのグラフを描きましたが、これが正しいかどうかはわかりません。特に、Nが常に16のままである最後のグラフです。ありがとう。

My drawing

3
owwyess

バックグラウンド

  • 次のような任意のforループの場合:

    _for (int p = …; …)
        do_something(p);
    _

    このループの実行時間は、単に内部計算の実行時間の合計(つまり、この場合はdo_something(p))であることは明らかです。内部計算の実行時間mayはループ変数に依存することに注意してください。

    do_something(p)の実行時間がループ変数から独立している特殊なケースでは、実行時間は内部計算が実行される回数に比例します。

  • 通常、インクリメント(_sum++_)などの単純な演算は、一定時間の演算です。

  • 簡潔にするために、私はlogを使用して2を底とする対数を示します。さらに、pow(x, y)を使用してxを累乗したyを示します。これは、_^_がCファミリの言語の他の何かでよく使用されるためです。

問題点

この一連の問題で興味深い一致は、各計算の実行時間が実際にsumの最終値に比例することです(理由がわかりますか)。そのため、質問を簡略化して次のようにすることができます。 sumの値はパラメーターNによって異なりますか?

sumは代数的に計算できますが、正確な結果を知る必要はありません。良い見積もりを作成する必要があるだけです。

問題A

_int sum = 0;
for (int n = N; n > 0; n /= 2)
  for (int i = 0; i < n; i++)
    sum++;
_

外側のループは何回実行されますか? Nから始まり、ゼロになるまで毎回半分ずつ減少します。これは、2を底とする指数関数的減衰の(個別バージョン)にすぎません。

_n ∝ 1 / pow(2, p)    [approximately]
_

ここでは、pを、外側のループが繰り返されるたびに1ずつ増加するカウンターとして定義します。実際、これはまさにあなたのグラフが描くものです。

pはどこから始まりますか?便宜上、_p_start = 0_を選択するだけで、比例係数を決定できます。

_ n ≈ N / pow(2, p)
_

pはどこで終わりますか? nがゼロになるたびに!これは整数除算であるため、nは、N < pow(2, p)の場合にのみゼロになります。これから、そのp_end ≈ log(N)(2を底とする対数)を推定できます。経験があれば、この分析全体を簡単にスキップして、代わりにこの結論に直接ジャンプできます。

これで、pの代わりに変数nを使用してループを書き直すことができます(ここでもapproximately)。 everynのインスタンスを置き換える必要があることに注意してください。

_int sum = 0;
for (int p = 0; p < log(N); p++)
  for (int i = 0; i < N / pow(2, p); i++)
    sum++;
_

このようなループを作成する利点は、外側のループが何回繰り返されるかが明らかになることです。 (熱心な読者はこれが

内部ループはsumのインクリメントのみで構成され、N / pow(2, p)回繰り返されるため、上記を次のように書き換えることができます。

_int sum = 0;
for (int p = 0; p < log(N); p++)
  sum += N / pow(2, p);
_

(このループの実行時間は同じではなくなる可能性がありますが、sumの値は元の問題の実行時間を反映しています)。

このコードから、sumの値を次のように記述できます。

sum ≈ ∑[p = 0 to log(N)] (N / pow(2, p))

(randomAが指摘したように、これは geometric series なので、よく知られた閉形式 式の合計 があります。ここでは、計算に基づくより一般的な手法を使用します、 しかしながら。)

これは、総和を積分として近似することにより、さらに簡略化できます。

sum ≈ ∫[0 to log(N)] (N / pow(2, p)) dp ≈ N / ln(2)

これで、問題Aの実行時間はNに対して線形になります。

問題B

_int sum = 0;
for (int i = 1; i < N; i *= 2)
  for (int j = 0; j < i; j++)
    sum++;
_

ここでの問題も同様ですが、一部の手順は省略します。変数iは指数関数的に増加するため、変換を実行できます。

_i ≡ pow(2, p)
_

pは、_0_から始まりlog(N)で終わる反復ごとに1ずつ増加します。変数の置換後、ループは次のようになります。

_int sum = 0;
for (int p = 0; p < log(N); p++)
  for (int j = 0; j < pow(2, p); j++)
    sum++;
_

これは次のようになります。

_int sum = 0;
for (int p = 0; p < log(N); p++)
  sum += pow(2, p);
_

同じトリックを再度適用して、合計の閉形式の式を見つけることができます。これもO(N)です。

問題C

_int sum = 0;
for (int i = 1; i < N; i *= 2)
  for (int j = 0; j < N; j++)
    sum++;
_

内側のループの繰り返し回数は外側のループ変数に依存しないため、これは実際にはかなり簡単です。このため、すぐに次のように簡略化できます。

_int sum = 0;
for (int i = 1; i < N; i *= 2)
  sum += N;
_

ループは、問題Bと同じ指数動作をするので、log(N)回だけ実行されるため、これは次のように簡略化できます。

_int sum = 0;
sum += log(N) * N;
_

したがって、ランタイムはO(N log(N))です。

6
Rufflewind

実際の実行時間は、アルゴリズムの学習の理論的観点からのアルゴリズムの入力の関数としての実行時間のgrowthよりも重要ではありません。通常、現実の世界は異なります。

とにかく、本はおそらく異なるコード構造を説明する方法として実行時間について説明しています。

タイミングコードの簡単なJavaの例は次のようになります。ほとんどの一般的な言語は同じことを達成するための同様の方法を持っています。

long begin = System.currentTimeMillis();
// Do some stuff
long interval = System.currentTimeMillis() - begin;
System.out.println("The algorithm took " + interval + "ms to process");

リストしたコード構造ごとにこのプログラミングイディオムを使用し、結果を比較します。それは本があなたに言っていることのように聞こえます。

最新のCPUがどれほど高速であり、最新のコンパイラーが最適化にどれだけ効果的であるかを考えると、正確な比較を得るために何百万回もループすることをお勧めします。つまり、コード内の変数Nは非常に大きくなければなりません。また、各アルゴリズムを数回実行して、平均を取ります。

その理由は、コードに関連するオーバーヘッドがあるためです。正確な比較を行うには、 キャッシュミス などの要素を最小限に抑える必要があります。

0
user22815