技術共有

【データ構造】 09. 木と二分木

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

1. ツリーの概念と構造

1.1 ツリーの概念

木は非線形データ構造。n (n>=0) 個の限定されたノードで構成される階層関係のセットです。バンドル根が上を向き、葉が下を向いている、逆さまの木に見えることからツリーと呼ばれています。

  • ルート ノード: ルート ノードには先行ノードがありません。
  • ルート ノードを除いた残りのノードは、ツリーと同様の構造を持つサブツリーに分割されます。各サブツリーのルート ノードには 1 つのみの先行ノードがあり、0 個以上の後続ノードを持つことができます。
  • したがって、その木は、再帰的定義の。

ここに画像の説明を挿入します

1.2 ツリーの関連概念

ここに画像の説明を挿入します

  • ノードの次数: 上の図に示すように、ノードに含まれるサブツリーの数はノードの次数と呼ばれます。A は 4 です。
  • リーフノードまたはターミナルノード: 次数 0 のノードはリーフ ノードと呼ばれます。上の図に示すように: ノード B、F、G、D、および H はリーフ ノードです。
  • 非終端ノードまたはブランチノード: 次数が 0 でないノード。 上の図に示すように、ノード A、C、E は分岐ノードです。
  • 親ノードまたは親ノード: ノードに子ノードが含まれる場合、このノードはその子ノードの親ノードと呼ばれます。上の図に示すように、A は B の親ノードです。
  • 子ノードまたは子ノード: ノードに含まれるサブツリーのルート ノードは、ノードの子ノードと呼ばれます。 上の図に示すように、B は A の子ノードです。
  • 兄弟ノード: 同じ親ノードを持つノードは兄弟ノードと呼ばれます。上に示すように、B と C は兄弟ノードです。
  • 木の程度: ツリーでは、上に示すように、最大​​のノードの次数がツリーの次数と呼ばれます。ツリーの次数は 4 です。
  • ノードレベル: ルートの定義から開始すると、ルートは第 1 レベル、ルートの子ノードは第 2 レベル、というようになります。
  • 木の高さまたは深さ: 上に示すツリー内のノードの最大レベル: ツリーの高さは 3 です。
  • いとこノード: 上の図に示すように、親が同じレベルにあるノードは互いにいとこです。F と G は互いに兄弟ノードです。
  • ノードの祖先: 上の図に示すように、ルートからノードまでの分岐上のすべてのノード: A はすべてのノードの祖先です。
  • 子孫 : 特定のノードをルートとするサブツリー内のノードは、そのノードの子孫と呼ばれます。上に示すように、すべてのノードは A の子孫です。
  • : m 個の互いに素なツリーの集合をフォレストと呼びます。

1.3 ツリー表現

木構造は線形テーブルよりも複雑であり、それを保存したり表現したりするのが面倒です。値の範囲とノードとノード間の関係の両方を保存する必要があります。実際、木を表す方法は次のようにたくさんあります。親表記、子表記、子親表記、子兄弟表記待って。

次のストレージ構造を導入するプロセスでは、次のツリーを例として取り上げます。ここに画像の説明を挿入します

1.3.1 親の表現

ツリーのノードは一連の連続した空間に格納されていると仮定します。各ノードには、リンク リスト内の親ノードの位置を示すインジケーターが付けられます。 。言い換えれば、各ノードは、自分が誰であるかを知ることに加えて、その親がどこにいるかも知っています。
ここに画像の説明を挿入します
このうちデータは、ノードのデータ情報を格納するデータフィールドです。そして、parent は、配列内のノードの親の添字を格納するポインター フィールドです。
以下は、親表現のノード構造定義コードです。

/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100
typedef int TElemType;	//树结点的数据类型,目前暂定为整型
/*结点结构*/
typedef struct TreeNode
{
	TElemType data;	//结点数据
	int parent;	//双亲位置
}TreeNode;
/*树结构*/
typedef struct
{
	TreeNode nodes[MAX_TREE_SIZE];	//结点数组
	int r, n;	//根的位置和结点数
}PTree;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

このようなストレージ構造を使用すると、使用されるノードの親ポインターに従って、その親ノードを簡単に見つけることができます。時間計算量は 0(1) です 、親が -1 (ツリー ノードのルートが見つかったことを示す) になるまで。ただし、ノードの子が何であるかを知りたい場合は、申し訳ありませんが、構造全体を調べてください。

1.3.2 子の兄弟の表現

