アルゴリズムとデータ構造(3):基本的なデータ構造 - リスト、スタック、キュー、木々が根付い

もともと今日はソート・ヒープで紹介したいと思います。使用ツリーヒープソートする必要があるが、しかし、基本的には、ツリーの概念とそれを使用するだけでなく、唯一の完全なバイナリツリーを必要とし、実際の実装もこれだけ、最初の主要なソートアルゴリズムを終了したかった配列であり、ツリーの概念について簡単な話。しかし、私はそれが木で唯一のコンセプトですが、執筆の過程であることを発見したが、木が理解すれば概念は言わなかった、実際には、本当に理解していません。だから私は、最初に基本的なデータ構造を導入することを決定し、その後、次の記事は、ソート・ヒープ紹介します。人々の読書、どのようにそれが鳩呼び出すことができますか?これは、戦略的調整、戦略的調整のノウハウを何ですか?私は、古代の儒教学者を教えてくれチューリング言っASDFGHJKL /、;!。「」

ない鳩は鳩、次の時間ではありません。

リスト

リストには、各オブジェクトは、線形順序であり、例えば、配列と多くの類似点が存在している、非常に、非常に重要なデータ構造です。彼らの最も重要な違いがあることである配列がある静的のコレクションにリンクされたリストがあるダイナミックのコレクション。私たちは生き生きと2の間の差を示し、以下の図を使用します。

図は、片方向リンク・リストの配列の、それぞれ、概略図です。配列使用する前に良い宣言するために使用するスペースの必要性を文が後に、空間に変更することはできませんが、データストレージは、連続して挿入要素を除去するための後続のすべての必要が移動するが、添字アドレッシング ; リストはちょうど反対で、使用してフロントステートメントを使用する必要なく、空間実際の状況(スペースの使用も変化)に応じてデータの数が、場合、付加および欠失を用いることができるデータストアが連続していない、高い挿入効率は削除するが、添え字によって対処しません

配列とリスト上の図は、その背後のスペースに矢印を持っている理由は注意深い学生が発見できますか?はい、それがあるかどうか、配列リスト、彼らの本質的ポインタ、全ての矢印上の図は、平均値のポインタです。もしINTアレイ[20]配列を宣言し使用する場合、配列は、本質的にバックポインタで使用され、例えば、次のいずれかの配列を使用することができる[1]第2のデータを取得するために、*(配列+ 1を使用することができます)と同じ効果を達成します。

そして、リストの構造はどのようなことでしょうか?

上記のように、鎖は一般的に分割されている二つの部分、リスト内の最初のノードへの1つのポインティングリスト、一般的に配列名のアレイの動作と同様のリスト、を指すために使用され、他方が真内部リスト記憶されたデータでありますノードノード

なぜそれは、2つの部分に分かれていますか?我々は言及上記のように、この設計の性質はでの模倣配列に設計されて検討する読者を招待します。例えば、私は今、ボブ私は、そのような識別子が存在しない場合は、することができ[2]彼は三つの要素1の配列を使用する必要がある場合、その後にのみ使用ARRAY1に必要な、配列1と配列2という名前の2つの配列を定義伝えます私たちは、[2]は要素を選択するために使用しますか?私は三つの要素2の配列を使用したい場合は、私たちはどのように何をすべき?もちろん、これは唯一の小さな問題で、理論的には、我々はまた、彼に対処するために他の手段を使用することができますが、シングルヘッドの定義は別のより重要な理由を持って、我々は中にリストを向かうことができる属性の店舗全体のリストなどのリストとして、要素、秩序あるかどうかなどの数。私たちは、リスト内の要素の数を知りたい場合は、単に適切な情報への直接アクセスから頭、そして常に再びリスト全体をスキャンし、結果を返しません。もちろん、私はそれでノードにこの情報を書き込むことができると言いましたか?私は追加する必要があるノードを、あなたがすることができ、主要な情報のリスト全体を格納していますが、彼は、各ノード行くをコピーしたいですか?あなたがブロック鎖ああだと思いますか?また、ストレージを分散?あなたは私のリストのノードがあなたに嘘ではないだろう恐れていますか?私たちはあなたが本当に私の長さが10であると信じていなかったと言う10節の長さの51%未満に?

