第 1 章 データ構造とアルゴリズムの概要 (C 言語)

目次

1.1. データ構造の基本概念と学習方法

1.1.1. データ構造の調査対象

1.1.2. データ構造の基本概念と基本用語

1.2. アルゴリズムとデータ構造

1.2.1. アルゴリズムの概念

1.2.2. アルゴリズムの記述方法

1.2.3. アルゴリズム解析

1.2.4. 時間計算量 (この章の焦点):

典型的な例 - 消える数字

1.2.5. 空間の複雑さ

古典的な例 - 回転配列


1.1. データ構造の基本概念と学習方法

1.1.1. データ構造の調査対象

(1) データ構造では、主にさまざまな論理構造や記憶構造、データに対するさまざまな操作(非数値計算分野の問題)を研究します。

(2) データ構造は通常、データの論理構造、データの物理記憶構造、データの操作 (またはアルゴリズム、操作) の 3 つの側面に分けられます。

注: ①.アルゴリズムの設計はデータの論理構造に依存します。

       ②.アルゴリズムの実装はデータの物理的なストレージ構造に依存します

1.1.2. データ構造の基本概念と基本用語

(1) データ:

  データは、コンピューターに入力してコンピューターによって処理できるすべてのシンボルの集合です。

(2) データ要素:

データ要素はデータの基本単位であり、通常はコンピューター プログラムで全体として処理および考慮されます。

(3) データ項目:

データ項目は、データ構造で議論される最小単位です。

注: ① データ要素が細分化できる場合、それぞれの独立した処理単位がデータ項目となります。

       ②. データ要素はデータ項目の集合です。

       ③. データ要素を細分化できない場合、データ要素とデータ項目は同じ概念です。

(4) データ構造:

データ構造は、相互に 1 つ以上の特定の関係を持つデータ要素のコレクションです。

注: データ構造 = データ要素 + データ要素間の相互関係 = オブジェクト + 関係、

(5) 論理構造:

論理構造とはデータ間の相互の論理関係であり、データの保存とは関係ありません。コンピュータからは独立しています。

論理構造は通常、次の 4 つの構造に分かれます。

① 線形構造:データ構造内の要素間には 1 対 1 の関係があります。

関係図は次のとおりです。

 ②. ツリー構造: データ構造内の要素間には 1 対多の関係があります。

関係図は次のとおりです。

 ③. グラフィカル構造: データ構造内の要素間には多対多の関係があります。

関係図は次のとおりです。

④. 集合構造:データ構造の要素間には「同じ集合に属する」という相互関係以外の関係はありません。

関係図は次のとおりです。

 (6)*物理的構造 (または保管構造):

物理構造は、コンピューター内のデータ構造の表現 (イメージとも呼ばれます) です。

注: 物理構造には、データ要素のマシン内表現と関係のマシン内表現が含まれます。

(7)*データ型:

データはデータ構造によって分類され、同じデータ構造のデータは同じカテゴリに属し、同じカテゴリに属する​​データ全体をデータと呼びます。

注記:

①:高級プログラミング言語では、データ型はデータの属性であるため、データ型は値の集合と、この値の集合に対して定義された一連の操作の総称であると考えられます。

②:高級言語におけるデータ型は、アトミック型と構造型の2つに分類できます。アトミックタイプの値は分解できませんが、構造タイプの値は特定の構造に従っていくつかのコンポーネントで構成され、分解でき、そのコンポーネントは構造化または非構造化のいずれかになります。 

(8) *抽象データ型 (ADT):
抽象データ型は、数学的モデルと、この数学的モデル上で定義された一連の演算を指します。

注: 抽象化の意味は、データ型の数学的抽象化特性にあります。データ構造のデータを抽象データ型のデータオブジェクトとみなし、データ構造の関係を抽象データ型のデータ関係とみなし、データ構造上のアルゴリズムを基本演算とみなします。抽象データ型。

1.2. アルゴリズムとデータ構造

注: コンピューター科学者の N. ヴィルスは、アルゴリズム + データ構造 = プログラムという公式を提案しました。

