目次
2.1 プレオーダー、インオーダー、ポストオーダーのトラバーサル
1. 二分木を手でこする
バイナリ ツリーの基本操作を学習する前に、バイナリ ツリーを作成してから、関連する基本操作を学習する必要があります。二分木の構造を深く理解していないため、学習コストを削減するために、簡単な二分木を作成し、すぐに二分木の演算学習に入る簡単なマニュアルを紹介します。ほぼ理解できたら、方向転換してバイナリ ツリーの実際の作成を検討します。
バイナリ ツリーのノードを定義します。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;
上記のバイナリ ツリー構造に従った手書きのバイナリ ツリー:
BTNode* BuyNode(BTDataType data)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
assert(node);
node->data = data;
node->left = node->right = NULL;
return node;
}
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 プレオーダー、インオーダー、ポストオーダーのトラバーサル
バイナリ ツリー構造を学習する最も簡単な方法は、バイナリ ツリー構造をたどることです。いわゆるバイナリ ツリー トラバーサル (Traversal) は、ある特定のルールに従ってバイナリ ツリー内のノードに対して対応する操作を順番に実行することであり、各ノードは 1 回だけ操作されます。アクセス ノードによって実行される操作は、特定のアプリケーションの問題によって異なります。トラバーサルはバイナリ ツリー上で最も重要な操作の 1 つであり、バイナリ ツリー上の他の操作の基礎でもあります。
ルールによれば、バイナリ ツリーの走査には以下が含まれます。preorder/inorder/postorder再帰的構造の走査:
- プレオーダー トラバーサル (プレオーダー トラバーサル、プレオーダー トラバーサルとも呼ばれます) - ルート ノードを訪問する操作は、左右のサブツリーをトラバースする前に発生します。
- インオーダー トラバーサル (Inorder Traversal) - ルート ノードを訪問する操作は、その左右のサブツリー (中央) をトラバースしながら実行されます。
- ポストオーダー トラバーサル - ルート ノードを訪問する操作は、その左右のサブツリーを走査した後に発生します。
訪問したノードは特定のサブツリーのルートでなければならないため、N (ノード)、L (左のサブツリー)、および R (右のサブツリー) はルート、ルートの左のサブツリー、およびルートの右のサブツリーとして解釈できます。。NLR、LNR、および LRN は、それぞれ、最初のルート トラバーサル、中間ルート トラバーサル、およびバック ルート トラバーサルとも呼ばれます。
バイナリ ツリー トラバーサルのコードは非常に簡単に記述できますが、初心者にとっては理解するのが少し難しいです。ここに 3 つのトラバーサル コードを示します。最初に見てください。
プレオーダートラバーサル
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
インオーダートラバーサル
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
ポストオーダートラバーサル
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
コードを読んだ後、これら 3 つの走査は非常に似ていると思いますか? 3 つの走査のコードをコンパイラで実行してみましょう。
int main()
{
BTNode* root = CreatBinaryTree();
PreOrder(root);
printf("\n");
InOrder(root);
printf("\n");
PostOrder(root);
return 0;
}
操作結果:
プリオーダートラバーサルの再帰的展開グラフ:
インオーダートラバーサルとその後のトラバーサルは、この図に似ています。
2.2 二分木のレベル順序走査
バイナリ ツリーのレベル順序トラバーサルは、幅優先探索 (BFS) 手法です。これは、バイナリ ツリーを階層順にレイヤごとに走査します。つまり、ルート ノードから開始して、最初に第 1 層のノードを走査し、次に第 2 層のノードを走査するというように、すべての層が走査されるまで続きます。
レベル順序トラバーサルを実装する一般的な方法は、キューを使用することです。具体的な考え方は以下のとおりです。
- 空のキューを作成し、ルート ノードをキューに追加します。
- キューが空になるまで、次の手順をループします。
- ノードをデキューし、その値を結果リストに保存します。
- ノードに左の子がある場合は、左の子をキューに入れます。
- ノードに正しい子がある場合は、その正しい子をキューに入れます。
このようにして、キューが空になると走査プロセスが完了し、階層走査の結果が結果リストに格納されます。
次のコードは、C++STL のキューを使用して、キューを手書きするトラブルを回避します。
void LevelOrder(BTNode* root)
{
assert(root);
queue<BTNode*> a;
a.push(root);
while (!a.empty())
{
BTNode* front = a.front();
a.pop();
printf("%d ", front->data);
if (front->left)
{
a.push(front->left);
}
if (front->right)
{
a.push(front->right);
}
}
}
int main()
{
BTNode* root = CreatBinaryTree();
LevelOrder(root);
return 0;
}
出力結果:
3. 二分木に対する一般的な操作
3.1 二分木のノード数を求める
方法 1:
グローバル変数カウントを定義し、各ノードをトラバースします。ノードがトラバースされるたびに、カウントは 1 ずつ増加します。
コード:
int count = 0;
void TreeSize1(BTNode* root)
{
if (root == NULL)
{
return;
}
count++;
TreeSize1(root->left);
TreeSize1(root->right);
}
int main()
{
BTNode* root = CreatBinaryTree();
count = 0;
TreeSize1(root);
printf("%d\n", count);
return 0;
}
方法 2:
int TreeSize2(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return TreeSize2(root->left) + TreeSize2(root->right) + 1;
}
int main()
{
BTNode* root = CreatBinaryTree();
printf("TreeSize2: %d\n", TreeSize2(root));
return 0;
}
まずルート ノードが空かどうかを確認し、空の場合は空のツリーであることを意味し、直接 0 を返します。ルート ノードが空でない場合は、それ自体を再帰的に呼び出して左右のサブツリーのノード数をカウントし、左側のサブツリーのノード数、右側のサブツリーのノード数、およびルート ノードの数を加算します。ノード自体 (1 ノード) を取得し、最後に結果を返します。
この再帰的な方法で、関数はバイナリ ツリー内のすべてのノードの合計数を計算できます。
3.2 二分木の葉ノードの数を求める
具体的なアイデア:
まずルート ノードが空かどうかを確認し、空の場合は空のツリーであることを意味し、直接 0 を返します。次に、ルート ノードの左側のサブツリーと右側のサブツリーが両方とも空かどうかを判断することで、現在のノードがリーフ ノードであるかどうかを判断します。葉ノードの場合は 1 を返します。リーフノードでない場合は、自分自身を再帰的に呼び出して、左サブツリーと右サブツリーのリーフノードの数を計算し、それらを結果として加算して返します。
int TreeLeafSize(BTNode* root)
{
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return 1;
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
int main()
{
BTNode* root = CreatBinaryTree();
printf("TreeLeafSize: %d\n", TreeLeafSize(root));
return 0;
}
3.3 二分木の k 番目の層のノード数を求める
アイデア:
まずルート ノードが空かどうかを確認し、空の場合は空のツリーであることを意味し、直接 0 を返します。k が 1 に等しい場合、現在のレイヤがターゲット レイヤであることを意味し、1 を返します。k が 1 より大きい場合、それ自体を再帰的に呼び出して、左右のサブツリーのレベル k-1 にあるノードの数をカウントし、それらを加算して結果として返します。
この再帰的な方法で、関数はバイナリ ツリー内の k 番目のレベルのノードの合計数を計算できます。
int TreeKLevel(BTNode* root, int k)
{
assert(k >= 1);
if (root == NULL) return 0;
if (k == 1) return 1;
return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}
int main()
{
BTNode* root = CreatBinaryTree();
printf("TreeKLevel: %d\n", TreeKLevel(root, 2));//第2层节点数量
printf("TreeKLevel: %d\n", TreeKLevel(root, 3));//第3层节点数量
printf("TreeKLevel: %d\n", TreeKLevel(root, 4));//第4层节点数量
return 0;
}
関数の再帰展開グラフ:
3.3 二分木の深さを求める
int TreeDepth(BTNode* root)
{
if (root == NULL) return 0;
int l = TreeDepth(root->left); //左子树的深度
int r = TreeDepth(root->right); //右子树的深度
return (l > r ? l : r) + 1; //返回左右子树深度的较大值加自身的深度1
}
int main()
{
BTNode* root = CreatBinaryTree();
printf("TreeDepth: %d\n", TreeDepth(root));
return 0;
}
まずルート ノードが空かどうかを確認し、空の場合は空のツリーであることを意味し、直接深さ 0 に戻ります。次に、関数はそれ自体を再帰的に呼び出すことによって左と右のサブツリーの深さを計算し、結果をそれぞれ変数 l と r に保存します。次に、l と r の大きさを比較して、大きい方の値を選択し、それに 1 (現在のノードの深さを表す) を加えて、二分木全体の深さとして計算します。
この再帰的な方法で、関数はバイナリ ツリーの深さ (高さ) を計算できます。
3.4 二分木で値が x であるノードを見つける
バイナリ ツリーで値 x を持つノードを探すときは、効率を向上させるために事前順序トラバーサルを使用してみてください。
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL) return NULL;
if (root->data == x) return root;
BTNode* ret1 = TreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = TreeFind(root->right, x);
if (ret2)
{
return ret2;
}
return NULL;
}
int main()
{
BTNode* root = CreatBinaryTree();
BTNode* ret = TreeFind(root, 3);
if (ret)
{
printf("找到了:%d\n", ret->data);
}
else
{
printf("找不到\n");
}
return 0;
}
まずルート ノードが空かどうかを確認し、空の場合は空のツリーであることを意味し、直接 NULL を返します。次に、現在のノードのデータが目標値 x と等しいかどうかを確認し、等しい場合は、目的のノードが見つかったことを意味し、現在のノードへのポインタを返します。等しくない場合は、関数を再帰的に呼び出して、左側のサブツリーと右側のサブツリーでそれぞれターゲット値 x を見つけます。返されたポインタが空でない場合は、ターゲット ノードがサブツリー内で見つかったことを意味し、ポインタを直接返します。対象ノードが左右のサブツリーに見つからない場合は、NULL が返されます。
この再帰を使用すると、関数はバイナリ ツリー内の特定の値のノードを検索し、そのノードへのポインタを返すことができます。ターゲット値が見つからない場合は NULL を返します。
4. バイナリツリーの破壊
バイナリ ツリーの破棄には、プレオーダー トラバーサルは使用できません。プレオーダー トラバーサルを使用すると、バイナリ ツリーのルート ノードが最初に破壊され、ルート ノードの左右のサブツリーが見つからなくなるためです。最初のサブツリーを使用する必要があります。 走査順序では、ノードの左側のサブツリーと右側のサブツリーを最初に保存する必要があります。しかし、ポストオーダートラバーサルを使用すれば、それを簡単に解決できます。
事後探索を使用して、左の子、右の子、ルート ノードの順序でバイナリ ツリーを破棄します。
void TreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
TreeDestory(root->left);
TreeDestory(root->right);
free(root);
}
int main()
{
BTNode* root = CreatBinaryTree();
PreOrder(root);
TreeDestory(root);
root = NULL;
}