先ほど、ツリーのストレージ構造を親の観点と子の観点から調べました。もちろん、ツリーのような階層構造の場合は、ツリー ノードの兄弟に着目するとどうなるでしょうか。観察した結果、どのツリーでも、そのノードの最初の子が存在する場合は一意であり、その右の兄弟も存在する場合は一意であることがわかりました。 したがって、ノードの最初の子とノードの右の兄弟を指す 2 つのポインターを設定します。

ここに画像の説明を挿入します

/*树的孩子兄弟表示法结构定义*/
typedef struct TreeNode
{
	TElemtype data;
	struct TreeNode *firstchild, *rightsib;
} TreeNode,* pTreeNode;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

1.4 樹木の実用化

ここに画像の説明を挿入します

2. 二分木の概念と構造

2.1 コンセプト

バイナリ ツリーは、次のような有限のノードのセットです。

  • または空の
  • これは、ルート ノードと 2 つのバイナリ ツリー (左サブツリーおよび右サブツリーとも呼ばれます) で構成されます。

ここに画像の説明を挿入します

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

  1. 二分木には次数が 2 より大きいノードはありません
  2. 二分木の部分木は左と右の部分木に分割でき、順序を逆にすることはできないので、二分木は次のようになります。順序付けられた木

注: バイナリ ツリーは次の状況で構成されます。
ここに画像の説明を挿入します

2.2 特殊なバイナリ ツリー

  1. 完全なバイナリツリー: 二分木、各層のノード数が最大に達すると、バイナリ ツリーは完全なバイナリ ツリーになります。 。つまり、バイナリ ツリーのレベル数が K で、ノードの総数が 2^k-1 であれば、それは完全なバイナリ ツリーになります。
  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=log (n+1) (ps: log は 2 を底とし、n+1 は対数です)
  5. n 個のノードを持つ完全なバイナリ ツリーの場合、すべてのノードが上から下、左から右の配列順序で 0 から始まる番号が付けられている場合、シリアル番号 i のノードについては次のようになります。
    • i>0 の場合、位置 i のノードの親番号: (i-1)/2;、i はルート ノード番号です。親ノードはありません。
    • 2i+1の場合、2i+1>=n そうでない場合は、左側の子は存在しません。
    • 2i+2の場合、2i+2>=n そうでない場合、正しい子は存在しません。

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

バイナリ ツリーは通常、次の 2 つの構造を使用して保存できます。シーケンシャル構造、チェーン構造。

  1. 順次ストレージ
    シーケンシャル構造のストレージを使用します配列保存するには、通常、配列を使用します完全なバイナリ ツリーを表す場合にのみ適しています完全なバイナリ ツリーではなく、スペースの無駄が発生するためです。実際には、ストレージに配列を使用するのはヒープだけです。バイナリ ツリーの順次ストレージは、物理的には配列であり、論理的にはバイナリ ツリーです。
    ここに画像の説明を挿入します2. チェーンストレージ
    バイナリ ツリーのリンクされたストレージ構造とは、次のことを指します。リンク リストを使用してバイナリ ツリーを表現するつまり、チェーンを使用して要素の論理関係を示します。 **通常の方法では、リンク リストの各ノードは、データ フィールドと左右のポインタ フィールドという 3 つのフィールドで構成されます。左ポインタと右ポインタは、左の子と右のポインタが指すリンクのストレージ アドレスを指定するために使用されます。ノードの右側の子がそれぞれ配置されます。 **鎖の構造は二元鎖と三分岐鎖に分けられます。現在、私たちは一般的に二元鎖を研究しています。
    ここに画像の説明を挿入します

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

通常のバイナリ ツリーは、無駄な領域が多くなる可能性があるため、配列での保存には適していません。そして完全なバイナリ ツリーは、シーケンシャル構造のストレージに適しています。 。実際、私たちは普段、ヒープ (バイナリ ツリー) は、連続した構造の配列を使用して格納します。ここでのヒープとオペレーティング システムの仮想プロセス アドレス空間のヒープは 2 つの異なるものであることに注意してください。1 つはデータ構造であり、もう 1 つはオペレーティング システムのメモリを管理する領域の分割です。
具体的な実装と応用については、以下を参照してください。[データ構造] 08. ヒープとヒープアプリケーション

4. 二分木のチェーン構造とその実装

4.1 バイナリツリートラバーサル

バイナリ ツリー構造を学習する最も簡単な方法は、バイナリ ツリー構造をトラバースすることです。いわゆる二分木走査(Traversal)とは、二分木のノードに対して一定の規則に従って対応する操作を順番に実行することであり、各ノードの操作は一度だけ行われます。 。ノードにアクセスすることによって実行される操作は、特定のアプリケーションの問題によって異なります。 トラバーサルはバイナリ ツリー上で最も重要な操作の 1 つであり、バイナリ ツリー上の他の操作の基礎でもあります。