私たちは、と呼ばれる、最も人気のあるリストのリストの上に言及した二重にリンクされたリストのリスト、実際には、意味は非常に簡単です、探索方向が2を持って、両方の前方検索(ノードへ前のポインタポイント)、も可能に後に(次のノードへの次のポインタ)を検索し、第1ノードフロントない他のノードため、前のポインタの最初のノードがNULLであり、次のポインタもNULLにエンドノードトークンと同じです。同様れる単独リストをリンクされた:のみPREVポインタまたは次に、一方向のみに検索。循環リストの前のポインタ、エンドノードポイントに最初のノード、最初のノードのエンドノードを指す次のポインタ(よりむしろ双方向リンクリストがNULLです)。あなたのような場合は、私たちが語ら他のデータ構造に続いて、理論的には、奇妙なリストの多様性を定義することができ、実際にいくつかの奇妙なリストが広く使用されています。

以下のリストの完全なコードは私であってもよく、githubの上で見つかりました。

次のように定義されたノードのノードリスト:

// 列表节点
typedef struct ListNode{
    int data;
    struct ListNode* prev;
    struct ListNode* next;
}ListNode;

次のようにリストが定義されたリスト:

// 列表
typedef struct List{
    struct ListNode* head;
    int length;
}List;

リストの初期化:

// 初始化一个列表,不包含任何数据
List* ListInit(){
    List* list = (List*)malloc(sizeof(List));
    list->head = NULL;
    list->length = 0;
    return list;
}

リストから検索し、指定されたノードを返します。

// 在list列表中搜索data为number_to_search的第一个节点,并返回此节点
ListNode* ListSearch(List* list, int number_to_search){
    ListNode* node = list->head;
    while(node != NULL && node->data != number_to_search){
        node = node->next;
    }
    return node;
}

リストに新しいノードを挿入します。

// 新建一个data为number_to_add的节点,并将其添加至list列表头部
int ListAdd(List* list, int number_to_add){
    // 新建节点
    ListNode* node_to_add = (ListNode*)malloc(sizeof(ListNode));
    node_to_add->data = number_to_add;
    node_to_add->next = list->head;
    node_to_add->prev = NULL;

    // 将新节点插入至列表头部
    if(list->head != NULL){
        list->head->prev = node_to_add;
    }
    list->head = node_to_add;
    list->length ++;
    return 0;
}

リンクリストで指定されたノードを削除します:

// 从list列表中删除node节点,并free掉node节点
int ListDelete(List* list, ListNode* node){
    if(node->prev == NULL){
        list->head = node->next;
    } else{
        node->prev->next = node->next;
    }

    if(node->next != NULL){
        node->next->prev = node->prev;
    }
    free(node);
    list->length --;

    return  0;
}

私たちは、コードが多くのであり、コード内で見ることができるの判断境界条件面倒なボーダー裁判官、それを必要としないどのような方法があり、それは不便であると言いますか?もちろん、学生には、当社の上記の循環リストがリコールされてわかりませんか?はい、我々は次の、NULLに、我々は無記号ノードと呼ばれるかもしれないすべてのノードのノード、テール・ノードナシ、他のノードのノードの使用と全く同じ構造で、前のポイントでこのノードをポイントに頭と尾のポインタをリスト最初のノードので、あなたの左の読者に境界条件を決定する必要はありません、このアプローチの具体的な実装を指します。

スタックとキュー

スタックとキューはシンプルなのに、動的なコレクションですが、非常に効果的。スタックは、(最後の-で,,最初のアウトLIFOを使用して 、LIFOは、 彼らが今の誇大宣伝されているとして名前が本当に物事のどのような種類の正直な感覚を反映している)文字通りの意味での戦略、そして最後に、このコレクション要素がされます最初に導入します例えば、我々は通常、アンドゥ機能テキストエディタは、最初に元に戻すには、あなたが最終的に、これはアプリケーションの典型的なスタックであることを行動である使用します。FIFOを使用してちょうど反対のキューとスタックは、(最初のアウト、最初に 、FIFOは) 戦略で、入力する最初のキュー要素がなる初めて導入し、例えば、通常の生活のキューを、人々のキューが最初に放電を始めます。

