データ構造二分木 (基本的な習得)


導入

        雄関路は本当に鉄のようで、今では最初から終わっています。この章ではバイナリ ツリーの学習を開始します (全文は合計 12,000 ワードです)。前のデータ構造と比較すると、バイナリ ツリーは非常に難しいものですが、これを徹底的に学習する限り、ブログを読めば基本的なバイナリツリーをマスターできます。

9df3a4b5d4034ec8a435fa1779c9911e.webp

 

 言うことはあまりありません。シートベルトを締めて、車をスタートさせましょう(コンピュータで見ることをお勧めします)


添付ファイル: 赤、その部分は重要な部分、青の色は暗記する必要がある部分 (丸暗記ではなく、もっとノックしてください)、黒の太字または他の色は副次的なキーポイント、黒は説明が必要な部分です。

目次

1. 木の概念

2.二分木

2.1 二分木の概念

2.2 二分木の逐次構造

2.2.1 ヒープ:

ヒープの実装(実装する関数):

上下調整の適用:

1. 直接ヒープ構築の上下調整方法:

2. ヒープサイズによる昇順と降順を実現

ヒープの TopK 問題:

2.3 二分木のチェーン構造

2.3.1 プレオーダー、インオーダー、ポストオーダー

2.3.1.1 preorder/postorder + inorderが与えられた場合、唯一の二分木が決定できる(試験共通試験会場)

2.3.2 バイナリ ツリー内のノードの数を確認します。

2.3.3 二分木の高さを求める

2.3.4 二分木で K 番目の層ノードを見つける

2.3.5 バイナリ ツリー内のノードの検索

高度な演習:

2.3.6 レイヤー順序のトラバーサル

演習: バイナリ ツリーが完全なバイナリ ツリーかどうかを判断します。


1. 木の概念

3dd109479d7745aaa0af73197b5b2d28.png

ツリーは非線形データ構造です。その構造が実際のツリーを反転したものに非常に似ているため、ツリーと呼ばれます。各データはノードと呼ばれます。ツリー内の最上位ノードはルート ノードとも呼ばます(ルート ノードには先行ノードがありません)、各ノードはツリー (またはサブツリー) と呼ばれます。各ツリーは複数のサブツリーで構成されます。各ノードには 1 つのみの先行ノードと、複数またはゼロの後続ノードがあります。ツリー内の各サブツリーは交差 (相互に素) を持つことはできません。n 個のノードを持つツリーには n-1 個の親ノードがあります (ツリー内の関係を説明するためによく使用されます)。

 

  • 親ノード(parent Node):ノードの先行ノード(上図AはBCDの親ノード)
  • 子ノード:ノードの後継ノード(Aの子ノードはBCDを持つ)
  • 次数: 各ノードが持つ子ノードの数 (A の次数は 3)
  • ツリーの次数: ツリー内のすべてのノードの最大次数 (上記のツリーは A の最大次数であるため、ツリーの次数は 3)
  • ノードのレベル: ルートから数えて、ルートのレベルは 1 で、その後は逆に進みます... (つまり、A のレベルは 1、j のレベルは 4)
  • 高さ(奥行き) : これも一般的には1から始まり、上の図の奥行き/高さは4になります(0から始まる場合もあるかもしれません)
  • 葉(子)ノード(終端ノード):次数が0のノード、または子ノードがないとみなせるノードが葉ノード(​​J、K、L、H、I、Fなど)
  • ブランチ ノード (非終端ノード): リーフ ノードではないノードは、ブランチ ノードまたは 0 以外の次数を持つノードとみなされます (上図のリーフ ノードを除く残りのノードはブランチ ノードと見なされます)。
  • 兄弟ノード: 同じ親ノードを持つノード (上記の BCD と同様、すべて兄弟ノードです)
  • カズンノード: 親ノードが同じレベルにあるノード (F と G など)
  • 祖先ノード: ルートからこのノードに渡されるすべてのノード
  • 子孫: そのノード以降のすべてのノードはそのノードの子孫と見なすことができます (上図のすべてのノードが A)
  • フォレスト: 複数の独立したツリー