4.1.1 事前注文トラバーサル

//根 左子树 右子树
void PrevOrder(pTreeNode root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	printf("%d ", root->val);
	PrevOrder(root->left);
	PrevOrder(root->right);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

以下の図は再帰的なプロセスを示しています。
ここに画像の説明を挿入します

4.1.2 順序どおりの走査

//左子树 根 右子树
void InOrder(pTreeNode root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	InOrder(root->left);
	printf("%d ", root->val);
	InOrder(root->right);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

再帰的なプロセスは次のとおりです。
ここに画像の説明を挿入します

4.1.3 事後トラバーサル

//左子树 右子树 根
void PostOrder(pTreeNode root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->val);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

再帰的なプロセスは次のとおりです。
ここに画像の説明を挿入します

4.2.4 レベル順序のトラバーサル

レベル順序トラバーサル: 事前順序トラバーサル、順序内トラバーサル、および事後順序トラバーサルに加えて、レベル順序トラバーサルもバイナリ ツリーで実行できます。バイナリ ツリーのルート ノードのレベル番号が 1 であるとします。レベル順序のトラバーサルは、バイナリ ツリーのルート ノードから始まり、最初に最初のレベルのルート ノードを訪問し、次に 2 番目のレベルのノードを訪問します。左から右にレベル、次にレイヤーのノード、というように続きます。ツリーのノードを上から下、左から右にレイヤーごとに訪問するプロセスは、レイヤー順序のトラバーサルです。
図:
ここに画像の説明を挿入します

ここでは、キューに介入してバイナリ ツリーの事前順序トラバーサルを実行します。

// 层序遍历
void LevelOrder(pTreeNode root)
{
	Queue q;
	QueueInit(&q);
	if (root)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		pTreeNode front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			printf("NULL ");
		}
		else
		{
			printf("%d ", front->val);
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}
	QueueDestory(&q);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

4.1 バイナリツリーの作成と破棄

バイナリ ツリーを作成および破棄するには、次を使用します。バイナリツリートラバーサル例えば。

//二叉树的创建
struct TreeNode* Creat(char* arr,int n,int* i)
{ 
    if(*i<n&&arr[*i]=='#')
    {
        (*i)++;
        return NULL;
    }
    
    TreeNode* newnode=(TreeNode*)malloc(sizeof(TreeNode));
    newnode->left=NULL;
    newnode->right=NULL;

    newnode->val=arr[(*i)++];
    newnode->left=Creat(arr,n,i);
    newnode->right=Creat(arr,n,i);
    
    return newnode;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
//二叉树的销毁
void TreeDestroy(struct TreeNode* root)
{
    if(root==NULL)
    {
        return;
    }
    TreeDestroy(root->left);
    TreeDestroy(root->right);
    free(root);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4.3 二分木に対するその他の操作

以下の操作はすべてトラバーサルの考え方に沿って実行されます。

// 二叉树节点个数
int TreeSize(pTreeNode root)
{
	if (root == NULL)
	{
		return 0;
	}
	return TreeSize(root->left) + TreeSize(root->right) + 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
// 二叉树叶子节点个数
int TreeLeafSize(pTreeNode root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
// 二叉树第k层节点个数
int TreeLevelKSize(pTreeNode root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return TreeLevelKSize(root->left, k - 1) + TreeLevelKSize(root->right, k - 1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
// 二叉树查找值为x的节点
pTreeNode TreeFind(pTreeNode root, TreeDataType x)
{

	if (root == NULL)
	{
		return NULL;
	}
	//相等就返回
	if (root->val == x)
		return root;
	//找左子树
	pTreeNode left=TreeFind(root->left, x);
	if (left)
	{
		return left;
	}
	//找右子树
	pTreeNode right = TreeFind(root->right, x);
	if (right)
	{
		return right;
	}
	//都没找到
	return NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
//二叉树的高度
int TreeHeight(pTreeNode root)
{
	if (root == NULL)
	{
		return 0;
	}
	int max_left = TreeHeight(root->left) ;
	int max_right = TreeHeight(root->right);
	return max_left > max_right ? max_left + 1 : max_right + 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
//判断是否是完全二叉树
bool TreeComplete(pTreeNode root)
{
	Queue q;
	QueueInit(&q);
	if (root)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		pTreeNode front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
		else
		{
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}
	while (!QueueEmpty(&q))
	{
		pTreeNode front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
			QueueDestory(&q);
			return false;
		}
	}
	QueueDestory(&q);
	return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36