データ構造 - バイナリ ツリー (1)

1. ツリーの概念と構造

1.1 ツリーの概念

ツリーは非線形データ構造であり、n (n>=0) 個の有限ノードで構成されます。階層関係のあるコレクション。逆さまの木、つまり根が上を向き、葉が下を向いているように見えるので、木と呼ばれます。

1. ルート ノードと呼ばれる特別なノードがあります。ルート ノードには先行ノードがありません
2. ルート ノードを除く残りのノードは、M (M>0) 個の素集合 T1、T2、...、Tm に分割され、それぞれが Ti (1<= i<= m) は、ツリーと同様の構造を持つ別のサブツリーです。 各サブツリーのルート ノードには先行ノードが 1 つだけあり、0 個以上の後続ノードを持つことができます
3. したがって、ツリーは再帰的に定義されます。

 注: ツリー構造では、サブツリー間に交差があってはなりません。交差しないと、ツリー構造になりません。

1.2 ツリーの関連概念


ノードの次数: ノードに含まれるサブツリーの数をノードの次数と呼びます。上の図に示すように、A は 6 です。< a i=2 >リーフ ノードまたはターミナル ノード: 次数 0 のノードはリーフ ノードと呼ばれます; 上の図に示すように: B、C、H、I... などのノード< a i=4>非終端ノードまたは分岐ノード: 0 以外の次数を持つノード。上の図に示すように: D などのノード、E、F、G... はブランチ ノードです。親ノードまたは親ノード: ノードに子ノードが含まれる場合、このノードは親と呼ばれます。上の図に示すように、A は B の親ノードです。: m (m>0) の互いに素なツリーの集合はフォレストと呼ばれます。 フォレスト: 特定のノードをルートとするサブツリー内のノードはすべて、このノードの子孫と呼ばれます。上の図に示すように、すべてのノードは A の子孫です。子孫: ルートからノードへのパス ブランチ上のすべてのノード。上の図に示すように: A はすべてのノードの祖先です。ノードの祖先: 親ノード同じレイヤー上の は互いにいとこです。上の図に示すように、H と I は互いに兄弟ノードです。いとこノード: ツリー内のノードの最大レベル。上に示すように: ツリーの高さは 4 です。ツリーの高さまたは深さ: ルートの定義から開始すると、ルートは第 1 レベル、ルートの子ノードは第 2 レベル、というようになります。ノード : ツリーでは、最大のノードの次数はツリーの次数と呼ばれます。上の図に示すように、ツリーの次数は 6 です。ツリーの次数:同じ親ノードを持つノードは兄弟ノードと呼ばれます。上に示すように、B と C は兄弟ノードです。兄弟ノード: のルート ノードノードに含まれるサブツリーは、ノードの子ノードと呼ばれます。上の図に示すように、B は A の子ノードです。子ノードまたは子ノード











1.3 木の表現

木構造は線形テーブルに比べて複雑で、それを保存したり表現したりするのが面倒です 値の範囲を保存するので、ノードとノード間の関係も保存する必要があります 実際には、さまざまな方法があります親表現、子表現、子親表現、子兄弟表現などのツリーを表現します。ここでは、子供の兄弟の最も一般的に使用される表現を簡単に理解します。

typedef int DataType;
struct Node
{
   struct Node* _firstChild1; // 第一个孩子结点
   struct Node* _pNextBrother; // 指向其下一个兄弟结点
   DataType _data; // 结点中的数据域
};


2. 二分木の概念と構造

2.1 コンセプト

バイナリ ツリーはノードの有限セットです。セットは次のとおりです:
1. または空
2. ルート ノードによって追加されます。2 つのノードで構成されます。バイナリ ツリー。 左サブツリーおよび右サブツリーとも呼ばれます。

 上の写真からわかるように:

1. バイナリ ツリーには次数が 2 より大きいノードはありません
2. バイナリ ツリーのサブツリーは左と右のサブツリーに分割できますが、順序は区別できません。逆になるので、二分木は順序付けられた木になります


: バイナリ ツリーは次の状況で構成されます。

2.2 特殊なバイナリ ツリー:

1. 完全なバイナリ ツリー: バイナリ ツリー。各レイヤのノード数が最大値に達すると、バイナリ ツリーは完全なバイナリ ツリー。つまり、バイナリ ツリーのレベル数が K で、ノードの総数が である場合、それは完全なバイナリ ツリーです。
2. 完全なバイナリ ツリー: 完全なバイナリ ツリーは非常に効率的なデータ構造です。完全なバイナリ ツリーが導出されます。完全なバイナリ ツリーから。深さ K でノードが n のバイナリ ツリーの場合、各ノードが深さ K の完全なバイナリ ツリー内の 1 から n までの番号が付けられたノードと 1 対 1 で対応する場合にのみ、完全なバイナリ ツリーと呼ばれます。完全なバイナリ ツリーは特別な種類の完全なバイナリ ツリーであることに注意してください。

 

2.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= log2(n+1) です。(ps: log2(n+1) は底 2 と n の対数です)対数として +1)。< a i=10> 5. n 個のノードを持つ完全なバイナリ ツリーの場合、すべてのノードが配列内で上から下、左から右の順序で 0 から始まる番号が付けられている場合、シリアル番号 i のノード:

1. i>0 の場合、位置 i のノードの親番号: (i-1)/2; i=0、i はルート ノード番号であり、親ノードは存在しません
2 . 2i+1
3. 2i+2

