コードの効率を高める方法 ~時間計算量と空間計算量~【C言語】

私たちが問題に直面したとき、それを解決する方法はたくさんあります。現在のコンピュータ技術は非常に高度なレベルに達しているため、問題に対処するためにさまざまな方法を使用しても、時間の違いは明らかではありませんし、小さな問題が発生した場合にはメモリの違いは通常感じられないため、問題が絡むことはありません。最適化プロセスをプログラムします。

しかし、将来の人生では、プログラムの内容は非常に豊かになり、時間と空間の効率が反映されます。今日はプログラムの時間と空間について学びましょう。

目次

アルゴリズムの効率

 アルゴリズムの品質を測定する方法

アルゴリズムの複雑さ 

 時間の複雑さ

時間計算量の概念

ビッグ O の漸近表記

一般的な時間計算量の計算例

空間の複雑さ

共通空間の複雑さの計算例

一般的な複雑さの比較


アルゴリズムの効率

 アルゴリズムの品質を測定する方法

アルゴリズムの品質を測定するにはどうすればよいでしょうか? たとえば、次のフィボナッチ数列の場合:

long long Fib(int N)
{
 if(N < 3)
 return 1;

 return Fib(N-1) + Fib(N-2);
}

フィボナッチ数列の再帰的実装は非常に簡潔ですが、簡潔であることが良いのでしょうか? では、その良し悪しをどのように判断するのでしょうか?

コードは非常に簡潔ですが、N の値を 50 より大きく設定すると、結果の計算が非常に難しくなり、コンピューターは大量の計算を実行することになります。

アルゴリズムの複雑さ 

アルゴリズムが実行可能プログラムにコンパイルされた後、実行時に時間リソースと空間 (メモリ) リソースを消費する必要があります。したがって、アルゴリズムの品質を測定するには、一般に時間と空間の 2 つの次元、つまり時間計算量と空間計算量から測定されます。時間計算量は主にアルゴリズムの実行速度を測定し、空間計算量は主にアルゴリズムの実行に必要な追加スペースを測定しますコンピューター開発の初期には、コンピューターの記憶容量はほとんどありませんでした。そのため、私は空間の複雑さを非常に懸念しています。しかし、コンピュータ産業の急速な発展に伴い、コンピュータの記憶容量は非常に高いレベルに達しました。したがって、アルゴリズムの空間の複雑さに特別な注意を払う必要はなくなりました。

 時間の複雑さ

時間計算量の概念

時間計算量の定義: コンピューター サイエンスでは、アルゴリズムの時間計算量は、そのアルゴリズムの実行時間を定量的に記述する関数です。理論的に言えば、アルゴリズムの実行にかかる時間は計算できず、プログラムをマシンに入れて実行して初めて知ることができます。しかし、コンピューター上で各アルゴリズムをテストする必要があるでしょうか? これらをすべてコンピュータ上でテストすることも可能ですが、非常に面倒なので、時間計算量を解析する手法があります。アルゴリズムにかかる時間はステートメントの実行回数に比例し、アルゴリズム内の基本操作の実行回数がアルゴリズムの時間計算量となります。

つまり、特定の基本文と問題サイズ N の間の数式を見つけることは、アルゴリズムの時間計算量を計算することになります。

プログラムの時間計算量を理解するためにさらに一歩進めます。

// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
 for (int j = 0; j < N ; ++ j)
 {
 ++count;
 }
}

for (int k = 0; k < 2 * N ; ++ k)
{
 ++count;
}
int M = 10;
while (M--)
{
 ++count;
}
printf("%d\n", count);
}

++count が合計で何回実行されたかを計算する必要がありますか? for ループの入れ子では、++count が N² 回使用され、3 番目の for ループで 2N 回実行され、最後に while ループで M=10 になるため、++count は 10 回ループするため、++count の合計は実行されます。 N²+2N+10回

N の値を大きくすると、式の最上位項目のみが式に大きな影響を与えます。 

実際には、時間計算量を計算するとき、必ずしも正確な実行数を計算する必要はなく、おおよその実行数のみを計算する必要があるため、ここではビッグ O の漸近表記を使用します。 

ビッグ O の漸近表記

Big O 記法 (Big O 記法): 関数の漸近的な動作を記述するために使用される数学的記法です。

ビッグ O オーダー メソッドを導出します。

1. 実行時のすべての加法定数を定数 1 に置き換えます。

2. 修正された実行時間関数では、最上位の項のみが保持されます。

3. 最上位項目が存在し、1 でない場合は、この項目に乗じた定数を取り除きます。結果はビッグオーオーダー。

big O の漸近表現を使用した後、Func1 の時間計算量は次のようになります。

N = 10 F(N) = 100 N = 100 F(N) = 10000 N = 1000 F(N) = 1000000

以上より、big O のプログレッシブ表現は結果に影響の少ない項目を削除し、実行回数を簡潔明瞭に表現していることがわかります。

さらに、一部のアルゴリズムには最良、平均、最悪の場合の時間計算量があります。

