目次
1:木とは
[1]木の概念
前に学習したシーケンス テーブルとリンク リストは線形構造に属しますが、ツリーは非線形データ構造であり、 n (n>=0) 個の有限ノードで構成される階層関係のセットです。根が上を向き、葉が下を向いている逆さまの木のように見えることから、ツリーと呼ばれます。
図:
(1) ツリーには、ルート ノードと呼ばれる特別なノードがあり、図のノード A です。
(2)ルート ノードを除いて、他のノードは M (M>0) 個の互いに素な集合T1 、T2 、... 、Tmに分割され、各集合 Ti (1<= i <= m) は A 部分木であり、その構造は木に似ています。各サブツリーのルート ノードには、先行ノードが 1 つだけあり、0 個以上の後続ノードを持つことができます。
(3) 親子ノード: たとえば、A は B、C、および D の親ノードであり、E および F はBの子ノードです。
(4) 葉ノード: E、F、C、G、 Hなどの子ノードを持たないノード。
注: ツリーのサブツリーはばらばらであり、ルート ノードを除く他のノードには 1 つの親ノードしかありません。
[2] 木のその他の重要な概念
図:
(1) ノードの次数:ノードに含まれるサブツリーの数をノードの次数と呼び、上図のようにAは6(2) 非終端ノードまたは分岐ノード:次数が 0 ではないノード;上図に示すように: D 、E 、F 、Gなどのノードは分岐ノードです。(3) 兄弟ノード:同じ親ノードを持つノードは兄弟ノードと呼ばれます; 上図に示すように: BとCは兄弟ノードです(4) 木の次数: 木の中で最大のノードの次数を木の次数と呼ぶ; 上図のように: 木の次数は6(5) ノードの階層:ルートの定義から始めて、ルートは最初のレイヤー、ルートの子ノードは 2 番目のレイヤーなどです。(6) ツリーの高さまたは深さ:ツリー内のノードの最大レベル; 上の図に示すように: ツリーの高さは4(7) ノードの祖先:ルートからノードのブランチ上のすべてのノードまで; 上図に示すように: Aはすべてのノードの祖先です(8) 子と孫:特定のノードをルートとするサブツリー内のノードは、そのノードの子孫と呼ばれます。上記のように、すべてのノードはAの子孫です(9) 森: m ( m>0)個のバラバラの木の集まりを森と呼びます。
[3]木のいくつかの表現方法
(1)シーケンステーブルを使用して子ノードのアドレスを格納する(複雑な構造)(2)木の次数(最大ノードの次数)を説明し、子ノードのアドレスを格納するポインタ配列を設定
(3) 構造体配列の格納
(4)左子右兄弟の表記(一般的で、構造が比較的単純で論理が比較的明快)
2: 二分木とは
【1】コンセプトと特徴
コンセプト:
バイナリ ツリーはノードの有限セットであり、空であるか、ルート ノードと、左右のサブツリーと呼ばれる 2 つのバイナリ ツリーで構成されます。
特徴:
1. 各ノードには最大で 2 つのサブツリーがあります。つまり、二分木には次数が 2 を超えるノードはありません。
2. 二分木の部分木は左右に分かれており、部分木の順序を逆にすることはできません。
[2] 2 つの特別な二分木
1. 完全二分木:
すべての葉ノードは最後の層にあります
すべての分岐ノードには 2 つの子があります
2. 完全な二分木:
このバイナリ ツリーに N 層があると仮定すると、最初の N-1 層がいっぱいになる必要があります。
最後のレイヤーはいっぱいにできますが、左から右に連続している必要があります
[3] 二分木の性質
1. ルート ノードのレイヤー数が 1 に指定されている場合、空でないバイナリ ツリーの i 番目のレイヤーには最大 2^(i-1) 個のノードがあります。
2. ルート ノードの層数が 1 として指定されている場合、深さ h の二分木のノードの最大数は 2^h-1 です。
3. 任意の二分木の場合、次数が 0、葉ノードの数が n0、次数 2 の枝ノードの数が n2の場合、n0=n2+1 となります。
4. ルート ノードの層数を 1 と指定すると、ノードが n 個の完全な二分木の深さ h=LogN となります。
[4] 二分木の2つの保存方法
(1) 順次構造体格納(配列)
順次構造ストレージは、ストレージに配列を使用することです. 一般に、配列は完全なバイナリ ツリーを表現する場合にのみ適しています. 完全なバイナリ ツリーはスペースを無駄にするからです. 実際には、ストレージに配列を使用するのはヒープだけです。二分木順次記憶域は、物理的には配列であり、論理的には二分木です。(2) チェーン構造収納
リンク リストは、バイナリ ツリーを表すために使用されます。つまり、要素の論理関係を示すためにチェーンが使用されます。
通常の方法では、連結リストの各ノードは、データ フィールドと左右のポインター フィールドの 3 つのフィールドで構成され、左右のポインターを使用して、左側の子とノードの右の子が配置されます。
チェーン構造は、さらにバイナリ チェーンとトリプル チェーンに分けられます.現在、バイナリ チェーンが一般的に使用されており、赤黒木などの高レベルのデータ構造はトリプル チェーンを使用します。
3: ヒープの実装
ヒープは、配列で実装されたバイナリ ツリーであり、通常、完全なバイナリ ツリーを実装するために使用されます。
ヒープは大きなヒープと小さなヒープに分けられます
大きな山: 父 >= 子供
小さな山: 子供 >= 父親
このホワイト ペーパーでは、多くの機能を実装しています。
ヒープの実装はシーケンス テーブルに似ているため、ここでは詳細には触れず、メイン インターフェイスの実装についてのみ説明します。
シーケンス テーブルのリンクを添付します: https://blog.csdn.net/2301_76269963/article/details/129352041?spm=1001.2014.3001.5501
【1】データ挿入
(1) 判定展開が系列表と一致している。
(2) 配列表と整合性のとれたデータを格納する。
(3) データ挿入後、挿入後も大きな山が残っていることを確認する必要があるため、親子関係を調整する必要があります。
調整を考える前に、父親の添字と子の添字の関係を見てみましょう
このようなルールを見つけることができます
親の添字 = (子の添字 - 1)/2。
このルールに従って調整関数を設計し、挿入するデータが親よりも大きい場合は、親よりも小さくなるか、ルート ノードになるまで2 つを置き換えます。
その後の削除でもスワップ調整が必要になるため、スワップを関数 HeapSwap( ) に個別にパッケージ化できます。
コード:
[2] データの削除(ヒープソートの基本)
基本的な考え方:
(1) ヒープのデータ削除には、ルート データの削除が必要です。
(2)シーケンステーブルのように直接上書きすることはできず、元のヒープの構造が破壊されることに注意してください。
図:
(3)元のルートデータを削除して調整する必要があります。
削除: ルートを最後のリーフと交換し、サイズ (有効なデータの数) を直接 1 減らすことができます。
図:
調整: 添字 0から下向きに調整.子が父親よりも大きい場合,両方の子よりも大きいか葉が調整されるまで交換.各判定の前に,より大きい 2 つの子を比較して防止するための構造を破壊する.大きなヒープ、古い子を親と交換し、繰り返します。
図:
コード:
完全なコード:
Heap.h (必要なヘッダー ファイルのインクルード、関数および構造の宣言)
#pragma once #include <stdio.h> #include <assert.h> #include <stdlib.h> #include <stdbool.h> typedef int HPDataType; typedef struct Heap { //存储数据 HPDataType* a; //有效数据个数 int size; //容量 int capacity; }HP; //初始化 void HeapInit(HP* hp); //销毁 void HeapDestory(HP* hp); //交换函数 void HeapSwap(int* p1, int* p2); //判空函数 bool HeapEmpty(HP* hp); //调整函数 void AdjustUp(HPDataType* a,int child); //向下调整,n是有效个数 void AdjustDown(HPDataType* a, int n, int parent); //插入数据 void HeapPush(HP* hp, HPDataType x); //打印数据 void HeapPrint(HP* hp); //删除数据 void HeapPop(HP* hp);
Heap.c (インターフェースの実装)
#define _CRT_SECURE_NO_WARNINGS 1 #include "Heap.h" //初始化 void HeapInit(HP* hp) { //断言,不能传空的结构体指针 assert(hp); hp->a = NULL; //初始化size和容量都为0 hp->size = hp->capacity = 0; } //销毁 void HeapDestory(HP* hp) { free(hp->a); hp->size = hp->capacity = 0; } //交换函数 void HeapSwap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } //判空函数 bool HeapEmpty(HP* hp) { return hp->size == 0; } //调整函数 //向上调整 void AdjustUp(HPDataType* a, int child) { //断言,不能传空指针 assert(a); //找到父结点的下标 int parent = (child - 1) / 2; //循环,以child到树根为结束条件 while (child > 0) { //如果父结点比child下,交换并更新 if (a[child] > a[parent]) { HeapSwap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } //如果父结点比child大,跳出循环 else { break; } } } //向下调整 void AdjustDown(HPDataType* a, int n, int parent) { //默认左孩子最大 int child = parent * 2 + 1; //当已经调整到超出数组时结束 while (child<n) { //找出两个孩子中大的一方 //考虑右孩子不存在的情况 if (child+1<n&&a[child + 1] > a[child]) { //如果右孩子大,child加1变成右孩子 child++; } //如果父亲比大孩子小,进行调整,否则跳出 if (a[child] > a[parent]) { HeapSwap(&a[child], &a[parent]); //迭代 parent = child; child = parent * 2 + 1; } else { break; } } } //插入数据 void HeapPush(HP* hp, HPDataType x) { if (hp->size == hp->capacity) { //判断扩容多少 int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2; //扩容 HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity); //更新 hp->capacity = newcapacity; hp->a = tmp; } //存储数据 hp->a[hp->size] = x; hp->size++; //进行调整 AdjustUp(hp->a, hp->size-1); } //打印数据 void HeapPrint(HP* hp) { //断言,不能传空的结构体指针 assert(hp); int i = 0; for (i = 0; i < hp->size; i++) { printf("%d ", hp->a[i]); } printf("\n"); } //删除数据 void HeapPop(HP* hp) { //断言,不能传空的结构体指针 assert(hp); //如果为空,不能删除,避免数组越界 assert(!HeapEmpty(hp)); //不为空,先交换根和最后一片叶子,然后size减1 HeapSwap(&hp->a[0], &hp->a[hp->size - 1]); hp->size--; AdjustDown(hp->a, hp->size, 0); }
text.c (テスト)
#define _CRT_SECURE_NO_WARNINGS 1 #include "Heap.h" void text1() { HP hp; HeapInit(&hp); HPDataType a[] = { 70,30,56,25,15,10.85,79}; int i = 0; for (i = 0; i < sizeof(a) / sizeof(a[0]); i++) { HeapPush(&hp, a[i]); } HeapPrint(&hp); HeapPop(&hp); HeapPrint(&hp); HeapDestory(&hp); } int main() { text1(); }