アプリケーション: ディレクトリ ツリー (左子右兄弟構造で表されます)

2.二分木

2.1 二分木の概念

70f34b6453da456693321880c4cc616b.png

二分木は木の中の特殊な種類の木で、二分木の最大次数は 2 であり、このときの各ノードの子ノードを左子、右子(左サブツリー、右サブツリー)と呼びます。

 

958544b82b7243048ab22f972b7c9950.png

  1. 完全なバイナリ ツリー: リーフ ノードの次数が 0、他のノードの次数が 2 であることを除き、各層はフルです        
    1. (2 ^ h) - 1 ノードを持つ h レベルの完全なバイナリ ツリー
  2. 完全な二分木: 高さ h の完全な二分木の場合、最初の h-1 層はいっぱいで、最後の層もいっぱいになる可能性がありますが、最後の層は左から右に連続している必要があります。
    1. h 層の完全な二分木には 2 ^ ( h - 1 ) ~ ( 2 ^ h ) - 1 が含まれます。
  3. バイナリ ツリーでは、h 番目の層のノードの最大数は次のとおりです: 2 ^ (h - 1) (バイナリ ツリーがいっぱいの場合、それはそのままそれに等しくなります)
  4. h 層のバイナリ ツリーの最大ノード数は ( 2 ^ h ) - 1 です。
  5. 空ではない二分木の場合、次数 0 のノード n0 = 次数 2 のノード n2 + 1: n0 = n2 + 1
  6. 完全な二分木では、次数 1 のノードは 1 / 0 のみです。ツリー内のノードの数が偶数の場合、次数 1 のノードの数は 1 で、奇数の場合、次数 1 のノードの数は 1 になります。 0です

2.2 二分木の逐次構造

二分木の構造は順序構造とリンクリスト構造で実現できますが、一般的な二分木の場合、順序構造で格納するとノードのない箇所が多く無駄になるため、不向きです。完全なバイナリ ツリーはシーケンシャル構造に非常に適しています。実際には、通常、シーケンシャル構造の配列にヒープ (バイナリ ツリー) を格納します。

2.2.1 ヒープ:

52f157a4988849248390b10a9fac02a5.png

  1. 完全な二分木です
  2. 大きなヒープ: ツリー内のすべての親ノードが子ノードよりも大きい
  3. 小さなヒープ: ツリー内のすべての親ノードは子ノードより小さい
  4. 親ノードと子ノードの関係
    1. 親ノードから左の子へ: leftchild = 親 * 2 + 1
    2. 親ノードから左の子へ: leftchild = 親 * 2 + 2
    3. 左右の子から親ノードへ: 親 = (子 - 1) / 2
  5. ヒープ構造については、その論理構造と物理構造を理解する必要があります
    1. 論理構造の観点から見ると、彼は完全な二分木です
    2. 物理構造的には、最下層は配列に格納されます。
    3. この完全なバイナリ ツリーを記述するには、彼の基礎となる配列を覚えておく必要があります。

ヒープの実装(実装する関数):

  1. 初期化(主にシーケンシャル構造に必要な領域)
  2. 破壊する
  3. データを挿入する
    1. ヒープアップスケーリングアルゴリズム
      1. おおよそのアルゴリズム原理:与えられた子の位置と親ノードの位置を比較し、大きいか小さい場合交換(大きいヒープ/小さいヒープ) ループ判定 子がヒープの先頭に到達したら停止
      2. 時間計算量は次のとおりです: O(N*logN)   
  4. データを削除する
    1. ヒープ調整アルゴリズム
      1. おおよそのアルゴリズム原理: 親ノードとツリーの数を指定して、親ノードと子ノードを比較させ、大きい/小さい場合は交換(小さいヒープ/大きいヒープ)ループ判定を行い、子ノードが存在しない場合は停止します。
      2. 時間計算量は次のとおりです: O(N) 
  5. ヒープの先頭にあるデータを取得し、ヒープ内にデータがいくつあるかを確認し、ヒープが空であると判断する

