連鎖二分木の実装
1.予備的な指示
なぜ連鎖二分木を構築するのですか?
不完全な二分木構造、つまり不規則な二分木構造を格納するために、中間ノードは要素を格納しない場合があります。例えば:
注:共通の二分木を追加、削除、確認、変更することは無意味です。データを格納するだけの場合は、シーケンステーブルの二分木の構造を使用することをお勧めします。?
Q:では、なぜ連鎖二分木を学ぶのですか?
回答:その構造をより適切に制御できるようにするために、より複雑な検索二分木のその後の学習の基礎を築きます。さらに、多くの二分木OJの問題は、通常の二分木にあります。
連鎖した二分木を作成するだけです。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;//节点存储的数据
struct BinaryTreeNode* left;//存储左子节点的地址
struct BinaryTreeNode* right;//存储右子节点的地址
}BTNode;
BTNode* BuyNode(BTDataType x)//开辟一个新节点
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
assert(newnode);
newnode->data = x;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
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;
}
注:上記のコードは二分木を作成する方法ではありません。二分木を作成する実際の方法は、シーケンスの後で詳細に説明されています。
二分木の基本的な操作を見る前に、二分木の概念を確認しましょう。二分木は次のとおりです。
- 空の木
- 空でない:ルートノード、ルートノードの左側のサブツリー、およびルートノードの右側のサブツリー。
概念からわかるように、二分木の定義は再帰的であるため、ポストオーダーの基本的な操作は基本的にこの概念に従って実装されます。
2.二分木のトラバーサル
2.1プレオーダー、インオーダー、ポストオーダートラバーサル(深さ優先トラバーサル(DFS))
バイナリツリートラバーサル(Traversal)は、特定のルールに従ってバイナリツリー内のノードに対して対応する操作を順番に実行することであり、各ノードは1回だけ操作されます。アクセスノードによって実行される操作は、特定のアプリケーションの問題によって異なります。トラバーサルは、バイナリツリーでの最も重要な操作の1つであり、バイナリツリーでの他の操作の基礎でもあります。
ルールによると、バイナリツリーのトラバーサルには次のものが含まれます。preorder/ inorder / postorder再帰的構造トラバーサル:
-
プレオーダートラバーサル(プレルートトラバーサルとも呼ばれます)-ルートノードにアクセスする操作は、その左右のサブツリーをトラバースする前に発生します。つまり、アクセス順序は次のとおりです。ルートノード-左サブツリー-右サブツリー
上記の二分木図を例にとると、プレオーダートラバーサルが次の図に示されています。
-
インオーダートラバーサル(ミッドルートトラバーサルとも呼ばれます)-ルートノードへのアクセスは、その左右のサブツリーのトラバーサル(中央)中に発生します。つまり、アクセス順序は次のとおりです。左サブツリー-ルートノード-右サブツリー
順序どおりのトラバーサルを図に示します。
-
ポストオーダートラバーサル(ポストルートトラバーサルとも呼ばれます)-ルートへのアクセスは、その左右のサブツリーをトラバースした後に発生します。つまり、アクセス順序は次のとおりです。左サブツリー-右サブツリー-ルートノード
注文後のトラバーサルを図に示します。
2.23つのトラバーサルの実装
2.2.1プレオーダートラバーサルの実装
コード:
void PreOrder(BTNode*root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);//遍历根节点
PreOrder(root->left);//遍历左子树节点
PreOrder(root->right);//遍历右子树节点
}
イラスト:()内の数字はコードの実行順序です
2.2.2インオーダートラバーサルの実装
コード:
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
アイコン:
2.2.3ポストオーダートラバーサルの実装
コード:
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
InOrder(root->right);
printf("%d ", root->data);
}
2.3レベル順トラバーサル(幅優先トラバーサル(BFS))
レイヤー順序トラバーサル-ヘッドノードから開始して1つずつ逆方向にトラバースし、レイヤーごとに直接トラバースします。上図の走査順序は次のとおりです。1-2-4-3-5-6
アイデア:
- キューの先入れ先出しの性質を利用して、最初にルートをキューに入れます
- 前のレイヤーのノードが消えたら、次のレイヤーの左右の子ノードを入れます
アイコン:
達成:
注:2つのファイルQueue.cとQueue.hを使用し、キューに格納されているデータ型をBinaryTreeNode *として定義します。ヘッダーファイルのインクルードと、キューファイルでの構造体の宣言に注意してください。
Queue.hファイルの変更:
Test.cファイルの変更
コード:
void LevelOrder(BTNode* root)
{
Queue q;//队列的创建
QueueInit(&q);
if (root)//判断是否为空二叉树
{
QueuePush(&q, root);//将root节点push到队列中
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);//拿到队列首元素
QueuePop(&q);//将出队列的节点从队列中删掉
if (front->left)
{
QueuePush(&q, front->left);//将左子节点push到队列中
}
if (front->right)
{
QueuePush(&q, front->right);//将右子节点push到队列中
}
printf("%d ", front->data);//打印出队列的数据
}
//对列的销毁
QueueDestory(&q);
}
2.4レベル順トラバーサルの適用----完全な二分木の判断
アイデア:
- レベル順トラバーサル、空のノードもキューに入れられます。注:キューに入る前に、フロントが空かどうかを確認してください。空でない場合は、ヌルポインターの逆参照のエラーが発生します。
- 空のノードに出た後、出口キュー内のすべてのデータは、すべてが空の場合は完全な二分木であり、空でない場合はそうではありません。
アイコン:
コード:
//判断一个二叉树是否是完全二叉树
bool BTreeComplete(BTNode* root)
{
Queue q;//队列的创建
QueueInit(&q);
if (root)//判断是否为空二叉树
QueuePush(&q, root);//将root节点push到队列中
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)//出到空的时候退出,后面再进行判断
{
break;
}
//为什么要放到后面push呢?为了防止对空指针进行解引用
QueuePush(&q, front->left);//此时front一定不为空
QueuePush(&q, front->right);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
//空后面出现非空,就说明不是完全二叉树
if (front)
{
return false;
}
QueuePop(&q);
}
//队列的销毁
QueueDestory(&q);
return true;
}
注:次の状況を考慮してください。
3.ノードの数と高さなど。
3.1二分木ノードの数
2つの方法:
方法1
アイデア:トラバース+カウント
コード:
int count = 0;
void BTreeSize(BTNode* root)
{
if (root == NULL)
{
return;
}
count++;
BTreeSize(root->left);
BTreeSize(root->right);
}
もちろん、ローカル静的変数を使用してノードの数を格納することもできますが、お勧めしません。コードは次のとおりです。
int BTreeSize(BTNode* root)
{
static int count = 0;//只会在第一次进入时初始化一次
if (root == NULL)
{
return count;
}
count++;
BTreeSize(root->left);
BTreeSize(root->right);
return count;
}
もちろん、上記の2つの方法はどちらも適切ではなく、連鎖二分木の再帰的定義の構造的特性を十分に活用していないため、上記の2つの方法はお勧めしません。上記の2つの方法の2番目は、特に要素数が複数回計算される場合に特に悪いです。2番目の計算でカウントが再初期化されない場合、前の計算の値は引き続き保持されます。使用される場合グローバル変数は、カウントごとに再割り当てすることもできますが、2番目の方法では、最後のカウント後の残差値を変更することはできません。
マルチスレッドの場合、グローバル変数に問題が発生します。たとえば、マルチスレッドの状況では問題が発生します。複数のスレッドがグローバル変数カウントを同時に使用し、このときにスレッドセーフの問題が発生します。
もちろん、呼び出し元の関数でカウント変数を定義し、呼び出し時にカウントのアドレスを渡すこともできます。その場合、BTreeSize関数は次のように定義する必要があります。
int BTreeSize(BTNode* root,size_t *count)
{
static int count = 0;//只会在第一次进入时初始化一次
if (root == NULL)
{
return count;
}
(*count)++;
BTreeSize(root->left);
BTreeSize(root->right);
return count;
}
方法2(推奨)
(再帰的方法)
アイデア:サブ問題
1.空のツリー、最小スケールのサブ問題、ノードの数は0を返します
2.空ではない、左側のサブツリーノードの数+右側のサブツリーノードの数+ 1(自己)
コード:
int BTreeSize(BTNode* root)
{
return root==NULL ? 0 :
BTreeSize(root->left) +
BTreeSize(root->right) + 1;//1在最前面就是前序,在中间就是中序,在最后就是后序
}
この方法では、連鎖二分木の本質がルートノードと2つのサブツリーで構成されているため、連鎖二分木の再帰的定義の構造プロパティを最大限に活用します。サブツリー全体を見つけるには、ルートノード1と左右のサブツリーのノード。
写真が示すように:
注:図の紫色の数字は戻り値を表し、黒い数字は対応する関数の戻り値を表します。
使用したアルゴリズムのアイデア:分割統治
分割統治法:複雑な問題を小規模なサブ問題に分割し、サブ問題を小規模なサブ問題に分割します...サブ問題が分割できなくなり、結果を直接取得できるようになるまで。
3.2二分木の子ノードの数
方法1
アイデア:(トラバーサル+カウント)
リーフノードをカウントする目的を達成するために、リーフノードの判定条件がcount ++の前に追加されることを除いて、基本的に上記の考え方と同じです。
コード:
int count = 0;
void BTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL&&root->right==NULL)
{
count++;
}
BTreeLeafSize(root->left);
BTreeLeafSize(root->right);
}
方法2
アイデア:分割統治のアイデアを採用します。
コード:
int BTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL&&root->right==NULL)
{
return 1;
}
else
return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}
もちろん、上記のコードは次のコードに短縮できます。
int BTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return root->left == NULL && root->right == NULL ?
1 + BTreeLeafSize(root->left)+ BTreeLeafSize(root->right) :
0 + BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}
ただし、比較的簡単に言えば、単純化されていないものの方が理解しやすいため、より推奨されます。
3.3二分木のk番目の層のノード数
注:k> = 1
考え:
- 空のツリー、0を返します
- null以外、1を返します
- 空ではなく、k> 1、シークに変換左側のサブツリーのk-1レベルのノード数+右側のサブツリーのk-1レベルのノード数。
int BTreeKLevelSize(BTNode* root,int k)
{
assert(k >= 1);
if (root == NULL||k<)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BTreeKLevelSize(root->left, k - 1) +
BTreeKLevelSize(root->left, k - 1);
}
アイコン:
3.4二分木の深さ
アイデア:分割統治のアイデア
二分木の高さ=左側のサブツリーの高さと右側のサブツリーの高さ、大きい方のサブツリー+1。
int BTreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftDepth = BTreeDepth(root->left);//左子树的深度
int rightDepth = BTreeDepth(root->right);//y
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;//1是因为根节点本身也算作是一层
}
3.5値xのノードを見つけるための二分木
アイデア:分割統治 二分木=ルートノード+左サブツリー+右サブツリー
- 現在のルートノードが空かどうかを確認します
- 現在のノードが探している値であるかどうかを判断します
- 探しているノードが左側のサブツリーに存在するかどうかを確認します
- 探しているノードが正しいサブツリーに存在するかどうかを確認します
- 探しているノードが現在の二分木に存在しません
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
//节点为空的情况:直接返回NULL
if (root == NULL)
{
return NULL;
}
//当前节点就是我们要查找的节点:返回当前节点的地址
if (root->data == x)
{
return root;
}
//左子树
BTNode* leftRet = BinaryTreeFind(root->left, x);
if (leftRet)
{
return leftRet;
}
//右子树
BTNode* rightRet = BinaryTreeFind(root->right, x);
if (rightRet)
{
return rightRet;
}
//都找不到的情况下返回NULL,就是说当前二叉树中不存在存储该值的节点
return NULL;
}