スタック操作キューとが含まれているすべての、また非常に類似している挿入は、(エンキュー、ENQUEUEキューと呼ばれるスタックプッシュ、スタックと称する)を削除キューDEQUEUE呼ばスタックPOP、スタックと呼ばれます(チーム)。

スタック

チームのスタックとキューは、予備的な理解を持って、我々はスタックを詳細に説明します。スタックはどちらか、私たちは(実際には同じ理由で、ほとんど差をリンクリストをされて使用して実現)を達成するために、配列を使用しますが、単純に説明するために、達成するために、リストを使用することができ、達成するために配列を使用することができます。

図は二つのポインタ、スタック・ポインタの下の下、トップスタックポインタ、毎回私たちのPUSH操作のスタックを備え、スタックの概略図である場合には、トップポインタがインクリメントされ、その後、データをコピーし、現在のポインタのトップへ下に示すような、位置を指します。

スタックはこの手順の逆のプロセスである、トップポインタは、一つのデータに戻るだけデクリメントされます。

最も重要なことは、それはあなたがしたいですか何ボトムポインタを見ていないPUSHスタックと操作をPOPするのですか?実際には、ボトムポインタマーク最も重要な役割は、これをスタックすることで開始する場所ですが、また、私たちは、スタックが空であるか否かを判断するのに役立つことができ、そのようなスタックなどのクロスボーダーの問題を防ぐために。

私たちは、スタックポインタを1つのトップデクリメントさについて話すとき、データはまだそれの内側に積み重ねることができることを我々はまた、ノート?実際には、心配する必要スペースは本当に、そこかどうかを確認するためのデータ、データの中に存在しない場合は、あなたの解釈によると、裁判官には良いが、実際には、その瞬間から組み立て全体のコンピュータは、彼のスペースが「フル」となっているので、コンピュータの性質に関するデータは、0-1文字列です-ので、低レベルのビットが高く、それ手段ではありません1または0のいずれか。あなたは、コンピュータ内のファイルを削除すると、データは実際にそれを消去するには、ドキュメントのですか?いいえ、マークをした他のプログラムを伝えるだけで、オペレーティング・システム:これらはあなたがスペースを必要とする場合は、直接これらの場所を使用することができ、有用なデータではありません。これは、誤って削除したファイル、データ復旧の原則である-長い間、このスペースは、まだ他のデータに書き込まれていないとして、その後、私はちょうどそれをマークしてから戻ってくると場所がファイルを持っていることを伝える必要があるとして、ファイルが成功します(もちろん等、ファイルインデックスを変更するなどの操作のいくつかの他の詳細、を伴う)を回収。スタックポインタは、この時点で、我々だけで缶トップをデクリメントされたときにも同様したがって、我々のために説明するのでトップポインタは、スタック内のデータの標準部分よりも大きくありませんまた、どのような方法は、あなたがこのデータを消去しますか?それが0に割り当てることができますか?私は0それを自分自身をスタックした場合は?

完全なコードは、私の下のスタックで見つけることができますgithubの上で見つかりました。

次のようにスタックが定義されています。

// 定义栈
typedef struct Stack{
    int* array;
    int top;
    int length;
}Stack;

スタックの初期化:

// 初始化栈
Stack* StackInit(int stack_length){
    Stack* stack = (Stack*)malloc(sizeof(Stack));
    stack->array = (int*)malloc(sizeof(int) * stack_length);
    stack->top = -1;
    stack->length = stack_length;
    return stack;
}

一般的にスタックヘルパー関数を使用しました。

// 判断栈是否已满
int StackIsFull(Stack* stack){
    return (stack->top >= (stack->length - 1));
}

// 判断栈是否为空
int StackIsEmpty(Stack* stack){
    return (stack->top < 0);
}

プッシュ操作:

// 压栈
int StackPush(Stack* stack, int number_to_push){
    if(StackIsFull(stack)){
        return -1;
    } else{
        stack->top ++;
        stack->array[stack->top] = number_to_push;
        return 0;
    }
}

ポップ操作:

// 出栈
int StackPop(Stack* stack){
    stack->top --;
    return stack->array[stack->top + 1];
}