1.2.1. アルゴリズムの概念

(1)アルゴリズムは、特定の問題を解決するための手順の記述であり、各命令が 1 つ以上の操作を表す、限られた命令のシーケンスです。

(2). アルゴリズムには次の5 つの重要な特性がなければなりません。

① 有限性: アルゴリズムは、正当な入力値に対して常に有限ステップを実行した後に終了する必要があり、各ステップは有限時間内に完了できます。

②. 決定性: アルゴリズム内の各命令は正確な意味を持っていなければならず、読むときに曖昧さがあってはなりません。

③. 実現可能性: アルゴリズムで記述された演算は、基本演算を限られた回数実行することで実現可能です。

④. 入力: アルゴリズムには n (n>=0) 個のデータ入力があります。

⑤. 出力: アルゴリズムには、入力と特定の関係を持つ量である有効な情報の出力が 1 つ以上必要です。

1.2.2. アルゴリズムの記述方法

アルゴリズムは、自然言語プログラミング言語準プログラミング言語フローチャートなどで記述できます。

1.2.3. アルゴリズム解析

アルゴリズムを設計するときは、通常、次の目標を達成することを考慮する必要があります。

①:正しさ

②:可読性

③:堅牢性

④: 高効率: アルゴリズムの実行時間が短く、必要な記憶容量も少なくて済みます。

1.2.4. 時間計算量 (この章の焦点):

(1) 一般に、アルゴリズムに含まれる基本演算の実行回数をアルゴリズムの時間計算量といいます。これは、アルゴリズムの実行時間の相対的な尺度です。

(2)問題のスケールは、問題を解くためのアルゴリズムの入力量であり、一般に整数 n で表されます。

(3). アルゴリズムの時間計算量は、 T(n)として記録される問題サイズの関数として見ることができます

(4) 実際、対応する桁数が大まかに計算されている限り、一般にアルゴリズムの時間計算量を正確に計算する必要はありません。この方法は「ビッグオーの漸近表現」と呼ばれます

 基本的な考え方: 大きな取引を掴む/決定的な結果を得る。

例:

1).T(n)=n^2+2n+10 ——> O(n^2) //大きな頭を捕まえる

2) .T(n)=2n+10 ——> O(n) //係数は省略する必要があります

3).T(n)=M+N ——> O(M+N) //M と N の影響は不明

4).T(n)=3M+2N ——> O(M+N) //M と N の影響は不明

5).T(n)=M+N ——> O(N) //N の影響は M の影響よりはるかに大きい、または N と M の影響は等しい

6).T(n)=M+N ——> O(M) //M の影響は N の影響よりはるかに大きい、または N と M の影響は等しい

7).T(n)=100 ——> O(1) 

8) .T(n)=10000000 ——> O(1)

注: O(1) は時間ではなく、一定の時間を表します。   

(5) アルゴリズムの時間計算量には最良の状況、最悪の状況、平均的な状況があり、現時点では最悪の状況の時間計算量が支配的である。                    

 例:

1). バブルソートアルゴリズムの時間計算量 ( O(n^2) )

①: 最良のケース: 配列はすでにソートされており、一度だけ走査する必要があります -> O(n)

②: ワーストケース: 配列の逆順、時間は (n-1)+(n-2)+...+2+1、つまり (n*(n-1))/2 — —> O(n^2)

2). 二分探索アルゴリズムの時間計算量 ( O( \log_{2}N) )

①: 最良のケース: 初めて発見された ——> O(1)

②: 最悪の場合: 検索間隔のスケーリングに値が 1 つだけ残っている場合 (最後の値が見つかった場合)。

 写真からわかるように、回数に加えて、2は検索した回数を意味します

x 回検索したとすると、
2^x=Nになります。

つまり、
x= \log_{2}N=T(n)

つまり、時間計算量は O( \log_{2}N) または O( \log N)です。

注: 対数はテキストで記述するのに不便です。数式表示をサポートする一部のエディタでのみ記述できます。したがって、対数が 2 に基づく場合は、次のように 2 を省略できます: O( ) ~
O \log_{2}N( \log N)