最悪の場合: 任意の入力サイズに対する最大実行数 (上限)

平均的なケース: 任意の入力サイズでの予想実行数 最良のケース: 任意の入力サイズでの最小実行数 (下限) 例: 長さ N の配列でデータ x を検索

最良のケース: 1 件の発見 最悪のケース: N 件の発見 平均的なケース: N/2 件の発見 

実際には、一般的な状況はアルゴリズムの最悪の動作に関するものであるため、配列内のデータを検索する時間計算量は O(N) です。

一般的な時間計算量の計算例

// 计算Func3的时间复杂度?
void Func3(int N, int M)
{
 int count = 0;
 for (int k = 0; k < M; ++ k)
 {
 ++count;
 }
 for (int k = 0; k < N ; ++ k)
 {
 ++count;
 }
 printf("%d\n", count);
}

このプログラムは最初の for ループで M 回実行され、2 番目の for ループで N 回実行されるため、O(M+N) を表すには大きな O を使用します。

// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
 assert(a);
 for (size_t end = n; end > 0; --end)
 {
 int exchange = 0;
 for (size_t i = 1; i < end; ++i)
 {
 if (a[i-1] > a[i])
 {
 Swap(&a[i-1], &a[i]);
 exchange = 1;
 }
 }
 if (exchange == 0)
 break;
 }
}

BubbleSort の時間計算量を求めます。この関数はバブル ソート方法であり、配列のセットをソートする場合、最良のケースは N であり、チェックする必要があるのは 1 回だけです。最悪のケースは、順序を常に変更する必要があり、(N*(N+1))/2 回実行する必要があることです。大きな O 次法 + 時間計算量を導出することにより、一般に最悪のケースは次のようになります。が見られ、時間計算量は O(N^2 ) です。

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
 assert(a);
 int begin = 0;
 int end = n-1;
 // [begin, end]:begin和end是左闭右闭区间,因此有=号
 while (begin <= end)
 {
 int mid = begin + ((end-begin)>>1);
 if (a[mid] < x)
 begin = mid+1;
 else if (a[mid] > x)
 end = mid-1;
 else
 return mid;
 }
 return -1;
}

この関数は二分探索法であり、この関数の時間計算量を見つける必要があります。

 上の図によると、二分探索の時間計算量は O(logN) であると結論付けることができます。

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
 if(N < 3)
 return 1;

 return Fib(N-1) + Fib(N-2);
}

時間計算量を学習したので、再帰を使用してフィボナッチ数列を見つける場合の時間計算量を計算できます。また、フィボナッチ数列を見つけるために再帰を使用することが単純なコードであるにもかかわらず、それが良い方法ではない理由がわかります。

上の図から、この再帰の時間計算量は O(2^N) であることがわかります。したがって、このアルゴリズムは非常に悪いです。

空間の複雑さ

スペース複雑度も数式であり、動作中にアルゴリズムが一時的に占有するストレージ スペースの量の尺度です

空間複雑度はプログラムが占めるバイト数ではありません。これはあまり意味がないため、空間複雑度は変数の数によって計算されます。空間計算量の計算ルールは基本的に実際の計算量と同様であり、ビッグ O の漸近表記も使用されます。

注:実行時に関数に必要なスタック スペース (ストレージ パラメーター、ローカル変数、一部のレジスタ情報など) はコンパイル中に決定されるため、スペースの複雑さは主に、実行時に関数によって明示的に要求される追加スペースによって決まります。

共通空間の複雑さの計算例

// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
 assert(a);
 for (size_t end = n; end > 0; --end)
 {
 int exchange = 0;
 for (size_t i = 1; i < end; ++i)
 {
 if (a[i-1] > a[i])
 {
 Swap(&a[i-1], &a[i]);
 exchange = 1;
 }
 }
 if (exchange == 0)
 break;
 }
}

この関数は一定量の追加スペースを使用し、size_t int、int Exchange、size_t i の 3 つの変数スペースを開くため、スペースの複雑さは O(1) になります。

// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
 if(N < 3)
 return 1;

 return Fib(N-1) + Fib(N-2);
}

再帰を使用してフィボナッチ数を計算します。関数が呼び出されるたびに関数スタック フレームを開く必要があり、この関数のスタック フレーム空間は呼び出しが完了した後に破棄されるため、空間複雑度を計算するときは次のようになります。累積計算を順番に実行し、関数が使用されていないときに他のコンテンツを再帰的に呼び出すことはありません。フィボナッチ量はツリーのように再帰的ですが、空間計算量は時間計算量とは異なります。空間計算量は同時に呼び出す数に依存するため、この関数の空間計算量は N となり、O(N と表されます) )。

一般的な複雑さの比較

 

このように、複雑さはプログラムに大きな影響を与えるので、プログラムを設計する際には、どのように設計すれば最適解になるのか、一度立ち止まって考えてみてはいかがでしょうか。! 

おすすめ

転載: blog.csdn.net/m0_74755811/article/details/131965924