1 はじめに
以前に、シーケンシャル リスト、リンク リスト、スタック、キューなど、データ構造の線形構造のいくつかの構造を学習しました。今日は、非線形データ構造ツリーを学習します。二分木はデータ構造の重要かつ難しいポイントであるため、この記事では、二分木の関連する概念と特性、および二分木の応用について紹介することに焦点を当てます。
2. ツリーの概念と構造
2.1 ツリーの概念
ツリーは非線形データ構造であり、n (n>=0) 個の有限ノードで構成される階層関係のセットです。根が上を向き、葉が下を向いている逆さまの木のように見えることから、ツリーと呼ばれます。
注:
1. ツリーは再帰的に定義されます
2. ツリー構造では、サブツリー間に交差があってはなりません。そうでなければ、ツリー構造ではありません
2.2 ツリーの関連概念
下の図に示すようなツリーがある場合、
次の概念があります。
ノードの次数: ノードに含まれるサブツリーの数は、ノードの次数と呼ばれます; 上図に示すように: A は 6 葉ノード
または終端ノードです: 次数 0 のノードは葉と呼ばれますノード; 上図に示すように: B, C, H, I... などのノードは葉ノード、
非終端ノードまたは枝ノードです: 次数が 0 ではないノード; 上図に示すように: 次のようなノードD、E、F、G... は分岐ノードです。
親ノードまたは親ノード: ノードに子ノードが含まれる場合、このノードはその子ノードの親ノードと呼ばれます。上の図に示すように、A はB 子ノード
または子ノードの親ノード : ノードに含まれるサブツリーのルート ノードは、ノードの子ノードと呼ばれます; 上図に示すように: B A の子ノードです.
兄弟ノード: ノード同じ親ノードを持つノードは兄弟ノードと呼ばれます; 上の図に示すように: B と C は
兄弟ノード ツリーの次数です: ツリーでは、最大のノードの次数はツリーの次数と呼ばれます; 上の図のように: ツリーのレベルは 6
ノードです: ルートの定義から始めて、ルートが最初のレベル、ルートの子ノードが 2 番目のレベル、というように、
ツリーの高さまたは深さ: 最大ツリー内のノードのレベル; 上図のように 図: ツリーの高さは 4
カズン ノード: 親が同じ層にある
ノードはカズンです ; 図: A はすべてのノードの祖先と子孫です
:サブツリー内の任意のノード特定のノードをルートとするものは、そのノードの子孫と呼ばれます。上の図に示すように: すべてのノードは A
フォレストの子孫です: m (m>0) 個の互いに素なツリーの集まりはフォレストと呼ばれます。
2.3 ツリー表現
ツリーを保存する際には、データとノード間の関係の両方を保存する必要があります.実際には、ツリーの表現方法には、親表現、子表現、子親表現、子兄弟表現などがあります.等 以下に、最も一般的に使用される子兄弟表記を紹介します。
typedef int TreeDataType;
struct TreeNode
{
TreeDataType data;//结点的数据域
struct TreeNode* FirstChild;//指向其第一个孩子结点
struct TreeNode* NextBrother;//指向其下一个兄弟结点
};
3. 二分木の概念
バイナリ ツリーはノードの有限セットです。セット:
1. または空
2. ルート ノードと、それぞれ左サブツリーおよび右サブツリーと呼ばれる 2 つのバイナリ ツリーで構成されます。
下の図に示すように、これは二分木です。
上図から、次のことがわかります。
1. 二分木には次数が 2 を超える節点はありません
2. 二分木の部分木は左右に分割され、順序を逆にすることはできないため、二分木は順序付き木です
3.1 特別な二分木
二分木には、次の 2 つの特殊な二分木もあります。
1.完全二分木: 二分木。各層のノード数が最大値に達すると、この二分木は完全二分木になります。つまり、バイナリ ツリーに K 層があり、ノードの総数が 2^k - 1 の場合、それは完全なバイナリ ツリーです。
2.完全二分木: 完全二分木は非常に効率的なデータ構造であり、完全二分木は完全二分木から派生します。深さが K でノードが n のバイナリ ツリーの場合、各ノードが、深さのあるフル バイナリ ツリーで 1 から n までの番号が付けられたノードと 1 対 1 で対応している場合にのみ、完全なバイナリ ツリーと呼ばれます。 Kの 完全二分木は特別な種類の完全二分木であることに注意してください。
3.2 二分木の性質
1. ルート ノードの層数が 1 に指定されている場合、空でない二分木の i 番目の層には最大で 2^(i-1) 個のノードがあります。 hの
二分木のノードの最大数は 2^h-1 です
。次数 2 の分岐ノードの数は n2 であり、n0 = n2 + 1
4. ルート ノードの層数が 1 として指定されている場合、n ノードの完全な二分木の深さ h = log2(n +1)
5. 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 の場合、あり権利のない子供
4. 二分木の逐次保存
順次構造ストレージは、ストレージに配列を使用することです. 一般に、配列は完全なバイナリ ツリーを表す場合にのみ適しています. 完全でないバイナリ ツリーはスペースを浪費するためです.
二分木順次記憶域は、物理的には配列であり、論理的には二分木です。
実際には、ストレージに配列を使用するのはヒープだけです。
4.1 ヒープの概念
すべての要素は、完全なバイナリ ツリーの順序で 1 次元配列に格納されます。ルート ノードが最大のヒープを最大ヒープまたは大ルート ヒープと呼び、ルート ノードが最小のヒープを最小ヒープまたは大ルート ヒープと呼びます。小さなルートヒープ。
ヒープ内のノードの値は常に、その親ノードの値よりも大きくも小さくもなりません。
ヒープは常に完全なバイナリ ツリーです。
4.2 ヒープの実装
4.2.1 ヒープノードの定義
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
4.2.2 ヒープの印刷と破棄
印刷:
void HeapPrint(HP* php)
{
assert(php);
int i = 0;
for (i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
破壊:
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
4.2.3 ヒープ挿入
最初に配列の末尾にデータを挿入し、大きなルート ヒープまたは小さなルート ヒープが満たされるまで上方調整アルゴリズムを実行します。
上方調整アルゴリズム: 例として小さなルート ヒープを取り、このノードから親ノードを見つけ始めます。ノードが親ノードよりも小さい場合は、ノードを親ノードと交換し、小さなルートまで上方に調整し続けます。満足のいくヒープです。
入れる:
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("malloc");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
上方調整のアルゴリズム:
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
交換:
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
4.2.4 ヒープの削除
ヒープの削除とは、ヒープの最上部のデータを削除し、ヒープの最上部のデータを最後のデータに置き換えてから、配列の最後のデータを削除してから、下方調整アルゴリズムを実行することです。
下方調整アルゴリズム: 例として、小さなルート ヒープを取り上げます。ノードから開始して子ノードを下方向に見つけます。子ノードがノードよりも小さい場合は、子ノードをノードと交換してから、下方向に調整を続けます。小さな根の山は満たされています
消去:
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
アルゴリズムを下に調整します。
void AdjustDown(HPDataType* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && (a[child + 1] < a[child]))
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
4.2.5 ヒープの先頭データを取得する
ヒープの一番上の要素は添字が 0 の要素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
4.2.6 ヒープエンプティ判定
サイズは0です空です
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
4.2.7 ヒープ内のデータ数
size のサイズはデータ数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
4.3 ヒープの適用
4.3.1 ヒープソート
ヒープソートとは、ヒープの考え方を利用してソートすることで、
1. ヒープを構築する
昇順: 大きなヒープを構築する
降順: 小さなヒープを構築する
2. ヒープを利用してアイデアを削除する選別
void HeapSort(int* arr, int size)
{
//排升序建大堆,排降序建小堆
//向上调整建堆O(N*logN)
//int i = 0;
//for (i = 1; i < size; i++)
//{
// AdjustUp(arr, i);
//}
//向下调整建堆O(N)
int i = 0;
for (i = (size - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, size, i);
}
int end = size - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
int main()
{
int arr[10] = {
23,45,48,123,12,49,80,15,5,35 };
HeapSort(arr, 10);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
4.3.2 TOP-K問題
TOP-K 問題: データの組み合わせで上位 K 個の最大要素または最小要素を見つける. 一般に、データ量は比較的多い.
例: トップ 10 のプロ プレーヤー、世界のトップ 500、リッチ リスト、ゲームでアクティブなプレーヤーのトップ 100 など。
アイデア:
1. データセットの最初の K 個の要素を使用してヒープを構築する.
最初の k 個の最大の要素を見つけてから、小さなヒープを構築する.
最初の k 個の最小の要素を見つけてから、大きなヒープを構築する
. 2. 残りの N 個の要素を使用する. - K 個の要素を順番にヒープの先頭要素と比較し、満たされない場合はヒープの先頭要素を置き換えます
残りの N - K 個の要素をヒープの先頭要素と順番に比較した後、ヒープの残りの K 個の要素検索された最初の K 個の最小または最大の要素です。
void PrintTopK(int* a, int n, int k)
{
//1.建堆--用a中前k个元素建堆
int* kMaxHeap = (int*)malloc(sizeof(int)*k);
assert(kMaxHeap);
int i = 0;
for (i = 0; i < k; i++)
{
kMaxHeap[i] = a[i];
}
for (i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kMaxHeap, k, i);
}
//2.将剩余n-k个元素依次与堆顶元素交换,不满则则替换
int j = 0;
for (j = k; j < n; j++)
{
if (a[j] > kMaxHeap[0])
{
kMaxHeap[0] = a[j];
AdjustDown(kMaxHeap, k, 0);
}
}
for (i = 0; i < k; i++)
{
printf("%d ", kMaxHeap[i]);
}
}
void TestTopk()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
srand(time(0));
for (int i = 0; i < n; i++)
{
a[i] = rand() % 1000000;
}
a[5] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[531] = 1000000 + 3;
a[5121] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2335] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[76] = 1000000 + 8;
a[423] = 1000000 + 9;
a[3144] = 1000000 + 10;
PrintTopK(a, n, 10);
}
int main()
{
TestTopk();
return 0;
}
5. 二分木の連鎖記憶
二分木の連結格納構造とは、二分木を表現するために連結リストを使用すること、つまり要素の論理関係を示すためにリンクを使用することを意味します。
通常の方法では、リンク リストの各ノードは、データ フィールドと左右のポインター フィールドの 3 つのフィールドで構成され、左右のポインターを使用して、左の子と右の子がリンク ポイントの格納アドレスを指定します。ノードの右の子が配置されます。
チェーン構造はさらに二股チェーンと三股チェーンに分けられます。
5.1 連鎖二分木のノード定義
typedef int BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;
5.2 ノードの作成
BTNode* CreatBTNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
assert(node);
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
バイナリ ツリーの作成はより複雑なので、単純なバイナリ ツリーを手動でシミュレートして、後続の操作の実装を容易にします。
5.3 二分木を作成するシミュレーション
BTNode* CreatBinaryTree()
{
BTNode* node1 = CreatBTNode(1);
BTNode* node2 = CreatBTNode(2);
BTNode* node3 = CreatBTNode(3);
BTNode* node4 = CreatBTNode(4);
BTNode* node5 = CreatBTNode(5);
BTNode* node6 = CreatBTNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
5.4 二分木トラバーサル
バイナリ ツリーのトラバーサルには以下が含まれます。 preorder/inorder/postorder/level order の再帰構造トラバーサル:
1. Preorder traversal (Preorder Traversal、preorder traversal とも呼ばれます) - ルート ノードを訪問する操作は、その左右をトラバースするときに発生します。サブツリー 前。
2. Inorder Traversal (Inorder Traversal) - ルート ノードを訪問する操作は、その左右のサブツリー (間) をトラバースする際に発生します。
3. ポストオーダー トラバーサル - ルート ノードを訪問する操作は、その左右のサブツリーをトラバースした後に発生します。
4. レベル トラバーサル (レベル トラバーサル) - 配置されているバイナリ ツリーのルート ノードから開始して、最初のレベルのルート ノードにアクセスし、次に左から右に 2 番目のレベルのノードにアクセスし、次に 3 番目のレベルのノードにアクセスします。 level 同様に、ツリーのノードを上から下へ、左から右へレイヤーごとに移動するプロセスは、レイヤー順トラバーサルです。
5.4.1 予約注文トラバーサル
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
5.4.2 インオーダー・トラバーサル
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
5.4.3 ポストオーダートラバーサル
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
5.4.4 レベル順トラバーサル
階層トラバーサルはもう少し複雑で、実装するためにキューが必要です. ここでは以前に作成したキューを使用します. ここではあまり表示しません. 核となるアイデアは: 最初にノードが空かどうかを判断し, 空でない場合はキューに入る. 、次に、チームが空であるかどうかを判断します。この時点で最初にチーム ヘッド要素を印刷しない場合は、チーム ヘッド要素を削除し、印刷されたチーム ヘッド要素の左の子と右の子が空であるかどうかを判断します。空でない場合は引き続きチームに参加します。このように、ループの反復はレイヤー シーケンスのトラバーサルです。
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
QueueDestroy(&q);
}
5.5 二分木のノード数や高さなどの操作
5.5.1 二分木のノード数
二分木の左部分木と右部分木は別々の二分木と見なすことができるため、中心となる考え方は、左部分木と右部分木のノード数を再帰的に計算し、最終的に戻り、ノードの数に 1 を追加することです。二分木番号。
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
5.5.2 二分木の葉ノード数
左右の部分木が両方とも空のノードが葉ノードです. ここでも再帰的思考を用いて二分木を左部分木と右部分木に分けます. 左部分木も左部分木と右部分木に分けることができます. 再帰計算と見つけられるツリー全体の葉ノードの数を返します。
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
5.5.3 二分木の第 k 層のノード数
ここでの考え方は、ルート ノードの k 番目の層のノードの数を見つけることは、ノードの左の子の k-1 層 + ノードの右の子の k 番目の層のノードの数を見つけることに変換できるということです。 - 再帰的に第 1 層のノード数と、ルート ノードの第 k 層のノード数は、終了後に返されます。
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k >= 1);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
5.5.4 二分木で値 x を持つノードを見つける
ここでの考え方は、ノードが必要なノードである場合はノードを直接返し、そうでない場合はノードの左側のサブツリーと右側のサブツリーを再帰的に検索し、見つかった場合は返すというものです。
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = BinaryTreeFind(root->right, x);
if (ret2)
{
return ret2;
}
return NULL;
}
5.5.5 二分木の深さの計算
バイナリ ツリーの深さは、バイナリ ツリーの左サブツリーと右サブツリーの最大の深さに 1 を加えたものです. ここでの考え方は、左サブツリーの深さと右サブツリーの深さを再帰的に計算することです. 各戻り値はあるノード上の二分木の深さをルートとし、最後に返ってくるのが二分木の深さです。
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int LeftDepth = BinaryTreeDepth(root->left);
int RightDepth = BinaryTreeDepth(root->right);
return LeftDepth > RightDepth ? LeftDepth + 1 : RightDepth + 1;
}
5.5.6 完全な二分木かどうかを判断する
ここでもキューが使用されます. 中心となる考え方は, 完全な二分木のプロパティを使用することです. 完全な二分木の空でないノードは一緒に接続する必要があります. 空のノードが表示されると, 空のノードの背後にあるすべてのノードは空でなければなりません. . まだ空でないノードがある場合、それは完全な二分木ではありません。
int BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
else
{
break;
}
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
6.エンディング
ここまで二分木の概念と二分木の列構造と連鎖構造の実装と応用について紹介してきました. 二分木はデータ構造の要点と難点であるため, 繰り返し学習し, 徹底的に理解する必要があります.みんなの理解力と基本的なスキル. 私も初心者のため, 必然的にこの記事には間違いや脱落がたくさんあります. この記事はあなたの学習と参考のためだけです. この記事が、皆さんが二分木を学ぶのに役立つなら、ブロガーは非常に光栄です。
最後に, 忍耐強い読書とサポートに感謝したいと思います. この記事がよく書かれていると思う友人は、フォローして3回サポートすることができます. この記事に質問がある場合や間違いがある場合は, 私にプライベートメッセージを送るか、コメント欄ディスカッションにメッセージを残してください。