3).図に示すように ( O(2^N) )

  

再帰に遭遇したとき、編集者は図を描くことでより理解が深まると考えます。

この関数は、以下の図の右下隅にある三角形の切り取りに抽象化できます。

再帰を段階的に入力すると、毎回の再帰の回数には次の規則があることがわかります。

 したがって、基本演算の数は (2^0)+(2^1)+(2^2)+...+(2^N-1) とほぼ等しくなります。合計は (2^ N) -1

したがって、時間計算量は次のようになります: O(2^N)

(6)、性能比较:O(1)<O(log n)<O(n)<O(nlog n)<O(n^2)<O(n^3)<O(2^n)< O(n!)<O(n^n)

典型的な例 - 消える数字

 エディターは、この質問に対して合計 3 つのアイデアを提供します:
(1). 最初に配列をソートし、次に走査します。時間計算量は次のとおりです: O(logn*n)

(2). 0 から n までの算術シーケンスを合計し、配列内の数値を減算します。結果は、消えた数値と等しくなります。時間計算量は次のとおりです: O(n)

(3).「単一の犬のアイデア」: XOR (^)

よく使用されるいくつかの公式:

①:a^b ^b=a   

②:a^b ^a=b

③:a^b^a^b=a^a^b^b(交換法則)

ここで、消えた数字をxと仮定し、上式のa(つまり数字x)とします。

0 から n までの等差数列から数値 x を除いた残りの配列は、上記の式の b (つまり、配列 nums) です。

つまり、a^b は、0 から n までの等差数列内のすべての数値が排他的論理和されます: (1^2^3^4^...^x^...^n)

したがって、公式 a^b^b =a によれば、次のことがわかります。

1^2^3^...^x^...^n-1^n^(数字)=消える数字x

 したがって、コードに示されているように、T(n)=2n+1

したがって、時間計算量は次のようになります: O(n)

1.2.5. 空間の複雑さ

(1). 空間の複雑さに関する関連する概要は次のとおりです。

 初心者が概念だけを話すと間違いなく混乱するでしょう。理解を助けるためにいくつかの例を次に示します。

 

例:

1). 下図に示すように、一般に形状参加空間複雑度は関係ないため、配列 a や整数 n は空間複雑度としてカウントされません 空間複雑度はバブル実装時に一時的に開けられる空間です以下の図に示すように、ソート アルゴリズム を使用します。開いた空間には end、exchange、i が含まれるため、T(n)=3、空間複雑度は O(1) になります。

 2).フィボナッチ数列の空間複雑度を計算する

 このアルゴリズムによって一時的に開かれた空間には上の赤いボックスがあることがわかります。つまり、T(n)=n+2、空間複雑度は O(n) です。

注: 空間計算量は時間計算量よりも単純で、そのほとんどは O(n) または O(1) です。 

3). 再帰の空間複雑さを見てみましょう:

 前述したように、再帰に遭遇したとき、それを理解するために絵を描くことに慣れています。

 上記より、一般的な再帰もアルゴリズムに属するため、N 以降の再帰では、スタック フレームによって開かれた空間も一時的に開かれ、各スタック フレームは一定の空間 (O(1)) を開き、N 個のスタック フレームが開かれます。 n 次元空間を開くため、このアルゴリズムの空間計算量は O(n) です(この説明には知識点にいくつかの誤りがある可能性があり、参考およびアイデアの提供のみを目的としています)。

 4). フィボナッチ数再帰の空間複雑さを計算します。

 同様に、内部グラフを再帰的に描画します。

図に示すように、フィボナッチ再帰の空間計算量が計算されますが、このアルゴリズムの時間計算量は O(2^n) であると上で述べましたが、最初に空間計算量について学んだとき、私は深く理解しておらず、このアルゴリズムの空間計算量は時間計算量と同じであると思われがちですが、そうではありません。ここで非常に重要な文を覚えておく必要があります。

時間は蓄積されて二度と戻ってこない

スペースは再利用可能です。