具体的な内容については、実装時の詳細なメモを記載します(メモは今後の作業において非常に重要なので、習慣として書くことができます)

 #define _CRT_SECURE_NO_WARNINGS 1

#include"Heap.h"


void If_Add_Capacity(HP* php)
{
	if (php->_size == php->_capacity)//判断已有成员个数是否等于容量,若等则进去
	{
		HPDataType* ptr = (HPDataType*)realloc(php->_a, sizeof(HPDataType) * php->_capacity * 2);//进来后就说明空间不够了,需要开空间
		//一般多直接开辟比容量大两倍的空间 即 对a开辟结构体大小为原capacity两倍的空间
		if (ptr == NULL)
		{
			perror("realloc");

			return;
		}
		php->_a = ptr;//因为可能是异地扩容所以还要将ptr赋值给数组a
		php->_capacity *= 2;//容量 乘于 2
		ptr = NULL;//无用的指针置为NULL(好习惯)
	}
}

//对于堆的初始化和销毁就不过多赘述了相信通过我前面的几篇文章已经能很好的了解其原理了!
void HeapInit(HP* php)
{
	assert(php);

	php->_capacity = 4;
	php->_a = (HPDataType*)malloc(sizeof(HPDataType)* (php->_capacity));
	if (php->_a == NULL)
	{
		perror("php::malloc");
		return;
	}
	php->_size = 0;

}
void HeapDestroy(HP* php)
{
	assert(php);
	free(php->_a);
	php->_capacity = php->_size = 0;
	php->_a = NULL;

}

//----------------------------------------------------------------
void swap(HPDataType* t1, HPDataType* t2)
{
	HPDataType tmp = *t1;
	*t1 = *t2;
	*t2 = tmp;
}
//对于向上调整来说,他能形成大堆/小堆
//此处我们先实现一个小堆
// 
//小堆:树中任意一个位置的父节点都要比子节点小
//父子节点的关系: leftchild = parent * 2 + 1 、rightchild = parent * 2 + 2 、parent = (child - 1)/ 2 
void AdjustUp(HPDataType* a, int child) {
	
	while (child > 0)//循环来进行调整,从数组最后一直要调整到堆顶,顶部时child为0 所以条件是要大于0
	{
		int parent = (child - 1) / 2;//找到父节点
		if (a[parent] > a[child])//判断自己是否小于父节点
		{
			swap(&a[parent], &a[child]);//若小于则进行交换
		}
		else {
			break;//反之只要不小于就退出
		}
		child = parent;//修改子节点的下标,让其不断往上走
	}
}

//对于堆的插入我们要知道的是其实他是把数据插入到了一个数组中
//但是要注意的是,如果需要实现一个堆的话 , 那就必须是大堆 / 小堆
//所以我们不仅仅只是把数据插入数组中,而且还需要对数组中的数据进行排序
//通过排序后让他变成大、小堆
//此处就需要用到  向上调整算法
//向上调整算法的前提是在调整的树(除去刚刚插入的数据外)已经满足大堆/小堆
//而此处的数据是一个个插入的(向上调整后就形成了大堆/小堆)所以就能很好的满足这个前提条件
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	If_Add_Capacity(php);//判断容量是不是满了
	
		//首先将数据插入顺序表的尾部
	php->_a[php->_size] = x;
	AdjustUp(php->_a, php->_size);//就行向上排序让新插入的数据也成功的融入到这个堆中
	php->_size++;//一定不要忘记要增加一下size

}

//--------------------------------------------------------------------
//向下调整成小堆
//向下调整的原理和向上调整差不多
//只不过反了过来

