【データ構造】アルゴリズムの時間と空間の複雑さ

目次

1. アルゴリズムとは何ですか?

1.1 アルゴリズムの複雑さ

2. アルゴリズムの時間計算量

2.1 時間計算量の概念

Func1 の ++count ステートメントが合計で何回実行されたかを計算します。

2.2 ビッグ O の漸近表現

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

例1: 2N+10回実行

例 2: M+N 回実行する

例 3: 100000000 回実行

例 4: strchr の時間計算量を計算する

例 5: BubbleSort の時間計算量を計算する

例 6: BinarySearch の時間計算量を計算する

例 7: 階乗再帰計算の時間計算量

例 8: フィボナッチ再帰 Fib の時間計算量を計算する

3. アルゴリズムの空間複雑度

例 1: BubbleSort の空間複雑度を計算する

例 2: フィボナッチの空間複雑度を計算する

例 3: 階乗再帰 Fac の空間計算量を計算する

4. 一般的な複雑さの比較


1. アルゴリズムとは何ですか?

アルゴリズム:

アルゴリズム : 1 つまたは一連の値を入力として受け取り、1 つまたは一連の値を出力として生成する、明確に定義された計算プロセスです。簡単に言えば、アルゴリズムは入力データを出力結果に変換するために使用される一連の計算ステップです。
ソート/ 二分検索の一般的なアプリケーション
アルゴリズムの特徴:

1. 無限。アルゴリズムには、無限ではなく、有限の操作ステップが含まれている必要があります。実際、「限界」は「妥当な制限内」を意味することがよくあります。完成までに 1000 年かかるアルゴリズムをコンピュータに実行させた場合、それは有限ではありますが、合理的な限界を超えており、人々はそれを有効なアルゴリズムとはみなしません。

2. 確実性。アルゴリズムのすべてのステップは明確である必要があり、漠然とした曖昧なものであってはなりません。アルゴリズムの各ステップは異なる意味として解釈されるべきではなく、非常に明確である必要があります。つまり、アルゴリズムの意味は一意である必要があり、「曖昧さ」を生み出してはなりません。

3. 入力が 0 個以上あるいわゆる入力とは、アルゴリズムの実行時に外部から取得される必要な情報を指します。

4. 1 つ以上の出力があります。アルゴリズムの目的は問題を解決することであり、出力のないアルゴリズムは意味がありません

5. 有効性。アルゴリズムの各ステップは効率的に実行できる必要があります。そして確かな結果が得られます。

1.1 アルゴリズムの複雑さ

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

2. アルゴリズムの時間計算量

2.1時間計算量の概念

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

Func1 の ++count ステートメントが合計で何回実行されたかを計算します。

試してみましょう:
// 请计算一下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);
}

時間計算量の関数式 (つまり、Func1 によって実行される基本演算の数):

                                               F(N)=N^2+2N+10

しかし、この表現は正確すぎ、詳細すぎ、そして煩雑すぎます。時間計算量は、この数学関数の実行回数を正確に計算することではなく、それにレベル(大きさ)を割り当てることです

例: 馬雲氏と馬化騰氏のように、彼らの口座の具体的な金額を気にする必要はありません。彼らが金持ちであることだけを知っておく必要があります。
正確な値 F(N)=N^2+2N+10 推定値 O(N^2)
N = 10
F(N) = 130 100
N = 100
F(N) = 10210
10000
N = 1000
F(N) = 1002010
1000000
結論 1:
N が大きいほど、後者の項目の結果への影響は小さくなります。つまり、 最も高い次数 (N^2) の 項目が最も影響力のある項目となり、最も高い次数の項目が保持されます。
結論 2:
以上より、big O のプログレッシブ表現は結果に影響の少ない項目を削除し、実行回数を簡潔明瞭に表現していることがわかります。
実際には、時間計算量を計算するとき、必ずしも正確な実行数を計算する必要はなく、おおよその 実行数のみを計算する必要があるため、 ここではビッグ O の漸近表記を使用します。
大きな○で表されている限り、それは推定値であることを意味します。

2.2ビッグOの漸近表現

Big O notation ( Big O notation ): 関数の漸近的な動作を記述するために使用される数学的表記法です。
Big O 注文方法の導出:
1. 実行時のすべての加法定数を 定数 1に置き換えます
2. 修正された実行時間関数では、 最上位の項のみが保持されます
3. 最上位の項目が存在し、 1でない場合は この項目に乗じた定数を 削除します 結果はビッグオーオーダー
一部のアルゴリズムには、最高、平均、最悪の場合の時間計算量があります。
最悪の場合: 任意の入力サイズに対する最大実行数 ( 上限 )
平均的なケース: 任意の入力サイズでの予想される実行数
最良のケース: 任意の入力サイズに対する最小実行数 ( 下限 )
例:長さ Nの配列で データ xを検索するには、次のようにします。
最良のケース: 1 件 が見つかりました
最悪の場合: N 回見つかった
平均的なケース: N/2 が見つかりました
実際には、一般的な状況はアルゴリズムの最悪の動作 に関するものである ため、配列内のデータを検索する時間計算量は O(N)です。

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