2.4 バイナリツリーの記憶構造

バイナリ ツリーは通常、シーケンシャル構造とチェーン構造の 2 つの構造を使用して保存できます。
1. シーケンシャル ストレージ
シーケンシャル構造ストレージでは、ストレージに配列が使用されます。一般に、配列は完全なバイナリ ツリーではないため、完全なバイナリ ツリーを表す場合にのみ適しています。 、スペースの無駄が生じます。実際には、ストレージに配列を使用するのはヒープだけですが、ヒープについては次の章で具体的に説明します。バイナリ ツリーの順次ストレージは、物理的には配列であり、論理的にはバイナリ ツリーです。

2. リンク ストレージ
バイナリ ツリーのリンク ストレージ構造は、リンク リストを使用してバイナリ ツリーを表すこと、つまり、チェーンを使用して論理的なものを示すことを意味します。要素の関係。通常の方法では、リンク リストの各ノードは、データ フィールドと左右のポインタ フィールドの 3 つのフィールドで構成されます。左ポインタと右ポインタは、左の子と右の子がポイントするリンクのストレージ アドレスを与えるために使用されます。ノードのそれぞれが配置されます。連鎖構造は二分鎖と三分岐鎖に分けられ、現在は二分鎖を中心に学習していますが、後の講座では赤黒木や三分岐鎖などの高次データ構造について学習していきます。


3. 二分木の逐次構造と実装

3.1 二分木の逐次構造

通常のバイナリ ツリーは、無駄な領域が多くなる可能性があるため、配列での保存には適していません。完全なバイナリ ツリーは、順次構造の保存に適しています。 実際には、通常、シーケンシャル構造の配列を使用してヒープ (バイナリ ツリー) を保存します。ここでのヒープとオペレーティング システムの仮想プロセス アドレス空間内のヒープは 2 つであることに注意してください。 1 つはデータ構造、もう 1 つはオペレーティング システムのメモリを管理する領域の分割です。

3.2 ヒープの概念と構造

ヒープのプロパティ:
ヒープ内のノードの値は、常にその親ノードの値よりも大きくも小さくもありません。
ヒープは常に完全なバイナリ ツリーです。

3.3 ヒープの実装

3.3.1 ヒープの定義

配列はヒープの一番下に定義できます。

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

3.3.2 ヒープの初期化

void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

3.3.3 ヒープの破壊

void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

3.3.4 データの挿入

まず、スペースがいっぱいかどうかを判断し、いっぱいの場合は拡張します。次に、三項演算子を使用して最初の拡張かどうかを判断し、対応する拡張を実行してから、realloc の特性を使用して容量を拡張または調整します。 size の位置にデータを挿入し、次に size++ を挿入します。しかし、今はヒープではないので、上方調整を使用して調整する必要があります。

void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	// 扩容
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);
}

3.3.5 データを上方調整する

このような小さなヒープ データのセットがあり、20 が挿入されて上方に調整されたとします。

 最初のステップは、添字parent=(child-1)/2 を通じて親ノードを見つけ、親ノードかどうかを判断することです。親ノードがこの子ノードより小さい場合、調整は必要ありませんが、それ以外の場合はスワップが必要です。交換後は、添え字 child=parent となり、下の図では 10 が 4 に置き換えられます。次に、親の添字を再計算します。この時点では親はまだ 4 なので、parent=(parent-1)/2 によって父の添字が計算され、父ノードが息子ノードより小さくなるか、このノードが調整されるまで上向きに判断されます。ルートノードに。

 したがって、この関数は親ノードの計算から開始し、次に while ループに入りますが、ループの終了条件は child=0、つまりルート ノードの位置に調整されることを意味します。ループに入るときは、まず息子ノードと父ノードのサイズを決定します。息子ノードが父ノードより小さい場合は交換を開始します。Swap 関数を使用して値を交換し、息子ノードの添字を調整します。息子ノードが父ノードよりも大きい場合、息子ノードもループから抜け出します。

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

3.3.6 印刷ヒープ

size-1 は最後のデータの添字であるため、左閉右開の for ループを作成するだけです。

void HeapPrint(HP* php)
{
	assert(php);

	for (size_t i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

3.3.7 データを下方調整する

配列表の末尾の削除と末尾の挿入の効率が良いため、末尾ノードと先頭ノードの位置を交換し、末尾ノードのサイズを削除することができます。

 データ 20 がヘッド ノードに配置されると、この小さなヒープを維持できなくなるため、下方に調整する必要があります。下方に調整することは、子ノード child=parent*2+1 を見つけて書き込むことと同じです。 while ループの終了条件は、child=n が範囲外であることです。この時点でもう 1 つ行うことは、左右の子のうち小さい方の子を見つけることです。if child+1

 

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child<n)
	{
		//找小的孩子
		if (child + 1 < n && a[child] > a[child + 1])
		{
			child = child + 1;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//继续往下调整
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

3.3.8 データの削除

まずルート ノードとテール ノード、サイズを交換し、次に下方に調整します。

void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0],&php->a[php->size - 1]);
	--php->size;
	AdjustDown(php->a, php->size, 0);
}

3.3.9 ルートノードデータの取得


HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

3.3.10 ヒープが空かどうかを確認する

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

今日の共有はここで終わります、読んでくださった皆様、ありがとうございました!

 

おすすめ

転載: blog.csdn.net/2301_79035870/article/details/134651857