void AdjustDown(int* a, int n, int parent)
{
	//找到小的那个孩子
	//建小堆的话需要父节点小于子节点
	//为什么要找小的孩子呢,因为我们要找到子节点中小的那个孩子,才能避免大的孩子如果大于父节点而小的孩子却小于父节点的情况

	//此处用了一种特殊的方法
	//先将左孩子看成小的,再判断如果左孩子小于右孩子的话再改变child即可
	int child = parent * 2 + 1;
	while (child < n)//要判断一下孩子节点是否在size范围内
	{

		if (child+1 < n && a[child + 1] < a[child])//细节判断一下child+1这个节点是否存在
		{
			child++;
		}

		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}	
		else{
			break;
		}
	}
}
//堆的删除数据是将堆顶的数据删除
//而这个删除并不是像顺序表一样的进行覆盖,而是
//先将堆顶的数据和最后的数据进行交换
//交换后size--,这样就表示成把数据删除了,因为访问时是在size范围内进行的
//然后对交换到堆顶的数据进行向下调整(让他保持还原成一个堆,满足堆的条件)
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	swap(&php->_a[0], &php->_a[php->_size - 1]);
	php->_size--;
	AdjustDown(php->_a, php->_size ,0);
}

HPDataType HeapTop(HP* php){
	assert(php);
	assert(!HeapEmpty(php));
	return php->_a[0];
}
bool HeapEmpty(HP* php){

	assert(php);

	return php->_size == 0;
}
int HeapSize(HP* php){
	assert(php);

	return php->_size;
}

上下調整の適用:

1. 直接ヒープ構築の上下調整方法:

上方調整: ヒープ内で実行されます。この前提条件が無視されるように配列の 2 番目のデータから調整を開始します (上向きに見ると単一ノードがヒープとみなせるため)

 

下方調整: 左右のノードがヒープ内に作成され、最後の葉ノードの親ノードから調整を開始できるため、前提条件を考慮する必要がなくなります (単一のノードをヒープとみなすことができるため)下を向いたとき)

 

このように、配列を与えて大小のヒープにすると、ループを上下に調整することでヒープを構築できます。

詳細は次のとおりです。

小さなヒープを構築する:(大きなヒープを構築したい場合は、上下調整機能でサイズと関係を変更して大きなヒープを構築できます)

void HeapSort(int* a, int n)
{
	//原理和在堆中插入数据差不多
	//向上调整建小堆
	//从第二个数据开始然后不断往后
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}

	//向下调整建堆
	//从最后的叶子节点的开始然后不断往前
	for (int i = (n - 2) / 2; i >= 0; i--) // 最后一个叶子节点 n - 1 的父节点(( n - 1 ) - 1 )/ 2 
	{
		AdjustDown(a,n,i);
	}
}

2.ヒープサイズによる昇順と降順を実現

大きなヒープを昇順で構築し、小さなヒープを降順で構築します

大きな山を構築する場合、親ノードの w ポイントは子ノードよりも大きくなければならないため、この時点ではルート ノードも最大のノードになります。ルート ノードと最後のノードを交換して、最大のノードが得られるようにすることができます。が送信され、次にサイズ -- このノードを最初に置きます。除外してから下方に調整するとヒープが復元され、最後に上記のプロセスをループして最終的に昇順を達成します。

Xiaoduiのやり方も同じなので詳しくは割愛します。コードに目を通す

//通过向下调整建小堆
for (int i = (n - 2) / 2; i >= 0; i--) // 最后一个叶子节点 n - 1 的父节点(( n - 1 ) - 1 )/ 2 
{
		AdjustDown(a,n,i);
}

int end = n - 1;//找到最后一个节点
while (end > 0)
{
	swap(&a[0], &a[end]);//将最后的节点和堆顶元素交换下

	AdjustDown(a, end, 0);//再还原一下堆

	--end;//改变尾
}

ここでの時間計算量は次のとおりです: O(N + N*logN)==O(N*logN)

ヒープの TopK 問題:

ヒープから最大/最小の上位 K を取得し、最大の上位 K を見つけるために大きなヒープを構築し、最小の上位 K を見つけるために小さなヒープを構築します。