例1: 2N+10回実行

// 计算Func2的时间复杂度?
void Func2(int N)
{
 int count = 0;
 for (int k = 0; k < 2 * N ; ++ k)
 {
 ++count;
 }
 int M = 10;
 while (M--)
 {
 ++count;
 }
 printf("%d\n", count);
}
基本演算は 2N+10 回実行され、ビッグ O 次法を導出することで時間計算量は O(N) になります。

例 2: M+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);
}
Example 2では 、基本演算が M+N回実行され、2 つの未知数 MN があり 、時間計算量はO(N+M)です。
N が無限に大きいとは言えず、M は重要ではありません。関係が得られない限り、次のようになります。
N は M よりもはるかに大きく、時間計算量は O(N) です
M が N よりもはるかに大きい場合、時間計算量は O(M) になります。
M が N に等しい場合、または 2 つの差が大きくない場合、時間計算量は O(M+N) になります。

例 3: 100000000 回実行

void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100000000; ++k)
	{
		++count;
	}
	printf("%d\n", count + N);
}

int main()
{
	Func4(100000);
	Func4(1);

	return 0;
}

実行: 実際、CPU の速度は非常に速く、実行回数の違いは無視できるため、時間計算量は依然として O(1) です。

O(1) は 1 時間を表すのではなく、一定の時間を表します。k<10 億の場合でも、O(1) です。

通常書き込むことができる最大の定数は約 40 億 (整数で表現できる範囲) であり、CPU はこれに耐えることができます。

例 3 では、基本演算が 100000000 回実行され、ビッグOオーダー法を導出することで時間計算量はO(1)になります。

例 4: strchr の時間計算量を計算する

// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );

これは strchr のモック実装についてです

#include<stdio.h>
#include<assert.h>
char* my_strchr(const char* str, const char ch)
{
	assert(str);
	const char* dest = str;
	while (dest != '\0' && *dest != ch)
	{
		dest++;
	}
	if (*dest == ch)
		return (char*)dest;
	return NULL;
}
int main()
{
	char* ret = my_strchr("hello", 'l');
	if (ret == NULL)
		printf("不存在");
	else
		printf("%s\n", ret);
	return 0;
}

私の理解では、strchrとstrstrの違いは、strstrは文字列を入力してメイン文字列を検索することですが、strchrは文字を入力してメイン文字列を検索することです。このリンクには、strstr に関する知識ポイントがあります: http://t.csdn.cn/NEaip

検索が失敗した場合は、NULL を返します。検索が成功すると、最初の文字のアドレスが返され、その後 '\0' の終わりまで出力されます。

それで:

この配列の長さを指定してそれを見つける場合の時間計算量は O(1) です。長さが明確でない場合、長さは N となり、N 回の再帰が必要になり、時間計算量は O(N) になります。
4では 、基本操作はよくて 1 回 、最悪で N 回実行されますが、時間計算量は一般に最悪であり、時間計算量は O(N)です。

例 5: BubbleSort の時間計算量を計算する

// 计算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;
 }
}
図:

 次に、比較の数が等差数列を構成します。算術数列の合計公式を使用して、最終的な実行数を取得すると、F(N)=(N-1)*N/2 になります。

ループに関するこの質問は、ループのネストが 2 層ある場合、比較の数がわかっている場合 (外側のループ n<10、内側のループ) であるため、その時間計算量が O(N^2) であると直接判断されることを意味するものではありません。 n< 1000000) つまり O(1) であり、バブル ソートの最適化されたバージョンが存在します。順序の場合、その時間計算量は O(N) で、外側のループのみが使用されます。
5 基本演算は N 回実行するのが最適で、最悪の場合は (N*(N+1)/2 回実行します。ビッグ O 次法 + 時間計算量を導出することにより、一般に最悪の計算量が見られ、時間計算量はは O(N^2 )

例 6: BinarySearch の時間計算量を計算する

// 计算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;

}

図:

x 回が見つかったと仮定すると、x 2 は除きます。

2^x =N --> x = log2N   

したがって、最後の検索から元の配列の長さに 2 を掛けることができます。

例 6 基本演算は良くて 1 回、悪くても O(log2N) 回実行され、時間計算量は O(log2N) ps: logN は、アルゴリズム解析において底が 2、対数が N であることを意味します。場所によっては lgN と表記されることもあります。

例 7: 階乗再帰計算の時間計算量

次の 2 つのコード部分の時間計算量を計算します。
//实例7:
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
	if (0 == N)
		return 1;

	return Fac(N - 1) * N;
}

long long Fac(size_t N)
{
	if (0 == N)
		return 1;

	for (size_t i = 0; i < N; i++)
	{

	}

	return Fac(N - 1) * N;
}

図:

