私たちは皆知っている、すなわち、収納スペース節約より多くのコードを作成する方法を、より速くあなたのコードの実行を行うためにどのようなデータ構造とアルゴリズムの問題そのものを解決するため、「高速」と「省」、 。したがって、このアルゴリズムの効率は非常に重要な考慮事項指標です。
コードは、あなたがそれを書くことを、アルゴリズムの効率を測定するには?:ここでは、我々が今日の話を何を使用しなければならない時間、空間の複雑さ分析。実際には、限り、上記のデータ構造とアルゴリズムとして、それは時間と空間の複雑性分析なしに行うことはありません。そして、私は個人的にと思い、あなたがそれをしたら、データ構造とアルゴリズムの内容は基本的に半分を習得し、分析の複雑さを学ぶためのアルゴリズム全体の本質です。
分析の複雑さは、あまりにも重要であるので、私はコンテンツの二つの用語を使用するつもりです。このコンテンツ学校を卒業した後うまくいけば、いずれのシナリオの下で、任意のコード分析の複雑さの顔は、あなたが「Paodingjieniu」様やすさを行うことができます。
なぜ我々は、分析の複雑さが必要なのですか?
あなたは、アルゴリズムの実行時間とメモリサイズを取得することができ、私はモニター、統計によって、再びコードを実行し、いくつかの疑問を持っていることがあります。なぜ、時間と空間の複雑な分析を行いますか?私は実際にそれを実行することができますよりも、分析するこの方法は、再びデータをより正確に取得しますか?
まず第一に、私はあなたがアルゴリズムの効率のこの評価が正しい実行することを確かに言うことができます。多くのデータ構造とアルゴリズムの本は、このメソッドをコールバック名、持っている統計の後に。しかし、この統計的手法の種類は非常に大きな制限があります。
1.テスト結果は非常に依存してテスト環境です
テスト結果は、異なるハードウェアテスト環境が大きな影響を与えるだろう。例えば、我々は言うまでもない、実行するために、インテルCore I9プロセッサーとインテルCore i3のプロセッサを搭載し、それぞれ、コードの同じ部分を取り、I9プロセッサ速度が非常に速くi3のプロセッサよりもあります。あり、例えば、我々は別のマシンに変更するときのように高速Bコードよりも実行が、正反対の結果が存在し得ることをマシンコードの速度に元々ありました。
2.試験結果を大幅にデータの大きさによって影響を受けます
その後、我々はアルゴリズムのソートについて話すよ、私たちは、例えば、それを獲得します。注文の同程度のソートアルゴリズムは、実行時間が非常に異なるものになります注文、ソートする、同じデータではありません。データがすでにソートアルゴリズムは何もする必要がないことが、注文された場合、極端な例では、実行時間が非常に短くなります。テストデータが小さすぎるまた、テスト結果は、アルゴリズムの真の性能を反映していないことがあります。例えば、小規模なデータをソートするために、挿入ソートは、実際にはクイックソートより速いかもしれません!
私はそれがおおよそ推定できるアルゴリズム実装方法の効率をテストするために、特定の試験データを行う必要があります。これは、我々が今日の時間と空間の複雑さの解析について話すものです。
ランダウの記号の複雑さ
アルゴリズムの効率は、大まかに言えば、アルゴリズムコードの実行時間です。しかし、それはどのように、コードの実行時間の一部を取得するには、「裸眼」で、コードを実行せずにいるのですか?
nと、蓄積された... 1,2,3を求めて、非常に簡単なコードセグメントがあります。今、私は1つが、このコードの実行時間を推定するためにあなたを取りますよ。
INT CAL(INT N){ int型の和= 0 。 int型私は1を= 。 用(; I <= N; ++ I){ 合計 =合計+ I。 } 戻り値の和。 }
CPUの観点から、類似したコードが実行されるオペレーションの各行は:-動作-書き込みデータデータを読み出します。数がCPUによって実行されるコードの各行に対応しているが、実行時間は異なるが、ここで私たちは概算であり、コードの実行時間の各ラインはunit_timeに対して同じであると仮定することができます。上記のこの仮定に基づき、このコードの総実行時間はどのくらいですか?
2行目と3行目は、それぞれ、unit_time実行時間、行4、5ランをn回必要であり、それは2Nする必要がある* unit_time実行時間は、総実行時間コードが(2N + 2)であり、* unit_time。見ることができる、すべてのコードの実行時間T(n)は、コードの各行回数に比例します。
アイデアのこの分析によると、私たちはコードを見ていきます。
INT CAL(INT N){ int型の和= 0 。 int型私は1を= 。 int型 J = 1 ; 用(; I <= N; ++ I){ J = 1 。 用(; J <= N; ++ J){ 合計 =合計+ I * J。 } } }
我々はまだ、それぞれの文の実行時間がunit_timeであることを前提としています。このコードT(N)の合計実行時間はどのくらいですか?
2,3,4行、各行が1 unit_time実行時間を必要とし、第5及び第6行目のループが実行されたn回、2N * unit_time必要な実行時間、ループラインの実行7-8 N2回、それはunit_time実行時間*必要2N2です。コード総実行時間のため、全体のセクションT(N)=(2N2 + 2N + 3)* unit_time。
私たちは、正確な値unit_timeを知らないが、しかし、コードの実行時間のこれら2つの導出により、我々は、非常に重要な法律、得ることができるコードの各行がnであるT(n)と回数すべてのコードの実行時間を比例。
私たちは式には、この法律をまとめることができます。ことに注意してくださいビッグOがデビューします!
私は、この特定の式を説明しましょう。ここで、T(n)がすでにそれがタイムコードの実行を表し、言及した;データサイズの大きさをN-表し、F(n)が実行されるコードの各行の数の合計を表します。これは式であるため、そうF(N)と表されます。O、実行時間コードT(n)とF(N)からの式は、式に比例します。
したがって、T(N)の第一の例= O(2N + 2)、 T(N)の第二の例= O(2N2 + 2N + 3)。これは大きなO時間の複雑さの表記です。大きなO時間複雑性は、実際の実時間を示す特定のコードを実行するのではなく、データの増加傾向を持つコードの実行時間のサイズは、したがって、しない、時間(漸近的時間計算量)の漸進的複雑さ、短時間の複雑として知られています。
ときに大きなN、あなたは10000,100000と考えることができます。下位の3つの部分式は、定数の係数は、無視することができ、成長の傾向に関するものではありません。我々は唯一の大きさの最大順序を記録しておく必要があり、大きなO記法時間の複雑さがちょうど2つのコードが、それはのように書くことができることを言えば、それに:T(N)= O(N)、T(N-) O =(N2) 。
時間の複雑さの解析
原点Oの前に導入され、大きな時間の複雑さの表現。今、私たちは、コードの一部の時間複雑さを分析する方法を見て?私はあなたと共有するための3つのより多くの実用的な方法を持っています。
唯一のサイクルのほとんどの数に関係1.コードの一部を実行します
私は、言ったように、A表現のビッグOの複雑さが唯一の傾向を示しています。私たちはしばしば式の定数を無視し、低次の係数は、それだけの大きさの最大の順序を記録する必要があります。したがって、我々は、アルゴリズムにのみその上にコードの一部、そのサイクルの最大数の実装に焦点を当てたコードの複雑さの部分を、時間を分析します。、コードの複雑さを分析するための全体の時間であるため、このコアコードの実行時間のN。
あなたの理解を容易にするために、私が説明するために、前の例を取ります。
INT CAL(INT N){ int型の和= 0 。 int型私は1を= 。 用(; I <= N; ++ I){ 合計 =合計+ I。 } 戻り値の和。 }
前記第二及び第三のラインは、関係なく、nの大きさの、全く効果のために複雑定数ステージの実行時間です。ループは、最大数は、コードの4、5行であるので、コードのこの部分は、分析の焦点であることを実行します。総時間複雑度はO(N)であるので、我々はまた、n回実行されるコードの以前の2つのラインについて話しました。
2.追加ルール:全体的な複雑さの複雑さは、コードの大部分の順序と同じです
私はここでコードの一部を持っています。あなたは、まずそれを分析しようとした後、同じ考えかどうかの私の分析を見下ろすことができます。
INT CAL(INT N){ int型 sum_1 = 0 。 int型のp = 1 ; 用(; P <100; ++ P){ sum_1 = sum_1 + P。 } INT sum_2 = 0 。 INT Q = 1 。 用(; Q <N; ++ Q){ sum_2 = sum_2 + Q。 } INT sum_3 = 0 。 int型私は1を= 。 int型 J = 1 ; 用(; I <= N; ++ I){ J = 1 。 用(; J <= N; ++ J){ sum_3 = sum_3 + I * J。 } } 戻り sum_1 + sum_2 + sum_3。 }
コードはsum_1、sum_2、sum_3を求め、つまり、3つの部分に分割されます。私たちは、個別に各パートの時間複雑さを分析して、それらを一緒に入れ、その後、全体のコードの最大複雑さと大きさの注文を取ることができます。
最初の段落の時間の複雑さはどのくらいですか?このコードループは、実行時間は定数、n個の大きさとは何の関係があるので、100回実行されます。
ここで私は長い間、既知の数として、n個とは何の関係もが、まだ実行時間の一定のレベルではないとして、このコードは、10万回の10,000サイクルであったとしても、それを強調しなければなりません。無限の時間をn個の場合、それは無視することができます。コードの実行時間が大幅に影響を受けたが、時間の概念の裏の複雑されるが、それはトレンドの変化がどのくらいの時間実行の定数、私たちができるように関係なく、アルゴリズムとデータ量の増加の効率ではない表現しました無視。それ自体は、成長への影響はありませんので。
コードの3段落の2番目の段落のコードのその時の複雑さはどのくらいですか?O(n)とO(N2)である、あなたは簡単にそれを分析することができるはずの答えは、私は長いったらしいません。
コードの3つのセクション全体の時間の複雑さは、我々は大きさの最大を取ります。したがって、コード全体の時間複雑度は、O(N 2)です。言い換えれば、総時間の複雑さは、コードの大部分のための時間計算量と同等です。我々この法律抽象化式は、もしT1(N)= O(F (N))、T2(N)= O(G(n))を、 次いでT(N)= T1(N )+ T2( N)= MAX(O(F (N))、O(G(N)))= O(MAX(F(N)、G(N)))。
3.乗算ルール:ネストされたタグの複雑さは、ネストされた内側及び外側コードの複雑さの積に等しいです。
私はここにあり、また、ルールの複雑さの解析について話しました乗算ルールが。類推ビットは、あなたは右、式が何であるかを「推測」のことができるようにする必要がありますか?もしT1(N)= O(F (N))、T2(N)= O(G(n))を、 次いでT(N)= T1(N )* T2(N)= O(F(N)) * O(G(N)) = O(F(N)* G(N)) であり、T1(N)= O(Nと仮定 )、T2(N)= O(N 2)、 T1を( N)* T2(N)= O(N3)。特定のコードの実装は、我々は原則としてネストされたループを掛けることができ、私はあなたに説明するための例を与えます。
INT CAL(INT N){ int型 RET = 0 。 int型私は1を= 。 用(; I <N; ++ I){ RET = RET + F(I)。 } } int型 F(INT N){ int型の和= 0 。 int型私は1を= 。 用(; I <N; ++ I){ 合計 =合計+ I。 } 戻り値の和。 }
我々アローンCAL()関数。仮定F()は、複雑さの最初の4〜6行は、T1(N)= O(N)である時、普通の操作です。しかし、関数f()自体は簡単な操作ではなく、その時間複雑さはT2(N)= O(N)であり、従って、時間複雑さの全体CAL()関数は、T(N)= T1(n)は* T2(N)= O(N * N)= O(N 2)。
私は分析能力に複雑の3種類を言います。ただし、メモリを使用することを気にしないでください。実際には、分析の複雑さは、この事への鍵です。「堪能。」あなただけの、より多くの分析は、我々が達成することができるようになりますケースを見て、「ノーストロークトリックに勝ちます。」
時間複雑分析の一般的な例
コードは異なりますが、複雑さのレベルの一般的な尺度はあまりありませんが。私は、ビットは、これらの複雑なメトリック学年のカバーは、ほぼすべてのコードの複雑さの指標は、将来的にあなたに連絡することができますどのようにまとめました。
新たに記載されているため、複雑度の測度、我々は、大きくすることができる二つのカテゴリーに分け、順序の大きさの多項式および非多項式オーダー。前記2つだけの非多項式オーダー:O(2N)とO(N!) 。
我々は入れNP(非決定性多項式非決定性多項式、)と呼ばれる非多項式アルゴリズムの問題のための時間複雑な問題を。
データサイズNが上昇すると、実行時間の問題を解決するために、劇的に増加する非多項式アルゴリズムのための実行時間は無限に成長します。したがって、このアルゴリズムは複雑さは実際には非常に非効率的なアルゴリズムである非多項式時間です。したがって、NP時間の複雑さに関しては私は話して起動しません。私たちが主に見て、いくつかの一般的な多項式時間の複雑さ。
1.(1)
まず、あなたが概念を定義する必要があり、O(1)の複雑さの時定数レベルの唯一の表現であり、それは、コードの一行だけが実行されていることを意味するものではありません。このコード例では、3行があっても、その時間複雑度はO(1)の代わりに、O(3)です。
int型私= 8 ; int型 J = 6 ; int型の合計= I + J;
我稍微总结一下,只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。
2. O(logn)、O(nlogn)
对数阶时间复杂度非常常见,同时也是最难分析的一种时间复杂度。我通过一个例子来说明一下。
i=1; while (i <= n) { i = i * 2; }
根据我们前面讲的复杂度分析方法,第三行代码是循环执行次数最多的。所以,我们只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。
从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:
所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2x=n 求解 x 这个问题我们想高中应该就学过了,我就不多说了。x=log2n,所以,这段代码的时间复杂度就是 O(log2n)。
现在,我把代码稍微改下,你再看看,这段代码的时间复杂度是多少?
i=1; while (i <= n) { i = i * 3; }
根据我刚刚讲的思路,很简单就能看出来,这段代码的时间复杂度为 O(log3n)。
实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。为什么呢?
我们知道,对数之间是可以互相转换的,log3n 就等于 log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一个常量。基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等于 O(log3n)。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。
如果你理解了我前面讲的 O(logn),那 O(nlogn) 就很容易理解了。还记得我们刚讲的乘法法则吗?如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。而且,O(nlogn) 也是一种非常常见的算法时间复杂度。比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。
3. O(m+n)、O(m*n)
我们再来讲一种跟前面都不一样的时间复杂度,代码的复杂度由两个数据的规模来决定。老规矩,先看代码!
int cal(int m, int n) { int sum_1 = 0; int i = 1; for (; i < m; ++i) { sum_1 = sum_1 + i; } int sum_2 = 0; int j = 1; for (; j < n; ++j) { sum_2 = sum_2 + j; } return sum_1 + sum_2; }
从代码中可以看出,m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。
针对这种情况,原来的加法法则就不正确了,我们需要将加法规则改为:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法则继续有效:T1(m)*T2(n) = O(f(m) * f(n))。
空间复杂度分析
前面,咱们花了很长时间讲大 O 表示法和时间复杂度分析,理解了前面讲的内容,空间复杂度分析方法学起来就非常简单了。
前面我讲过,时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
我还是拿具体的例子来给你说明。(这段代码有点“傻”,一般没人会这么写,我这么写只是为了方便给你解释。)
void print(int n) { int i = 0; int[] a = new int[n]; for (i; i <n; ++i) { a[i] = i * i; } for (i = n-1; i >= 0; --i) { print out a[i] } }
跟时间复杂度分析一样,我们可以看到,第 2 行代码中,我们申请了一个空间存储变量 i,但是它是常量阶的,跟数据规模 n 没有关系,所以我们可以忽略。第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。
我们常见的空间复杂度就是 O(1)、O(n)、O(n2),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。所以,对于空间复杂度,掌握刚我说的这些内容已经足够了。
内容小结
基础复杂度分析的知识到此就讲完了,我们来总结一下。
复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系,可以粗略地表示,越高阶复杂度的算法,执行效率越低。常见的复杂度并不多,从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)。等你学完整个专栏之后,你就会发现几乎所有的数据结构和算法的复杂度都跑不出这几个。
复杂度分析并不难,关键在于多练。 之后讲后面的内容时,我还会带你详细地分析每一种数据结构和算法的时间、空间复杂度。只要跟着我的思路学习、练习,你很快就能和我一样,每次看到代码的时候,简单的一眼就能看出其复杂度,难的稍微分析一下就能得出答案。
注: 本文出自极客时间(数据结构与算法之美),请大家多多支持王争老师。如有侵权,请及时告知。