方法 1: 大きいヒープで k 回ポップして最大の上位 K を見つけ、小さいヒープで k 回ポップして最小の上位 K を見つける

ただし、この方法にはいくつかの欠点があります。最初にヒープを構築する必要があり、データ量が非常に多い場合は多くのスペースが必要になり、メモリ不足につながります (K が比較的小さい場合に当てはまります)。

方法 2:上位 K 個の最大のものを見つけたい場合は、これらのデータを保存するための K サイズの小さなヒープを作成し (すべてのデータがスペースを圧迫するのを避けるため)、それらが見つかったときに 1 つずつ比較させます。ヒープの最上部より大きい ヒープに入って小さなヒープを構築できる理由: 最大の最上位 K を探しているため、小さなヒープのルート ノードは最小のデータです。この方法でのみ、探している大きなデータ (大きなヒープのルート ノードが最大です)

成し遂げる:

void TopK(int K)
{
	FILE* p = fopen("TopK.txt", "r");
	if (p == NULL)
	{
		perror("p");
		return;
	}

	//建K大小的小堆
	HPDataType* heap_arr = (HPDataType*)malloc(sizeof(HPDataType) * K);
	if (heap_arr == NULL)
	{
		perror("heap_arr::malloc");
		return;
	}
	int i = 0;
	for ( i = 0;i < K; i++)
	{
		fscanf(p, "%d", &heap_arr[i]);
	}
	
	for (int i = (K - 2) / 2; i >= 0; i--) // 最后一个叶子节点 n - 1 的父节点(( n - 1 ) - 1 )/ 2 
	{
		AdjustDown(heap_arr, K, i);//对数组进行下下调整建堆
	}
	
	//查看剩下的数据
	HPDataType t = 0;
	while(!feof(p))//fscanf将文件中的数据读完后会设置feof的置为非0的值,故对feof取反
	{
		 fscanf(p, "%d", &t);

		if (heap_arr[0] < t)//查看文件中的值是否大于堆顶的数据
		{
			heap_arr[0] = t;//大于的话这进堆
			AdjustDown(heap_arr, K, 0);//然后向下调整,将堆中最小的放到堆顶
		}
	}

	//打印堆的数据
	for (i = 0; i < K; i++)
	{
		printf("%d\n", heap_arr[i]);
	}

	fclose(p);

}

void testtopk()
{
	//生成数据放进文件中
	FILE* p = fopen("TopK.txt", "w"); 
	srand((unsigned int)time(0));
	//生成一百万个随机数据放进文件中
	for (int i = 1; i <= 1000000; i++)
	{
		int r = rand() % 10000;
		fprintf(p, "%d\n", r);
	}
	fclose(p);

	TopK(10);//从小堆中找最大的前k(10)个
}

2.3 二分木のチェーン構造

27bfb1bea97145f38da7131e7015654c.png連鎖二分木の構造: int val、int* right、int* left。左右のサブツリーが NULL の場合、それは最後に到達したことを意味します。つまり、連鎖二分木の場合、主に左右のサブツリーを確認してください。

2.3.1 プレオーダー、インオーダー、ポストオーダー

事前順序: ルート、左サブツリー、右サブツリー、順序: 左サブツリー、ルート、右サブツリー、後置順序: 左サブツリー、右サブツリー、ルート

ここで、ルートとはルートノードのデータにアクセスすることを意味し、左サブツリーと右サブツリーは構造を通じて左サブツリーと右サブツリーのノードにアクセスすることを意味します。

 

上図に示すように、左右のサブツリーが NULL の場合、戻り値が開始されます。

前中間順序と後順序はすべて再帰に基づいて実行されます

