前回のブログでは、ビッグO表記法と分析スキルの複雑さをまとめ、一般的な複雑さ分析の例をいくつか挙げました。このブログでは、次の4つのナレッジポイントをご覧ください。
- 最良の場合の時間の複雑さ
- 最悪の場合の時間の複雑さ(最悪の場合の時間の複雑さ)
- 平均ケース時間の複雑さ
- 償却時間の複雑さ
1.最良および最悪の場合の時間の複雑さ
古いルール、最初に例を見てみましょう:
1 // nは配列の長さを表し ます2 int find(int [] array、int n、int x) 3 { 4 int i = 0 ; 5 int pos = -1 ; 6 7 for(; i <n; ++ i) 8 { 9 if(array [i] == x)pos = i; 10 } 11 12 return pos; 13 }
このコードの機能は、順序付けられていない配列で変数xのインデックスを見つけることです。見つからない場合は、-1を返します。上記のコードの時間の複雑さがO(n)であることを確認することは難しくありません。
ただし、配列内のデータの一部を見つけるために、毎回配列全体を走査する必要はありません。途中で見つけた場合は、早く終了できます。上記のコードの処理は十分に効率的ではありません。ループからジャンプする処理を追加しましょう:
1 // nは配列の長さを表し ます2 int find(int [] array、int n、int x) 3 { 4 int i = 0 ; 5 int pos = -1 ; 6 7 for(; i <n; ++ i) 8 { 9 if(array [i] == x) 10 { 11 pos = i; 12 break ; 13 } 14 } 15 16 位置を返す; 17 }
この時点で、最適化されたコードが途中で飛び出してくるかもしれませんが、その時間の複雑さはまだO(n)ですか?配列の最初の要素がxの場合、時間の複雑さはO(1)になります。xが配列に存在しない場合、時間の複雑さはO(n)です。前述の方法では、この問題は解決されないようです。
したがって、ベストケースの時間の複雑さ、ワーストケースの時間の複雑さ、および平均ケースの時間の複雑さという3つの概念を導入する必要があります。最良のケース時間の複雑さと最悪のケース時間の複雑さを理解することは難しくありません。ここでは、平均的なケース時間の複雑さの分析に焦点を当てます。
第二に、平均時間の複雑さ
最良の場合の時間の複雑さと最悪の場合の時間の複雑さは極端すぎるため、発生する確率は大きくありません。平均ケース時間の複雑さを採用する方がより科学的です。
上記の例を引き続き使用して分析します。配列内のxの位置にはn + 1のケースがあり、0〜n-1であり、配列内にはありません。さまざまな状況で走査する必要がある要素の数を合計し、次に走査する必要がある要素の平均数であるn + 1で割ります。
前述の簡略化された方法によれば、係数、低次、および定数は削除され、平均時間の複雑度はO(n)です。
この結論は正しいですが、実際には、計算プロセスには少し問題があります。これらのn + 1ケースの確率は異なります。xは配列内にあるか、配列内にありませんが、これら2つのケースに対応する確率を計算することは困難です。表現を簡単にするために、確率を1/2に設定することもできます。xが配列内にある場合、各位置に現れる確率は同じ、1 / nです。要約すると、xが配列のどこかに現れる確率は1 /(2n)です。上記の確率を考慮すると、平均時間の複雑さの数式は次のようになります。
この値は、確率論における加重平均(期待値)です。したがって、平均時間の複雑さの正式名称は、加重平均時間の複雑度または予想時間の複雑さである必要があります。
確率を導入した後も、このコードの加重平均時間の複雑さはO(n)のままです。
ほとんどの場合、最良、最悪、平均のケース時間の複雑さを区別する必要はありません。多くの場合、1つの複雑さで需要を満たすことができます。同じコードブロックの時間の複雑さの大きさが異なる場合にのみ、これらの3つの複雑さの表記を使用して区別します。
3.時分割の複雑さ
これまでのところ、アルゴリズムの複雑性分析のほとんどを整理してきました。より高度な概念、償却時間の複雑さ、および対応する分析方法である償却分析(または償却分析)を見てみましょう。
償却時間の複雑さは平均時間の複雑さのように聞こえ、これら2つの概念は実際に非常に混同しやすいものです。前述のように、ほとんどの場合、最良、最悪、および平均の複雑さを区別する必要はありません。平均の複雑さは、いくつかの特別な場合にのみ使用されます。タイムシェアリングが複雑なアプリケーションシナリオは、より特殊で限定的です。
古いルール、私たちはまだ例を通して分析します:
1 // N長さの配列を示す配列 2 // コードArray.lengthとがnに等しい 3。 INT []配列= 新しい新しい INT [n]を、 4 int型の COUNT = 0 ; 5 6。 ボイド(INSERT INT ヴァル) 7。 { 8 if(count == array.length) 9 { 10 int sum = 0 ; 11 12 for(int i = 0 ; i <array.length; ++ i) 13 { 14 sum = sum + array [i]; 15 } 16 17 array [ 0 ] = sum; 18 カウント= 1 ; 19 } 20 21 array [count] = val; 22 ++ カウント。 23 }
上記のコードは、配列にデータを挿入する関数を実装しています。配列がいっぱいの場合、つまりコードでcount == array.lengthの場合、forループを使用して配列を走査して合計し、合計値の後に合計値を配列の最初の位置に配置して、新しいデータを配置します。挿入。ただし、配列の先頭に空き領域がある場合、データは配列に直接挿入されます。
このコードの時間の複雑さは何ですか?
理想的なケースでは、配列に空き領域があり、配列インデックスの位置にカウントとしてデータを挿入するだけでよいので、最良の場合の時間の複雑さはO(1)です。
最悪の場合、配列に空き領域がないため、配列の走査と合計を実行してからデータを挿入する必要があるため、最悪の場合の時間の複雑さはO(n)です。
平均時間の複雑さはどれくらいですか?答えはO(1)です。前述の確率的方法で分析することはできます。
配列の長さをnとすると、データ挿入の場所に従って配列をn個のケースに分割でき、各ケースの時間の複雑さはO(1)です。これに加えて、配列に空き領域がないときにデータが挿入される「特別な」状況があり、このときの時間の複雑さはO(n)です。さらに、これらのn + 1ケースの発生確率は同じで、1 /(n + 1)です。したがって、加重平均の計算方法によると、取得した平均時間の複雑さは次のようになります。
実際、この例の平均的な複雑さは、確率論の知識を導入することなく、それほど複雑である必要はありません。
find()とinsert()の2つの例を比較して説明します。
- 極端な場合、find()関数の複雑度はO(1)です。しかし、ほとんどの場合、挿入()の時間の複雑さはO(1)です。いくつかの場合にのみ、複雑度は比較的高くなりますO(n)。
- 挿入()関数、O(1)時間複雑度挿入、O(n)時間複雑度挿入の場合、発生頻度は非常に規則的であり、前後に特定のタイミング関係があり、一般的にO (n)挿入後、n-1 O(1)挿入操作が周期的に続きます。
この特別なシナリオでは、より簡単な分析方法である償却分析方法を導入しました。私たちは、償却分析を通じて得られた時間の複雑さの名前を持っています。これは、償却時間の複雑度と呼ばれます。
償却分析方法を使用して、アルゴリズムの償却時間の複雑さを分析する方法は?例として、引き続きinsert()を使用します。各O(n)挿入操作の後にはn-1 O(1)挿入操作が続くため、時間のかかる操作は次のn-1時間のかかる操作に分散されます。償却後、この連続操作のグループの平均時間の複雑さはO(1)です。これは、償却分析の一般的な考え方です。
データ構造に対して一連の連続操作を実行する場合、ほとんどの場合、時間の複雑性は非常に低く、場合によっては、時間の複雑性が比較的高く、これらの操作の間に順次的なタイミング関係があります。このグループの操作を一緒に分析して、時間の複雑性が高く時間のかかる操作を、時間の複雑性が低い他の操作に均等に分散できるかどうかを確認できます。償却時間の複雑度分析を適用できる状況では、通常、償却時間の複雑度は、最良のケースの時間の複雑度と等しくなります。
償却時間の複雑さは特別な平均時間の複雑さであり、それらを区別するために多くの労力を費やす必要はありません。私たちが最もマスターする必要があるのは、その分析方法である償却分析です。分析の結果が平均と呼ばれるか平均と呼ばれるかについては、これは重要ではなく単なる声明です。それらのアプリケーションシナリオの簡単な要約。遭遇した場合、その原理を理解できます。
4、コンテンツの概要
このブログ投稿では、4種類の時間の複雑さを紹介します。
- 最良の場合の時間の複雑さ
- 最悪の場合の複雑さ
- 平均時間の複雑さ
- 時分割の複雑さ
これらの複雑さの概念を導入する理由は、同じコードでも、異なる入力条件では複雑さの測定レベルが異なる可能性があるためです。これらの概念を導入すると、コードの実行効率をより包括的に表現できます。
考える質問
次のadd()関数の時間の複雑さを分析します。
1 // グローバル変数、サイズ10の配列、長さlen、添え字i。 2 int array [] = new int [ 10 ]; 3 int len = 10 ; 4 int i = 0 ; 5 6 // 要素を配列に 追加7 void add(int element) 8 { 9 if(i> = len ) 10 { 11 // 配列スペースが不足しているため、2倍のサイズの配列スペースを再申請してください 12 int new_array [] = new int [len * 2]; 13 // 元の配列配列のデータをnew_array 14 にコピーしますfor(int j = 0 ; j <len; ++ j) 15 { 16 new_array [j] = array [j]; 17 } 18 // new_arrayがarrayにコピーされ、arrayのサイズが2倍になりましたlen 19 array = new_array; 20 len = 2 * len; 21 } 22 // 要素を添え字iの位置に配置し、添え字iと 23の 配列[i ] = 要素; 24 ++ i; 25 }
私の意見
n + 1のケースがあり、配列スペースが十分である場合、時間の複雑さはO(1)です。配列がいっぱいになると、時間の複雑度はO(n)になります。
平均時間の複雑さ:
このとき、O(1)です。
時分割の複雑さ:O(1)。