目次
アルゴリズムの複雑さ
アルゴリズムが実行可能プログラムに書き込まれた後、実行時に時間リソースと空間 (メモリ) リソースを消費する必要があります。したがって、アルゴリズムの品質を測定するには、通常、時間計算量と空間計算量だけでなく、時間と空間の 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 j = 0; j < N; ++j)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
Func1 の ++count の実行時間を個別に計算できます。Func1 には 3 つのループがあり、最初のループはネストされた for ループです。
最初のループで ++count が実行される回数はN*N回です
2回目のループで++countが実行される回数はN回です
3 番目のループでの ++count の実行回数はM 回です (M=10)。
これにより、時間計算量の関数式が導かれます: F(N)=N^2+2*N+10
この時間計算量の関数式を通じて、N が大きくなると、後の 2 つの項目が結果に与える影響が小さくなることがわかりました。
実際には、時間計算量を計算するとき、必ずしも正確な実行数を計算する必要はなく、おおよその実行数のみを計算する必要があるため、ここではビッグ O の漸近表現を使用します。
ビッグ O の漸近表記
Big O 記法 (Big O 記法): 関数の漸近的な動作。
Big O メソッドの導出:
1.実行時のすべての加法定数を定数 1に置き換えます。2. 修正された実行時間関数では、最上位の項のみが保持されます。3. 最上位の項目が存在し、1 でない場合は、この項目に乗じた定数を削除します。結果は、big-O 次数になります。
例 1: 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);
}
まず、前の例に従って、Func2 の正確な時間計算量を簡単に取得できます: F(N)=2N+10
次に、 big O の漸近表現に従って、この項目に乗算された定数 2 と加算定数 10 を削除し、最終的に空間計算量O(N)を取得します。
例 2: 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);
}
この質問では M と N の大小関係が説明されていないため、時間計算量は O(M+N) となります。
しかし:
M が N よりもはるかに大きい場合、時間計算量はO(M)であると考えることができます。
M が N よりもはるかに大きい場合、時間計算量はO(N)であると考えることができます。
M と N のサイズが類似している場合、時間計算量はO(N) または O(M)であると考えることができます。
特別な注意事項: 一般に、時間計算量の計算では N が未知数に使用されますが、M、K などの場合もあります。
例 3: Func2 の時間計算量を計算しますか?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
この関数は未知の数値を与えるのではなく、定数 100 を直接与えます。その時間計算量が O(100) であると考える人もいるかもしれませんが、そうではありません。big O の漸近表記の最初の記事は次のようになります。1. 実行時のすべての加算定数を定数 1 に置き換えます。 そのため、 func4 関数の時間計算量はO(1)になります。
同時に、時間計算量O(1) は、実行できる操作が 1 つだけであることを意味するのではなく、一定数の操作が実行できることを意味することも知っておく必要があります。
例 4: strchr の時間計算量を計算しますか?
const char* strchr(const char* str, int character);
まず最初に、strchr の一般的な内容を理解する必要があります。
最悪の実行結果によると、strchrの時間計算量はO(N) (N は *str の長さ) です。
例 5: バブルソートの時間計算量を計算しますか?
void Bubb1eSort(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=n の場合: for ループの 2 番目の層が N-1 回実行されます。
end=n-1 の場合: for ループの 2 番目の層が N-2 回実行されます
end=n-2 の場合: for ループの 2 番目の層が N-3 回実行されます
end=n-3 の場合: for ループの 2 番目の層が N-4 回実行されます
——
end=2の場合:forループの2層目を1回実行
合計実行数は 1+2+3+4+..+N=N*(N-1)/2 となります。
したがって、バブルソートの時間計算量はO(N^2)です。
この例は、時間計算量の計算がループのいくつかの層を調べるだけでなく、その考え方にも依存することを間接的に示しています。
例 6: BinarySearch の時間計算量を計算しますか?
int BinarySearch(int* a,int n,int x)
{
assert(a);
int begin = 0;
int end = n;
while (begin < end)
{
int mid = begin + ((end - begin) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid;
else
return mid;
}
return-1;
}
二分探索の時間計算量の計算では、最悪のケースも検討します。
最初に検索する範囲を N とし、検索するたびに範囲を 2 倍にし、N/2/2/2/....(N>=0) とします。
したがって、二分探索の時間計算量はO(log 2 N)です。
例 7: 階乗再帰 Fac を計算する時間計算量はどれくらいですか?
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
説明するのは少し面倒なので、画像を直接見ることをお勧めします。
空間の複雑さの概念
スペース複雑度は数学関数式でもあり、アルゴリズムの動作中に占有される一時記憶スペースの尺度です。
空間複雑度はプログラムが占めるバイト数ではありません。これはあまり意味がないため、空間複雑度は変数の数によって計算されます。
空間計算量の計算ルールは基本的に実際の計算量と同様であり、ビッグ O の漸近表記も使用されます。
注:実行時に関数に必要なスタック スペース (ストレージ パラメーター、ローカル変数、一部のレジスタ情報など) はコンパイル中に決定されるため、スペースの複雑さは主に、実行時に関数によって明示的に要求された追加スペースによって決まります。
空間の複雑さの計算方法
例 1: バブルソートの空間複雑さを計算しますか?
void Bubb1eSort(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とiの 2 つだけです。
end と i はループが通過するたびに破棄されますが、ループが再び開始されるときに end と i を 再度定義すると、元のスペースが引き続き使用されるため、関数で使用される追加のスペースは2になります。
したがって、この質問の空間複雑さは次のようになります: O(1)
例 2: フィボナッチの空間複雑さを計算しますか?
フィボナッチ数列の最初の n 項を返します。
1ong 1ong * Fibonacci(size_t n)
{
if (n == 0)
return NULL;
1ong 1ong * fibArray = (1ong 1ong*)ma11oc((n + 1) * sizeof(1ong 1ong));
fibArray[0] = 0;
fibArray[1] = 1;
for (inti = 2; i <= n; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
n+1 の配列が開かれているため、 明らかに空間計算量はO(N)です。
ちなみに、時間計算量を計算してください。O (N) 関数には for ループの層が 1 つだけあります。
例 3: 階乗再帰 Fac の空間計算量を計算しますか?
1ong 1ong Fac(size_t N)
{
if (N == 1)
return 1;
return Fac(N - 1) * N;
}
各再帰ではスタック フレームを作成する必要があり、各スタック フレームは一定数の変数を作成し、n 回再帰して n 個のスタック フレームを作成するため、空間計算量はO(N)になります。
再帰関数の空間複雑さは再帰の深さに関係します
時間と空間の複雑さの応用
消えていく数字
原題へのリンク:リコウ
アイデア:
方法 1 : まず配列内の要素を並べ替えます。すぐに並べ替えるには qsort を使用する必要があります。時間計算量は O(n*log2N) です。
方法 2 : 最初に最初の n 項目の合計と sum =(1+n)*n/2を求め、次に必要な数x=sum-(a[0]+a[1]+a[2]+) を求めます。 ...+ a[n-1])、時間計算量は O(N)、空間計算量は O(1)
方法 3 : 長さ n の配列を作成し、元の配列の中央値が存在する位置に値を書き込み、配列を走査して値のない項目を見つけます。時間計算量は O(N) で、空間は複素数です。次数は O(N) です。
方法 4 : 値 x=0 を与え、最初に x を 0~n と XOR 演算し、次に x を配列内の各値と XOR 演算します。最後に x は必要な数値で、時間計算量は O(N) です。
問題を解決するには、方法 4 を使用します。
int missingNumber(int* nums, int numsSize)
{
int x=0;
//跟[0,n]异或
for(int i=1;i<=numsSize;i++)
{
x^=i;
}
//在跟数组中值异或
for(int i=0;i<numsSize;i++)
{
x^=nums[i];
}
return x;
}
回転配列
原題へのリンク:リコウ
方法 1 :暴力的な解決法、k 回回転する 時間計算量は O(N*K)、空間計算量は O(1)
方法 2 : 空間を時間と交換するために追加の空間を開く時間計算量は O(N)、空間計算量は O(N)
方法 3 : 最初に最初の nk を反転し、次に最後の k を反転し、最後に全体を反転します。時間計算量は O(N)、空間計算量は O(1) です。
問題を解決するには、方法 3 を使用します。
void reverse(int*nums,int left,int right)
{
while(left<right)
{
int tem=nums[left];
nums[left]=nums[right];
nums[right]=tem;
++left;
--right;
}
}
void rotate(int*nums,int numsSize,int k)
{
if(k>=numsSize) k%=numsSize;
//前n-k个数逆置
reverse(nums,0,numsSize-k-1);
//后k个逆置
reverse(nums,numsSize-k,numsSize-1);
//整体逆置
reverse(nums,0,numsSize-1);
}
全文終わりました~~