d7e7550b15204f4fb2b4a37d712cb957.png

  1. 事前注文トラバーサルは次のとおりです: 1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NN 
    1. 分析: 事前順序トラバーサルでは、最初に到達したノードのデータを出力し、次に左右のサブツリーに移動し、NULL に遭遇した場合に戻ります。
  2. 順序トラバーサルは次のとおりです: N 3 N 2 N 1 N 5 N 4 N 6 N
    1. 分析: 順序トラバーサルでは、左側のサブツリーに移動して NULL に遭遇すると戻り、到達したノードのデータを出力し、次に右側のサブツリーに移動して NULL に遭遇すると戻ります。
  3. 事後走査は次のとおりです: NN 3 N 2 NN 5 NN 6 4 1
    1. 分析: 事後トラバーサルでは、左側のサブツリーに移動して NULL に遭遇したら戻り、次に右側のサブツリーに移動して NULL に遭遇したら戻り、到達したノードのデータを出力します。 

コードで実装:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType _data;
	struct BinaryTreeNode* _left;
	struct BinaryTreeNode* _right;
}BTNode;

BTNode* BuyNode(int x)
{
	BTNode* t = (BTNode*)malloc(sizeof(BTNode));
	if (t == NULL)
	{
		perror("malloc");
		return NULL;
	}
	t->_data = x;
	t->_left = NULL;
	t->_right = NULL;

	return t;
}

void Preorder(BTNode* node)
{
	if (node == NULL)
	{
		printf("N ");
		return;
	}

	printf("%d ", node->_data);
	Preorder(node->_left);
	Preorder(node->_right);
}

void Inorder(BTNode* node)
{
	if (node == NULL)
	{
		printf("N ");
		return;
	}

	Inorder(node->_left);
	printf("%d ", node->_data);
	Inorder(node->_right);
}

void Postorder(BTNode* node)
{
	if (node == NULL)
	{
		printf("N ");
		return;
	}

	Postorder(node->_left);
	Postorder(node->_right);
	printf("%d ", node->_data);
}



BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);
	node1->_left = node2;
	node1->_right = node4;
	node2->_left = node3;
	node4->_left = node5;
	node4->_right = node6;

	return node1;
}

int main()
{
	Preorder(CreatBinaryTree());
	printf("\n");
	Inorder(CreatBinaryTree());	
	printf("\n");
	Postorder(CreatBinaryTree());
		return 0;
}

再帰展開図 (原理を注意深く理解するにはこれを通して):

プロローグ:

8be6152944d44bfbb147012a37f47ca1.png

 途中シーケンスと後シーケンスは描かないので、初心者は必ず描くことをお勧めします

2.3.1.1 preorder/postorder + inorderが与えられた場合、唯一の二分木が決定できる(試験共通試験会場)

また、preorder + postorder が与えられた場合、必ずしも一意の二分木を決定できるとは限りません。

例: 予約注文: EFHIGJK、予約注文: HFIEJKG

 

事前順序はルート、左、右であり、順序は左、ルート、右であるためです。

  1. 次に、preorder が最初にルートを判断し、inorder が左右のサブツリーを判断できます。
  2. 現時点では、予約注文は最初にEです
  3. このとき、順序 E の左側のサブツリーは HFI で、E の右側のサブツリーは JKG です。 
  4. 次に、事前順序を通してルートを調べます。F は中間順序 E の左側のサブツリーにあり、事前順序は F が最初であるため、左側のサブツリーのルートになります。
  5. 次に、F の左右の部分木を inorder: left: H; right: I で確認します(内部にノードが 1 つしかないため、再配置する必要がないので完了です)
  6. この時点で、ツリーの左側のサブツリーが完成しているため、左側 (EHFI) を除外し、引き続き右側のサブツリー (JKG) を調べます。
  7. 同じ方法: 事前順序で前から後ろにルートを確認し、中間の順序で左右のサブツリーを確認し、ノードがなくなるまで下に進み続けます。
  8. 右のサブツリー: プレオーダーの開始時に、G は意図的にサブツリーのルート ノードになり、順序のみの左側のノードを確認すると、それらはすべて左側にあり、プレオーダーの開始として J が表示されます。 J をルートとして、右側の順序 K を確認すると、K が右側のサブツリーになります。
  9. 最終的に得たもの:834d678e5710411c9f2701c5e5e25a84.png

 