左側の各関数呼び出しにおける for ループ ステートメントの実行回数 (他のループ ステートメントがある場合)。左側の 1 は、1 回ではなく一定の回数であることを意味します (つまり、ループ ステートメントが存在する場合)。関数内にループ文はなく、いくつかの if 文があり、時間計算量も O(1)) です。

右側は単に関数呼び出しが N+1 個あり、各関数呼び出しのループ ステートメントが N+1 回実行されることを意味するため、再帰的に呼び出される各関数のループ ステートメントを合計する必要があります。

追加のポイント:

時間は乗算ではなく加算されます。たとえば、上図の戻り値 Fac(N-1)*N: これは、最後の結果に N を乗算することを意味しますが、実行回数も 1 回です。 ※ここはコンピュータ用の単なるコマンドです。時間計算量は、プログラムのプロセス中にこの命令が実行される回数として計算されます。

例 8: フィボナッチ再帰 Fib の時間計算量を計算する

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

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

}

図:

 実行回数の合計は等比数列に準拠します: 誤って配置された減算方法を使用します。

この質問は次のように理解できます。

三角形に関しては、N が大きくなると白い領域が黒い領域よりもはるかに大きくなり、時間計算量は特定の数学関数の大きさを計算し、それにレベルを割り当てるために使用されるため、次の計算とみなすことができます。完全なアイテムの状態、実行時間の合計は等比数列を構成し、計算の時間計算量は、ビッグ O 漸近表記を使用すると O(2^N) になります。

3. アルゴリズムの空間複雑度

スペース複雑度も数式であり、動作中にアルゴリズムが 一時的に占有するストレージ スペースの量の尺度 です。
空間複雑度はプログラムが占めるバイト数 ではありません 。これはあまり意味がないため、空間複雑度は 変数の数 によって計算されます。
空間計算量の計算規則は基本的に実際の計算量と同様であり、 ビッグ O の漸近表記 も使用されます。
注:実行時に 関数に必要なスタック スペース ( ストレージ パラメーター、ローカル変数、一部のレジスタ情報など )は コンパイル時に決定される ため スペースの複雑さは 主に、実行時に関数によって明示的に要求される 追加スペース によって決まります。

例 1: BubbleSort の空間複雑度を計算する

// 计算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;
	}
}

ここでは end、exchange、i 変数が 1 つだけ作成されるため、変数の種類に関係なく変数の数のみが計算され、空間内の特定のバイト数としてカウントされず、すべてループで作成されます。 、したがって、空間複雑さは O ( 1)です。

仮パラメータ int *a および int n については、空間計算量にはカウントされません。

例 2: フィボナッチの空間複雑度を計算する

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
 if(n==0)
 return NULL;
 
 long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
 fibArray[0] = 0;
 fibArray[1] = 1;
 for (int i = 2; i <= n ; ++i)
 {
 fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
 }
 return fibArray;
}

空間計算量は、この関数内にどれだけの余分な空間が開いているかを計算します。定数の場合はO1 です。開口部のサイズが不確実な場合は、一般に O(N) です。

したがって、空間の複雑さは O(N) です。

例 3: 階乗再帰 Fac の空間計算量を計算する

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
 if(N == 0)
 return 1;
 
 return Fac(N-1)*N;
}

図:

N 個のレイヤーが再帰的に呼び出されます。各呼び出しでスタック フレームが作成されます。各スタック フレームは定数空間 O(1) を使用します。
ここで N 個の関数が呼び出され、戻りがないため、合計は O(N) になります

例 4: フィボナッチ再帰 Fib の空間計算量を計算する (2 つの再帰)

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

序文:

スペースの破壊は、スペースが完全に破壊されることを意味するわけではありませんが、スペースを使用する権利がオペレーティング システムに返されます

メモリ空間はオペレーティング システムのプロセスに属しているため、たとえば、領域の一部を malloc すると、この領域を使用する権利を取得し、free を実行すると、その領域を使用する権利がオペレーティング システムに返されます。

時間は永遠になくなる 時間は累積計算であり、累積計算をしなくても空間は再利用できる
簡単に言えば、右側の関数と左側の関数はスタック フレームを共有します。

コードの実行: スタックは下方向に成長し、Func1 と Func2 の呼び出しはスペースを共有することと同じです。これは、Func1 が破棄された後、Func2 が作成されるとき、場所は依然として同じであり、アドレスも同じであるためです。

main 関数の a と Func1 の a は異なるスタック フレームにあるため、同じ名前を持つことができます。

例 4では、 N回再帰的に呼び出し、 N 個のスタック フレームを開き、各スタック フレームは一定のスペースを使用します。空間の複雑さはO(N)

呼び出し時にスタック フレームを作成します。

戻るときは破壊します。(OSに戻る)

4. 一般的な複雑さの比較

一般的なアルゴリズムの一般的な複雑さは次のとおりです。

図:

 この章の最後に不備がある場合は、修正してください。

おすすめ

転載: blog.csdn.net/weixin_65186652/article/details/131036702