上記のコードのために、我々は原因C言語自体の制限のために、これ以前にコールスタックは、(StackPopケースで空にされていないことを確認するために、自分でStackIsEmpty()関数を呼び出すようにユーザーに必要なことをポップことに注意する必要がある)をスタックの、それは、国境を越えるためにつながります予測不可能なエラーを引き起こします。しかし、あなたには、いくつか使用している場合にスロー Javaなどの高級言語の機能をスタックが空であるが、ユーザーはまだポップ操作を行い、未熟練誤作動を避けるために、ユーザーを強制するためにならば、あなたは例外をスローすることができます。

余談は:実際には、私は上記のコードは、特別な事情がない場合は、C言語、非常に準拠したC言語ではありません書き込み関数の戻り値は、関数を決定するために使用される効果実行するのを。戻り0は、例えば、リターンを通常の動作を示し-1アプリケーションに障害が発生したようなオーバーフローは、リターン-2(等、例えば、通常の変数を使用して、入ってくるデータは、計算がポインタ戻りデータを使用して行われる)パラメータを使用して実装され、必要なデータ転送の全ては、十分なスペースを示し示しますこのようなC言語の簡単な入出力関数のscanf()のprintf()など。サンプルコードので、私は多くの注意を払っていないこれらの詳細。

キュー

同様に、キューにもどちらか、あなたも達成するために配列を使用することができ、達成するためのリンクリストを使用しています。また、例えば、配列を使用します。差は、スタック、キュー、キューということである挿入データは、中に挿入されるテール次いで、テール・ポインタは、1だけインクリメントされ、ポインタの位置に導入データときにヘッド元のデータ先頭位置に戻り、その後、ポインタがインクリメントされるポインタによって示されます。

異なるキューおよびリンクリスト、設計、スタックは、私たちが使用を開始するために0位置からマークされなければならないので、多くの空間配列を、スタックはいつでもそんなにスペースを使用することができます。操作の数の後に、例えば、アレイ10の元の長さが定義されている場合は、FIFOキューので、我々は、デキューした後、私たちの前にスペースが位置5でラベルされたポイントのストレスへのヘッドポインタを5回か行いましたあなたはそれを使用することはできませんか?ないもちろん、私たちが使用することができます。この時点で呼び出された配列をループ:設計配列の添字の上限値を超えたときに、添字0は再び増加し始めます。実装するために実際には、非常に単純な、非常に背の高い上の外観に聞く:通常の状況下では、インデックスは長さテイクの配列にするたびに、その後、単純に+演算子こと、そして後のサイクル配列内の+演算子新しい値を増加させたとき添字は、配列の長さを超えているので、私は、それが再び増加I戻るアレイ添字0と等価であると解釈されるであろう。あなたはこの一節を理解していない場合は、我々は次のテキストを読むことができ、チームに操作機能。

私の中のキューの完全なコードgithubのは見ることができます。

キュー定義:

// 队列定义
typedef struct Queue{
    int* array;
    int length;
    int head;
    int tail;
}Queue;

キューの初期化:

// 初始化
Queue* QueueInit(int queue_length){
    Queue* queue = (Queue*)malloc(sizeof(Queue));
    queue->array = (int*)malloc(sizeof(int) * (queue_length + 1));
    queue->head = 0;
    queue->tail = 0;
    queue->length = queue_length + 1;
    return queue;
}

コードの実装では、我々は大きなキュー1の長さよりも配列の長さを適用することに注意してください、確認してください模式的な読者は、キューの上に達成、キューの先頭のポインタ位置があるキュー要素のヘッド、キューのテール・ポインタの場所があるチーム最後の要素の上付き+ 1私たちは、同じの待ち行列の長さの配列の長さを適用すると、読者は添え字が同じであるかどうかキューが空になると、キューがいっぱいの、ヘッドポインタとテールポインタポイントを想像してください?その結果、私たちは、キューが空である、またはキューがいっぱいであるかどうか、ヘッド==尾を決定することができないとき。もちろん、我々はまた、私たちは余分な長させずにこの問題を解決できるようにするために他のマーカーを使用することができますが、これは、あまりにも多くのエラーが発生しやすいだけでなく、フローを決定するために、全体の複雑な問題を作るためにバインドされ、作業効率が低下します。

補助機能、共通のキュー:

// 判断队列是否已满
int QueueIsFull(Queue* queue){
    return (((queue->tail + 1) % queue->length) == queue->head);
}