例:后序:bdeca 、中序:badce

このとき、ポストシーケンスとプリシーケンスは少し異なり、後ろから前に見る必要があります

postorder: left、right、root; inorder が: left、root、right であるためです。

  1. ポストオーダーの後ろから前にある木の根は、
  2. 次に、左側のサブツリーに b のみが存在する順序を確認し、次に左側のサブツリーに b のみが存在し、次に右側のサブツリーを確認します (dce)
  3. 左右のサブツリーのノードを決定した後、最後から 2 番目のサブツリーを後ろから前に見てみると、c が右のサブツリーのルートであることがわかります (ここでは、それが右のサブツリーとは異なることに注意してください)前のシーケンスでは、最初に正しいツリーを参照する必要があります。ルートは c)
  4. 順序を見ると、d が右サブツリーの左ノード、e が右ノードであることがわかります。
  5. 最終的に得たもの:28129fcfd96942b0b86a147a62385db4.png

 

 

2.3.2 バイナリ ツリー内のノードの数を確認します。

方法1の原理は前中後と同じです。

int size = 0;//用全局变量来记录节点个数
void BTreeSize1(BTNode* node)
{

	if (node == NULL)//当遇到NULL则返回且不记录
		return;
	
	++size;
	BTreeSize1(node->_left);
	BTreeSize1(node->_right);
}

方法 2 の原則: 自分自身 + 左のツリーのすべてのノード + 右のツリーのすべてのノードを返すため、再帰が継続され、NULL に達すると 0 が返されます。

int BTreeSize2(BTNode* node)
{
	if (node == NULL)//到尾了就返回
		return 0;

	return 1 + BTreeSize2(node->_left) 
		     + BTreeSize2(node->_right);
}

dade3635c74442589f71aadfd6c38c7b.png

 初心者は、より明確に理解できるように、再帰的な図を描く必要があることをお勧めします。以下は描きません、分からなかったら絵を描いたりコメント欄に質問して頂ければ読みます!

2.3.3 二分木の高さを求める

アイデア: 再帰的なアイデア。各ノードはその左と右のサブツリーから返された値を記録し、左と右の子から返された値の方が大きいと判断し、最終的に大きい側を返します。

成し遂げる:

int BTreeHight(BTNode* node)
{
	if (node == NULL)
		return 0;

//记录左右子树返回的值
	int left = BTreeHight(node->_left);
	int right = BTreeHight(node->_right);

//返回大的一边,+1是为了算自身
	return left > right ? left + 1 : right + 1;
}

2.3.4 二分木で K 番目の層ノードを見つける

アイデア: 変数を使用して、K 番目のレイヤーに到達すると 1 を返し、それ以外の場合は 0 を返すことを記録します。

成し遂げる:

int BTreeLevelKSize(BTNode* node,int k)
{
	assert(k > 0);//防止K不符合实际

	if (node == NULL) {//同样的当遇NULL就要返回
		return 0;
	}

//到达第K层时,k为1
	if (k == 1) {
		return 1;
	}

//左右子树都要加上
	return BTreeLevelKSize(node->_left, k - 1) + BTreeLevelKSize(node->_right, k - 1);
}

2.3.5 バイナリ ツリー内のノードの検索

アイデア: 二分木の高さの確認は上記と同様です。最初に継続的に再帰し、次に再帰プロセス中にノードの値が見つけたい値と等しいかどうかを判断します。等しい場合は、ノードのアドレスを返します。それ以外の場合は、空の場合にのみ NULL を返し、最後に左右のサブツリーの戻りステータスを記録し、NULL の場合は使用されず、NULL でない場合はノード ポインタが存在することを意味します。戻ってきた

成し遂げる:

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{

//查看节点是否为NULL若为则返回
	if (root == NULL)
	{
		return NULL;
	}

//判断节点数据是否等于要查找的数据
	if (root->_data == x)
	{
		return root;
	}

//记录左右子树的返回
	BTNode* left = BinaryTreeFind(root->_left, x);
	BTNode* right = BinaryTreeFind(root->_right, x);


//若不为NULL则表示找到了节点返回节点指针即可
	if (left)
		return left;
//用了三目、原理一样
	return right != NULL ? right : NULL;
}

