1. ツリーの概念と構造
1.1 木の概念
ツリーは非線形データ構造であり、n (n>=0) 個の有限ノードで構成される一連の階層関係です。根が上を向き、葉が下を向いている、逆さまの木に見えることからツリーと呼ばれています。
- ルート ノードと呼ばれる特別なノードがあり、ルート ノードには先行ノードがありません。
- ルートノードを除く他のノードはM (M>0)個の素集合T1**, T2 , ... , **Tmに分割され、各集合 Ti (1<= i <= m) は次のような構造を持つ部分木です。木に似ています。各サブツリーのルート ノードには 1 つのみの先行ノードがあり、0 個以上の後続ノードを持つことができます。
- したがって、ツリーは再帰的に定義されます。
注: ツリー構造では、サブツリー間に交差があってはなりません。そうでない場合、それはツリー構造ではありません。
1.2 ツリーの関連概念
ノードの次数: ノードに含まれるサブツリーの数はノードの次数と呼ばれます; 上の図に示すように: A は 6
リーフ ノードまたはターミナル ノード: 次数 0 のノードはリーフ ノードと呼ばれます。上の図に示すように: B、C、H、I... およびその他のノードはリーフ ノードです。
非終端ノードまたは分岐ノード: 次数が 0 ではないノード、上図に示すように: D、E、F、G... などのノードが分岐ノードです。
親ノードまたは親ノード: ノードに子ノードが含まれている場合、このノードはその子ノードの親ノードと呼ばれます; 上に示すように: A は B の親ノードです
子ノードまたは子ノード: ノードに含まれるサブツリーのルート ノードは、上に示すように、ノードの子ノードと呼ばれます: B は A の子ノードです。
兄弟ノード: 同じ親ノードを持つノードは兄弟ノードと呼ばれます。上に示すように: B と C は兄弟ノードです。
ツリーの次数: ツリーでは、最大のノードの次数がツリーの次数と呼ばれます。上に示すように、ツリーの次数は 6 です。
ノードのレベル: ルートの定義から始まり、ルートが第 1 レベル、ルートの子ノードが第 2 レベル、というようになります。
ツリーの高さまたは深さ: ツリー内のノードの最大レベル。上に示すように、ツリーの高さは 4 です。
いとこノード: 親が同じレイヤーにあるノードはいとこです。上の図に示すように: H と I は兄弟ノードです。
ノードの祖先: ルートからノードまでの分岐上のすべてのノード。上の図に示すように: A はすべてのノードの祖先です。
子孫: ノードをルートとするサブツリー内のノードは、ノードの子孫と呼ばれます。上に示すように、すべてのノードは A の子孫です。
森: m (m>0) 個の互いに素な木の集まりを森と呼びますが、
概念はたくさんあり、それを 1 つずつ理解する必要があります。
1.3 ツリー表現
ツリー構造は線形テーブルに比べて複雑で、保存や表現が面倒です。値の範囲が保存されるため、ノードとノード間の関係も保存されます。実際には、ツリーを表現する方法はたくさんあります。親表現、子表現、子の親表現、および子の兄弟表現として。ここでは、最も一般的に使用される子兄弟表記法を簡単に理解します。
1.4 ツリーの実際の応用 (ファイルシステムのディレクトリツリー構造を表す、インターネット上にある画像)
2. 二分木の概念と構造
2.1 関連概念
バイナリ ツリーはノードの有限セットです。
1. または空
2. ルート ノードと、左サブツリーと右サブツリーと呼ばれる 2 つのバイナリ ツリーで構成されます。
- 二分木には次数が 2 を超えるノードがありません
- 二分木の部分木は左右に分かれており、順序を逆にすることはできないため、二分木は順序付き木になります。
注: バイナリ ツリーは、次の状況で構成されます。
2.2 特殊なバイナリ ツリー:
1.完全なバイナリ ツリー: バイナリ ツリー。各層のノード数が最大値に達すると、このバイナリ ツリーは完全なバイナリ ツリーになります。つまり、バイナリ ツリーの層の数が K で、ノードの総数が である場合、それは完全なバイナリ ツリーです。
2.完全なバイナリ ツリー: 完全なバイナリ ツリーは非常に効率的なデータ構造であり、完全なバイナリ ツリーは完全なバイナリ ツリーから派生します。深さが K でノードが n のバイナリ ツリーの場合、その各ノードが完全なバイナリ ツリー内の 1 から n までの番号が付けられたノードと 1 対 1 対応する場合に限り、完全なバイナリ ツリーと呼ばれます。 Kの深さ。完全なバイナリ ツリーは特別な種類の完全なバイナリ ツリーであることに注意してください。
閉店の概念がわかりにくい、上の写真
2.3 二分木の性質
ルート ノードの層の数が 1 として指定された場合、空ではない二分木の i 番目の層には最大 2^(i-1) 個のノードが存在します。
ルートノードの層数を 1 に指定すると、深さ h の二分木の最大ノード数は 2^(h-1) になります。
任意の二分木について、次数が 0、葉ノードの数が n0、次数 2 の分岐ノードの数が n2 の場合、n0 = n2 + 1
ルート ノードの層数を 1 に指定すると、n 個のノードを持つ完全なバイナリ ツリーの深さ、h=long(n+1) (ps: log は 2 に基づいており、n+1 は対数です)。
n 個のノードを持つ完全なバイナリ ツリーの場合、配列順序に従って上から下、左から右にすべてのノードに 0 から番号が付けられている場合、シーケンス番号 i のノードについては次のようになります。
1. i>0の場合、i 位置のノードの親番号: (i-1)/2; i=0、i はルート ノード番号であり、親ノードはありません
2. 2i+1<nの場合、左の子の番号: 2i+1, 2i+1>=nそれ以外の場合、左の子はありません。
3. 2i+2<nの場合、右側の子のシリアル番号: 2i+2, 2i+2>=n、そうでない場合は右側の子はありません
バイナリ ツリーの性質を理解したら、いくつかの質問から始めましょう。
1. 二分木には 399 個のノードがあり、その中に次数 2 のノードが 199 個ある場合、二分木内の葉ノードの数は ( ) A になります。そのような二分木は存在しません B 200
C
198
D
199
答え:B. n0 = n2+1、すなわち、n0=199+1 = 200 2. 以下のデータ構造のうち、順次記憶構造は、( ) A 不完全なバイナリ ツリーB ヒープC キューD スタック
には適しません。
答え: A.
3. 2n 個のノードを持つ完全な二分木では、葉ノードの数は ( )
A n
B n+1
C n-1
D n/2です。
答え: A. 点の数を要約します n = n0+n1+n2; および n2 = n0-1、両方とも n=n0+n1+n0-1、完全な二分木では、n1 は 0 または 1 のみを持ち、A に代入します
4. 完全な二分木のノード数は 531 で、この木の高さは ( )
A 11
B 10
C 8
D 12となります。
答え:B
5. 767 個のノードを持つ完全なバイナリ ツリー。リーフ ノードの数は ()
A 383
B 384
C 385
D 386
答え:B. 質問 3 と同様に、要約ポイントは n = n0+n1+n0-1、n1 は 0 または 1 で、B を選択するのは割り切れます。
2.4 バイナリツリーの記憶構造
バイナリ ツリーは通常、シーケンシャル構造とチェーン構造の 2 つの構造を使用して格納できます。
1. 順次保存
シーケンシャル構造ストレージでは、ストレージに配列を使用しますが、完全でないバイナリ ツリーはスペースを無駄にするため、一般に配列は完全なバイナリ ツリーを表す場合にのみ適しています。実際には配列に格納されるのはヒープのみですが、ヒープについては次の章で具体的に説明します。バイナリ ツリー順次ストレージは、物理的には配列であり、論理的にはバイナリ ツリーです。
2. チェーン構造
二分木の連結記憶構造とは、二分木を表現するために連結リストが使用されること、すなわち、要素の論理的関係を示すためにリンクが使用されることを意味する。通常の方法では、リンク リストの各ノードは、データ フィールドと左右のポインタ フィールドの 3 つのフィールドで構成され、左右のポインタは、左の子と左の子がポイントするリンクのストレージ アドレスを与えるために使用されます。ノードの右の子が見つかります。チェーン構造はさらにバイナリチェーンとトリプルチェーンに分類され、現在はバイナリチェーンを使用して学習しますが、後のコースではトリプルチェーンを使用した赤黒ツリーなどの高度なデータ構造を学習します。
3. シーケンス構造と二分木の実装
3.1 二分木の逐次構造
通常のバイナリ ツリーは、無駄な領域が多くなる可能性があるため、配列での保存には適していません。完全なバイナリ ツリーは、順次構造の保存に適しています。実際には、通常、シーケンシャル構造の配列にヒープ (バイナリ ツリー) を格納します。ここでのヒープとオペレーティング システムの仮想プロセス アドレス空間内のヒープは 2 つの異なるものであることに注意してください。1 つはデータです。もう 1 つはオペレーティング システムでの管理であり、メモリの領域はセグメント化されます。
4. 二分木チェーン構造の実現
バイナリ ツリーの基本操作を学習する前に、バイナリ ツリーを作成してから、関連する基本操作を学習する必要があります。全員の学習コストを削減するために、ここでは簡単なバイナリ ツリーが手動で迅速に作成されます。
このツリーのコード実装は次のとおりです。
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* CreatTree()
{
BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
assert(n1);
BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
assert(n2);
BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
assert(n3);
BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
assert(n4);
BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
assert(n5);
BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
assert(n6);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n5->data = 5;
n6->data = 6;
n1->left = n2;
n1->right = n4;
n2->left = n3;
n2->right = NULL;
n3->left = NULL;
n3->right = NULL;
n4->left = n5;
n4->right = n6;
n5->left = NULL;
n5->right = NULL;
n6->left = NULL;
n6->right = NULL;
return n1;
}
枠組みを構築したら次のステップに進みます
4.1 レイヤー順序のトラバーサル
事前順序、順序どおり、順序後の走査再帰操作:
//先序
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
//中序
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
//后序
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
二分木のレベル順の走査は一見難しくないようですが、深く理解すると再帰呼び出しの処理が非常に複雑であることがわかりましたので、再帰呼び出しの展開図を理解してみましょう。
これら 3 つの走査方法に加えて、バイナリ ツリーには層順序走査もあります。
レベル順序トラバーサル: 前順序トラバーサル、順序内トラバーサル、後順序トラバーサルに加えて、バイナリ ツリーのレベル順序トラバーサルも実行できます。バイナリ ツリーのルート ノードが 1 層にあると仮定すると、層順序のトラバーサルは、バイナリ ツリーが配置されているバイナリ ツリーのルート ノードから開始し、最初に最初の層のルート ノードを訪問し、次に最初の層のノードを訪問します。 2 番目のレイヤーは左から右、次に 3 番目のレイヤーのノード、というように、上から下、左から右にレイヤーごとにツリーのノードを訪問するプロセスがレイヤー シーケンスです。
では、レイヤー順序のトラバーサルを実装するにはどうすればよいでしょうか?
すべてのノードがキューから外れるまで、キューを借り、ノードをキューに入れ、キューを空のままにせず、子をキューに取り込み、さらに子をキューに入れるということを繰り返すことができます。つまり、キューは空のシーケンスです。トラバーサルは終了しました。簡単に言うと、前の層のノードが出てきたときに、次の層のノードを取り込むことです。これは C 言語で実装されているため、まず手動でキューを記述する必要があります。
//二叉树
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
//队列(用链表实现,data的类型是BTNode*)
typedef BTNode* QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (pq->tail == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}
int QueueSize(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
int size = 0;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
//层序遍历
void TreeLevelOrder(BTNode*root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ",front->data);
//下一层
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q);
}
4.2 その他の操作
ノード数
// 节点的个数
int TreeSize(BTNode* root)
{
return root == NULL ? 0 :
TreeSize(root->left) +
TreeSize(root->right) + 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 TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int lh = TreeHeight(root->left);
int rh = TreeHeight(root->right);
return lh > rh ? lh + 1 : rh + 1;
}
k番目の層のノードの数
// 第K层节点个数
int TreeKLevel(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
return 0;
if (k == 1)
return 1;
// 转换成求子树第k-1层
return TreeKLevel(root->left, k - 1)
+ TreeKLevel(root->right, k - 1);
}
x が位置するノードを返します
// 返回x所在的节点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
// 先去左树找
BinaryTreeNode*lret = TreeFind(root->left, x);
if (lret)
return lret;
// 左树没有找到,再到右树找
BinaryTreeNode*rret = TreeFind(root->right, x);
if (rret)
return rret;
return NULL;
}
バイナリツリーの破壊
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
main 関数を通じて上記のいくつかの関数をテストします
int main()
{
BTNode* root = CreateTree();
PreOrder(root);
printf("\n");
InOrder(root);
printf("\n");
printf("Tree size:%d\n", TreeSize(root));
printf("Tree size:%d\n", TreeSize(root));
printf("Tree size:%d\n", TreeSize(root));
printf("Tree Leaf size:%d\n", TreeLeafSize(root));
printf("Tree Height:%d\n", TreeHeight(root));
printf("Tree K Level:%d\n", TreeKLevel(root, 3));
printf("Tree Find:%p\n", TreeFind(root, 8));
BTNode* ret = TreeFind(root, 7);
ret->data *= 10;
PreOrder(root);
printf("\n");
return 0;
}
4.3 完全二分木の判定
完全な二分木かどうかはどうやって判断するのでしょうか? これは、上で説明したレイヤー順序のトラバーサルに使用できます。
レイヤーごとに進みます。完全なバイナリ ツリーの場合、レイヤーごとに進みます。空のスペースに遭遇すると、空でないものは存在しません (完全なバイナリ ツリーは左から右に連続しているため)。空の場合、それは完全なバイナリ ツリーではありません。
//判断是否为完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
{
break;
}
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
//遇到空以后,后面全是空——完全二叉树
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front != NULL)
{
QueueDestory(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
目次
1.4 ツリーの実際の応用 (ファイルシステムのディレクトリツリー構造を表す、インターネット上にある画像)