コンテンツ
1はじめに
- 写真を使用して、最後のブログ投稿の最後に説明されている知識のポイントを確認してください。
以前のブログ投稿の説明から、完全な二分木と完全な二分木は配列で格納でき、それらの間の親子関係は添え字で表すことができることがわかりました。ここでは、物理構造が実際にはメモリに格納されていることを強調します。これは物理的には配列ですが、論理的にはバイナリツリーと見なす必要があります。ミルクに牛、ミネラルウォーターにミネラル、妻のケーキに妻がいないようなものです〜
- 完全な二分木と完全な二分木は配列への格納に適しているので、通常の二分木はどうでしょうか。読解:
通常の二分木は、無駄なスペースがたくさんある可能性があるため、配列に格納するのには適していません。完全な二分木は、順次構造での保管に適しています。スペース使用率が高いため、無駄がありません。実際には、通常、シーケンシャル構造の配列を使用してヒープ(一種のバイナリツリー)を格納します。ここでのヒープとオペレーティングシステムの仮想プロセスアドレス空間のヒープは、2つの異なるものであることに注意してください。データ構造ともう1つは、オペレーティングシステムの管理システムです。メモリの領域はセグメント化されています。完全なバイナリツリーまたは完全なバイナリツリーでない場合は、チェーンストレージを使用することをお勧めします。これは、前のブログ投稿で説明されており、繰り返されません。
ヒープは完全な二分木であり、配列に格納できます。次に、詳細に説明します。
ヒープの概念
上記からわかるように、ヒープは完全な二分木であり、そのすべての要素は完全な二分木の順序に従って1次元配列に格納されます。ヒープには、小さなルートヒープと大きなルートヒープの2種類があります。
- 小さなヒープ:各親ノードの値は、対応する子ノードの値以下であり、ルートノードの値は最小です。
- ラージヒープ:各親ノードの値は、対応する子ノードの値以上であり、ルートノードの値が最大です。
- ヒープのプロパティ:
- ヒープ内のノードの値は、常にその親の値より大きくも小さくもなりません。
- ヒープは常に完全な二分木です。
ヒープ構造
物理的な構造から、次の2つのポイントを知ることができます。
- 注文はヒープである必要があります
- 順序付けられていないものはヒープである可能性があります
2.ヒープの実装
2.1。準備
ヒープ構造を作成する
- アイデア:
上記から、ヒープの基本構造は配列であることがわかります。ヒープ構造を作成する場合、以前と同様に動的に開くことができます。操作プロセスも同様で、コードが直接追加されます。ただし、最初に小さなルートヒープを例として取り上げます。
- Heap.hファイル:
//创建堆结构 typedef int HPDataType; //堆中存储数据的类型 typedef struct Heap { HPDataType* a; //用于存储数据 size_t size; //记录堆中有效元素个数 size_t capacity; //记录堆的容量 }HP;
ヒープを初期化する
- アイデア:
ヒープを初期化すると、渡された構造体ポインターを空にすることはできません。まず、それをアサートする必要があります。残りの操作は、前のシーケンステーブルおよびスタックの初期化と同じです。
- Heap.hファイル:
//初始化堆 void HeapInit(HP* php);
- Heap.cファイル:
//初始化堆 void HeapInit(HP* php) { assert(php); php->a = NULL; php->size = php->capacity = 0; }
ヒーププリント
- アイデア:
実際、ヒープの印刷は非常に単純です。ヒープの物理構造は配列です。ヒープの印刷の本質は、前のシーケンステーブルの印刷と同じです。添え字にアクセスして、順番に印刷できます。
- Heap.hファイル:
//堆的打印 void HeapPrint(HP* php);
- Heap.cファイル:
//堆的打印 void HeapPrint(HP* php) { assert(php); for (size_t i = 0; i < php->size; i++) { printf("%d ", php->a[i]); } printf("\n"); }
ヒープの破壊
- アイデア:
動的に開かれたメモリの場合、使用後も破棄する必要があります。
- Heap.hファイル:
//堆的销毁 void HeapDestroy(HP* php);
- Heap.cファイル:
//堆的销毁 void HeapDestroy(HP* php) { assert(php); free(php->a);//释放动态开辟的空间 php->a = NULL; //置空 php->size = php->capacity = 0; //置0 }
2.2、ヒープ調整
ヒープスワップ
- アイデア:
ヒープの交換は比較的簡単です。以前に作成されたものと同じです。アドレスを渡すことを忘れないでください。
- Heap.hファイル:
//交换 void Swap(HPDataType* pa, HPDataType* pb);
- Heap.cファイル:
//交换 void Swap(HPDataType* pa, HPDataType* pb) { HPDataType tmp = *pa; *pa = *pb; *pb = tmp; }
ヒープアップ調整アルゴリズム
- アイデア:
このアルゴリズムは、データを挿入した後のヒープがヒープの性質と一致していることを確認するために個別にカプセル化される関数です。後で挿入する10の数字と同様に、最初に画像を描画します。
数値10を挿入した後もルートヒープが小さいことを確認するには、10と28を交換する必要があります。親が小さい場合は、親ノードの親と子ノードの子のサイズを順番に比較します。子ノードの場合は戻ります。それ以外の場合は、ルートまで常に交換されます。
前のルール、parent =(child --1)/ 2から、配列を操作していますが、それを二分木と考えています。調整プロセスを示すための図面:
- Heap.cファイル:
//向上调整算法 void AdjustUp(HPDataType* a, size_t child) { size_t parent = (child - 1) / 2; while (child > 0) { //if (a[child] > a[parent]) //大根堆 if (a[child] < a[parent]) //小根堆 { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }
ヒープ下方調整アルゴリズム
- アイデア:
まず、写真を使用して次のことを示します。
この時点で、この二分木は全体としてヒープのプロパティに準拠していないことがわかりますが、そのルートの左右のサブツリーは両方ともヒープのプロパティを満たしています。次に、それがヒープになるように調整します。ただの三部作。
- 左と右の子供たちの末っ子を探す
- 父親と比較し、父親より若い場合は交換する
- 交換された子の位置から下向きに調整し続けます
変更図は次のとおりです。
- Heap.cファイル:
//向下调整算法 void AdjustDown(HPDataType* a, size_t size, size_t root) { int parent = root; int child = 2 * parent + 1; while (child < size) { //1、确保child的下标对应的值最小,即取左右孩子较小那个 if (child + 1 < size && a[child + 1] < a[child]) //得确保右孩子存在 { child++; //此时右孩子小 } //2、如果孩子小于父亲则交换,并继续往下调整 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = 2 * parent + 1; } else { break; //如果中途满足堆的性质,直接返回 } } }
2.3、コア機能
ヒープ挿入
- 知らせ:
ヒープの挿入は、前のシーケンステーブルとは異なり、先頭に挿入したり、任意の位置に挿入したりできます。ヒープであるため、ラージルートヒープまたはスモールルートヒープの性質に準拠している必要があります。 、ヒープの元の構造は変更できないため、テール挿入が最適です。テール挿入後のヒープの性質に適合しているかどうかを確認してください。
たとえば、小さなルートヒープの性質に従って格納される配列の文字列があります。次に、図に示すように、配列の最後に数値10を挿入します。
- アイデア:
このツリーは、番号10が挿入される前の小さなヒープであり、挿入後ではないため、小さなルートヒープの性質が変わります。子ノード10はその親ノード28よりも小さいので、どうすればよいでしょうか。
何よりもまず、挿入する前に、ヒープの容量がデータを挿入するのに十分であるかどうかを最初に判断し、最初に容量を拡張するかどうかを確認し、次に拡張が完了した後に確認する必要があります。挿入された10は、ルート自体からルート、つまり祖先にのみ影響することがわかります。このパスがヒープの性質に準拠している限り、挿入は成功します。
コアアイデア:アルゴリズムを上方修正します。挿入された10が父親より28時間古いことがわかったら、この時点で番号を交換しますが、この時点で10は18より小さいので、もう一度交換し、最終的に10が15より小さいことがわかります。もう一度交換してください。もちろん、これは最悪のケースです。中間の変更中にヒープのプロパティが満たされている場合は、再度変更する必要はなく、直接戻るだけです。これは上方調整アルゴリズムと呼ばれ、上記の機能を直接適用することができます。
- Heap.hファイル:
//堆的插入 void HeapPush(HP* php, HPDataType x);
- Heap.cファイル:
//堆的插入 void HeapPush(HP* php, HPDataType x) { assert(php); //检测是否需要扩容 if (php->size == php->capacity) { //扩容 size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2; HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { printf("realloc fail\n"); exit(-1); } php->a = tmp; php->capacity = newcapacity; } php->a[php->size] = x; php->size++; //保持继续是堆,向上调整算法 AdjustUp(php->a, php->size - 1); }
- Test.cファイル:
void TestHeap() { HP hp; HeapInit(&hp); //插入数据 HeapPush(&hp, 1); HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); //打印 HeapPrint(&hp); //销毁 HeapDestroy(&hp); }
- 効果は次のとおりです。
ヒープの性質に準拠しています。
ヒープの削除
- 図に示すように:
- アイデア:
上記のヒープ挿入では、挿入後もヒープであることが明確になりました。ここでヒープを削除すると、削除後もヒープであることが保証されます。注:ここでのヒープの削除は、削除することです。ヒープの上部にあるデータ。小さなルートヒープを例にとると、ヒープの上部にあるデータを削除します。つまり、最小のデータを削除してから、それがまだヒープであることを確認します。
- まず、最初のデータを最後のデータと交換します
交換後、この時点でのヒープはその性質に準拠していません。これは、最後のデータが最初のデータよりも大きくなければならず、最後のデータがヒープの先頭に到達するため、ヒープではなく、左側のサブツリーになります。ルートノードと右側のサブツリーは影響を受けませんが、単独で表示するとヒープのままです。
- 次に、-sizeは、ヒープの最上位データが削除されることを保証します
シーケンステーブル--sizeと同様に、この時点でヒープの最上位のデータがヒープの最後に到達しているため、有効なデータが1つ減ることを確認します。つまり、最上位を確実に削除します。ヒープの
- 最後に、ダウンアジャストメントアルゴリズムを使用して、ヒープ構造であることを確認します
変更図は次のとおりです。
時間計算量分析
最初のデータと最後のデータの交換はO(1)であり、下方調整はノード数Nに応じて高さ時間を調整するため、下方調整アルゴリズムの時間計算量はO(logN)です。高さはおよそlogNであると推定できます
- Heap.hファイル:
//堆的删除 删除堆顶的数据 void HeapPop(HP* php);
- Heap.cファイル:
//堆的删除 删除堆顶的数据 void HeapPop(HP* php) { assert(php); assert(php->size > 0);//确保size>0 Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾 php->size--; //向下调整,确保仍然是堆结构 AdjustDown(php->a, php->size, 0); }
- Test.cファイル:
void TestHeap2() { HP hp; HeapInit(&hp); //插入数据 HeapPush(&hp, 1); HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); HeapPrint(&hp);//打印 //删除堆顶数据 HeapPop(&hp); HeapPrint(&hp);//打印 //销毁 HeapDestroy(&hp); }
- 効果は次のとおりです。
ヒープが空です
- アイデア:
ヒープの空の判断は非常に単純で、前のスタックシーケンステーブルと同じです。サイズが0の場合は、直接返すことができます。
- Heap.hファイル:
//堆的判空 bool HeapEmpty(HP* php);
- Heap.cファイル:
//堆的判空 bool HeapEmpty(HP* php) { assert(php); return php->size == 0; //size为0即为空 }
ヒープ内の要素の数を取得します
- アイデア:
サイズを直接返すだけです。
- Heap.hファイル:
//堆的元素个数 size_t HeapSize(HP* php);
- Heap.cファイル:
//堆的元素个数 size_t HeapSize(HP* php) { assert(php); return php->size; }
ヒープの最上位要素を取得します
- アイデア:
ヒープの先頭に戻るだけです。前提は、サイズ>0であることを表明することです。
- Heap.hファイル:
//获取堆顶元素 HPDataType HeapTop(HP* php);
- Heap.cファイル:
//获取堆顶元素 HPDataType HeapTop(HP* php) { assert(php); assert(php->size > 0); return php->a[0]; }
3.トータルコード
Heap.hファイル
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> //创建堆结构 typedef int HPDataType; //堆中存储数据的类型 typedef struct Heap { HPDataType* a; //用于存储数据 size_t size; //记录堆中有效元素个数 size_t capacity; //记录堆的容量 }HP; //初始化堆 void HeapInit(HP* php); //堆的销毁 void HeapDestroy(HP* php); //堆的打印 void HeapPrint(HP* php); //交换 void Swap(HPDataType* pa, HPDataType* pb); //堆的插入 void HeapPush(HP* php, HPDataType x); //堆的删除 删除堆顶的数据 void HeapPop(HP* php); //堆的判空 bool HeapEmpty(HP* php); //堆的元素个数 size_t HeapSize(HP* php); //获取堆顶元素 HPDataType HeapTop(HP* php);
Heap.cファイル
#define _CRT_SECURE_NO_WARNINGS 1 #include"Heap.h" //初始化堆 void HeapInit(HP* php) { assert(php); php->a = NULL; php->size = php->capacity = 0; } //堆的销毁 void HeapDestroy(HP* php) { assert(php); free(php->a); php->a = NULL; //置空 php->size = php->capacity = 0; //置0 } //堆的打印 void HeapPrint(HP* php) { assert(php); for (size_t i = 0; i < php->size; i++) { printf("%d ", php->a[i]); } printf("\n"); } //交换 void Swap(HPDataType* pa, HPDataType* pb) { HPDataType tmp = *pa; *pa = *pb; *pb = tmp; } //向上调整算法 void AdjustUp(HPDataType* a, size_t child) { size_t parent = (child - 1) / 2; while (child > 0) { //if (a[child] > a[parent]) //大根堆 if (a[child] < a[parent]) //小根堆 { Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } //向下调整算法 void AdjustDown(HPDataType* a, size_t size, size_t root) { int parent = root; int child = 2 * parent + 1; while (child < size) { //1、确保child的下标对应的值最小,即取左右孩子较小那个 if (child + 1 < size && a[child + 1] < a[child]) //得确保右孩子存在 { child++; //此时右孩子小 } //2、如果孩子小于父亲则交换,并继续往下调整 if (a[child] < a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = 2 * parent + 1; } else { break; } } } //堆的插入 void HeapPush(HP* php, HPDataType x) { assert(php); //检测是否需要扩容 if (php->size == php->capacity) { //扩容 size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2; HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity); if (tmp == NULL) { printf("realloc fail\n"); exit(-1); } php->a = tmp; php->capacity = newcapacity; } php->a[php->size] = x; php->size++; //保持继续是堆,向上调整算法 AdjustUp(php->a, php->size - 1); } //堆的删除 删除堆顶的数据 void HeapPop(HP* php) { assert(php); assert(php->size > 0);//确保size>0 Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾 php->size--; //向下调整,确保仍然是堆结构 AdjustDown(php->a, php->size, 0); } //堆的判空 bool HeapEmpty(HP* php) { assert(php); return php->size == 0; //size为0即为空 } //堆的元素个数 size_t HeapSize(HP* php) { assert(php); return php->size; } //获取堆顶元素 HPDataType HeapTop(HP* php) { assert(php); assert(php->size > 0); return php->a[0]; }
Test.cファイル
#define _CRT_SECURE_NO_WARNINGS 1 #include"Heap.h" void TestHeap1() { HP hp; HeapInit(&hp); //插入数据 HeapPush(&hp, 1); HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); //打印 HeapPrint(&hp); //销毁 HeapDestroy(&hp); } void TestHeap2() { HP hp; HeapInit(&hp); //插入数据 HeapPush(&hp, 1); HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); HeapPrint(&hp);//打印 //删除堆顶数据 HeapPop(&hp); HeapPrint(&hp);//打印 //销毁 HeapDestroy(&hp); } int main() { //TestHeap1(); TestHeap2(); return 0; }