高度な演習:

対称二分木:

アイデア: トピックを分析して、ツリーの対称性を証明するには、次のことを確認します。 左のサブツリーの左のノードが右のサブツリーの右のノードに等しいかどうか、左のサブツリーの右のノードが等しいかどうかを判断するは右のサブツリーの左のノードに等しい。

 

次に、最初に解決すべき問題は、固有の思考を放棄して 1 つのノードのみに注目することですが、このとき、左右のサブツリーが等しいかどうかを同時にクエリできる関数を作成できます。

 

return _isSymmetric(Leftroot->left, Rightroot->right) && _isSymmetric(Leftroot->right, Rightroot->left); 左ツリー左子-右ツリー右子、左ツリー右子- - という再帰的なアイデアも考えられます。右の木 左の子

 

次に、再帰を制限するというアイデアがあります。

同時に、NULL に遭遇した場合、それは等しいことを意味し (前方が空の木と等しい)、その後は等しいことを意味し、true を返します。片側だけが NULL の場合は等しくないので、false を返します
。 、
値が等しいかどうかを判断します

 

最終コード:
 

//关键的如何可以同时在一棵树中往左右走
bool _isSymmetric(struct TreeNode* Leftroot  ,struct TreeNode* Rightroot){
    
    //两个都为NULL
    if(Leftroot == NULL &&  Rightroot == NULL){
        return true;
    }
    //其中一个为NULL
    if(Leftroot == NULL ||  Rightroot == NULL){
        return false;
    }

    if(Leftroot->val != Rightroot->val){
        return false;
    }

    return _isSymmetric(Leftroot->left,Rightroot->right )
        && _isSymmetric(Leftroot->right,Rightroot->left);
}

bool isSymmetric(struct TreeNode* root){
    return _isSymmetric(root->left , root->right);
}

2.3.6 レイヤー順序のトラバーサル

81349588eebb45ab83e21455dd63950d.png階層トラバーサルの方法:ノードをキューの形式でキューに格納し、ノードがキューから出るたびに自分の左右のノードを取り込むことで、バイナリ ツリーの階層トラバースを実現できます。

コード:

void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
//若为NULL就不进去了
	if(root)
		QueuePush(&q, root);
//判断条件队列不为NULL
	while (!QueueEmpty(&q))
	{
//记录在队列最前面的节点
		BTNode* front = QueueFront(&q);
//出队列
        QueuePop(&q);
//打印该节点的值
		printf("%d ", front->_data);

//front的左右节点不是NULL则将节点带入
		if (front->_left)
			QueuePush(&q, front->_left);
		if(front->_right)
			QueuePush(&q, front->_right);
	}
}

演習:バイナリ ツリーが完全なバイナリ ツリーかどうかを判断します。

数列の考え方で二分木を見る

完全な二分木の特徴: ノードは最後の層を除いて満たされており、左から右に連続しています。

これは、シーケンス キュー内のすべてのノードが継続的に格納され、NULL が挿入されないことを意味します。

bool BTreeCompare(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
		QueuePush(&q, root);

//把节点逐一放进队列中
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		
		//只要第一次NULL就退出
		if (front == NULL)
			break;

		QueuePush(&q, front->_left);
		QueuePush(&q, front->_right);
	}

//查看队列后面是否全部为NULL若不是就表示不是完全二叉树
//因为完全二叉树的节点是连续的
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
			return false;
	}
	return true;
}

ご質問がございましたら、ぜひご相談ください。

この記事が役に立ったと思われる場合は、「いいね!」をお願いします。

大量のデータ構造と詳細なコンテンツを継続的に更新することで、早期の注目を失うことはありません。

 

おすすめ

転載: blog.csdn.net/ZYK069/article/details/131970026