// 判断队列是否为空
int QueueIsEmpty(Queue* queue){
    return (queue->head == queue->tail);
}

チームへ:

// 入队
int QueueEnqueue(Queue* queue, int number_to_enqueue){
    if(QueueIsFull(queue)){
        return -1;
    }

    queue->array[queue->tail] = number_to_enqueue;
    queue->tail  = (queue->tail + 1) % queue->length;
    return 0;
}

チーム:

// 出队
int QueueDequeue(Queue* queue){
    int return_value = queue->array[queue->head];
    queue->head = (queue->head + 1) % queue->length;
    return return_value;
}

根付いた木

私たちは、ほぼすべての木を含むデータ構造の達成することができ、上記のリストをご紹介します。

家系のが大きすぎるので、我々がただ簡単に私たちの追跡調査、コードレベルではない実際の実装を容易にするため、ツリーの概念を紹介しますので、完全な記事を説明することは不可能です。その後、我々は適切なタイミングで、家系図の詳細な説明を綿密多くを学びますし、コードレベルで実装します。

ツリーのノードは、一般的に備え、三つの部分データポインタ、子ノードのポインタを。ここでは、例として説明し、最も単純な二分木で始まります。後述するように、主要な問題を強調するために、すべてのデータ部分に示されているツリーノードを省略する。

二進木

上記のように、これは我々がルート付きツリーを呼ぶ理由であるルートノードの上部と呼ばれるバイナリツリーの概略図です。スペースが制限されるため、我々は、図中の各ノードのデータ部分を省略しています。バイナリツリーは、各ノードが含まれ、親ポインタと2つのつの子ノードのノードが親(これはルートノードのみで起こる)、次に親ノードポインタを持っていない場合、(左の子と右の子と呼ぶ)のポインタを子が残っていない場合には、左の子ポインタがNULL、何も右の子が存在しないのと同じ方法で、NULLです。このようなデータ構造は適切に私たちの木と呼ばれています。自然の中で、同じ木ではありません、私たちのツリーが後ろ向き一般的ですが、トップがあるノードで底があるリーフノード

無制限根ざした木の枝

我々は通常、バイナリツリーは、最も頻繁に使用され、最もシンプルな木、そしてどのようにツリーを対応します。だから、どのように我々は、多分岐の木を表現する必要がありますか?それは、バイナリツリーと同じで、多くの子どもたちは、いくつかの子ノードポインタがありますがありますか?もちろん、これはまた、達成するための方法ですが、合理的ではない- 使用のバイナリ頻度が非常に高く、彼らの個々の設計構造は非常に合理的ですが、他のバイナリツリーと何に加えて、非常に一般的な、異なるプログラムであります少し汎用性の間にツリー構造、および私は百万ツリーが必要と言うならば、あなたは何のコードでコード子ノードは1万回をコピーする必要はありませんか?

次のように我々は通常、マルチツリー構造を使用します。

まったく同じバイナリツリーデータ構造ではなく、解釈がある異なります以下、互いに兄弟と呼ばれる同じ行のノードに位置する、親ノードが上記である子ノードです。オリジナルの左の子は「兄弟」、彼らの「仲間」と「兄弟」との子となるために、元の権利、「兄」は、元の親ノードがまだ親であるが、他の旧兄弟の親に子ノードになりノードは、「兄弟」ノードになります。のみによって異なる解釈に、完全のデータ構造変更ケースを、我々はだろうマルチツリーを達成するため、ノードの数は、サブツリーのいずれかよりも大きいです。

慎重に実際にそれらの間に差がないバイナリと多木の上に私たちの例では、読者を遵守してください?

エピローグ

この記事では、4つの基本的なデータ構造を説明し、読者がリストの概念の深い理解を持っている必要があり、ほとんどすべてのデータ構造のリストから、影で見つけることができます。次の記事我々は、ヒープの並べ替え(間違いない鳩を!!!)ご紹介します、ヒープの並べ替えは、ツリーの概念を実現するために使用されますが、それは木の特別な種類である - 完全なバイナリツリー。

オリジナルリンク:albertcode.info

個人ブログ:albertcode.info

マイクロチャンネル公共数:AlbertCodeInfo

おすすめ

転載: www.cnblogs.com/AlbertShen99/p/12553030.html