上の図からわかるように、最初のレイヤー Fib(N-1) が再帰的に続くと、スタック フレームは合計 n 個のスペースを空けることになります。最初の再帰のラウンドの後、使用されたスペースは動作中のスペースに返されます。右側の Fib(N-2) を再帰するとき、再帰の最初のラウンドで使用された空間が再利用されるため、合計空間は n まで開かれるため、空間計算量は O になります。 (n)

(この説明には知識の誤りが含まれる可能性がありますので、参考程度にしてください。)

ここでの説明は少し抽象的かもしれませんが、理解に役立つ例を次に示します。

次のようなコードです。

void fun1()
{
	int a = 0;
	printf("%p\n", &a);
}

void fun2()
{
	int b = 0;
	printf("%p\n", &b);
}

int main()
{
	fun1();
	fun2();
	return 0;
}

実行結果は次のとおりです。

main 関数内で関数 fun1 と fun2 を順番に呼び出すと、2 つの関数で作成された変数が同じ領域を占有することがわかります。これは、fun1 関数が呼び出された後、そのスタック フレームが破棄され、使用されていた領域が失われるためです。オペレーティング システムは、fun2 を呼び出すときに、fun1 関数の変数によって使用されるスペースを使用できます (関数のアドレスは、スタック フレームによって開かれたスペースとは関係がないことに注意してください。2 つの関数のアドレスは異なりますが、作成された変数によって使用されるスペースは次のとおりです。同様に、ここでのエディターでは、関数スタック フレームの作成と破棄について学習することをお勧めします。これにより、理解しやすくなります)。

(2) 上記の例には基本的に、基本的な 90% 空間複雑度アルゴリズムが含まれていますが、これについては後で詳しく分析します。

古典的な例 - 回転配列

 

この質問に対する最良の解決策は次のとおりです。

時間計算量は O(n)

空間複雑度は O(1)

エディターはここで3 つの解決策を提供します。

注: 質問の意味を理解すると、k (回転数) が要素の総数に等しい場合、配列が復元されるため、どのアルゴリズムでも k% = n が設定されることがわかります。 (n は配列要素の数) 数値) を使用して、繰り返しの回転を排除します。

①: 変数を作成し、配列 nums の最後の番号を毎回記録し、最初の n-1 (n は配列要素の数) の番号を順番に後方に移動し、記録した番号を nums[0 ] に入れるだけです。次のように k 回ループします。

 

アルゴリズム:

時間計算量 O(n^2): 最悪のケースでは k==n であるため、配列の最初の n-1 個の数値を合計 (n*n) 回移動する必要があります。

空間計算量 O(1): 配列 nums の最後の値を記録するために一時変数が 1 つだけ作成されるためです。

②: 時間にスペースを使用し、一時配列 tmp を動的に割り当て、最初に配列 num の最後の k 番号を一時配列に貼り付け、次に配列 num の最初の nk 番号を一時配列に貼り付け、最後に一時配列を貼り付けます。配列全体を配列 nums に格納します (2 つの配列間で貼り付けるには関数 memcpy を使用するのが一般的です。さまざまな友達がそれを学ぶことができます)。

時間計算量: O(n): 後で一時配列 tmp の値を 1 つずつ nums に代入する必要があるため。

空間計算量: O(n): サイズ (n*sizeof(int)) バイトの配列が動的に開かれるため (nums と同じサイズ)

コードは次のように実装されます。

#include<string.h>
void rotate(int* nums, int n, int k)
{
	k %= n;
	int* tmp = (int*)malloc(sizeof(int) * n);
	memcpy(tmp, nums + n - k, sizeof(int) * k);
	memcpy(tmp + k, nums, sizeof(int) * (n - k));
	memcpy(nums, tmp, sizeof(int) * n);
	free(tmp);
	tmp = NULL;
}

 ③: この方法が最適です。次のように 3 回回転させます。

時間計算量: O(n):

空間複雑度: O(1):

 コードは次のように実装されます。

 

この章は以上です。お役に立てば幸いです。      

おすすめ

転載: blog.csdn.net/hffh123/article/details/131901140