目次
2. バイナリ ツリー トラバーサル: 深さが優先、幅が優先
1. 二分木とは何ですか?
バイナリ ツリーは、各ノードが左の子と右の子と呼ばれる最大 2 つの子ノードを持つことができるツリー データ構造です。// 時間計算量はツリーの深さに関係します
以下は、整数データを含むツリー ノードの例です。
class Node {
int key;
Node left, right;
public Node(int item)
{
key = item;
left = right = null;
}
}
Java コードを使用してバイナリ ツリーを構築します。
public class BinaryTree {
// Root of Binary Tree
Node root;
// Constructors
BinaryTree(int key) {
root = new Node(key);
}
BinaryTree() {
root = null;
}
public static void main(String[] args) {
BinaryTree tree = new BinaryTree();
// 创建根节点
tree.root = new Node(1);
/* 下面是上述语句后的树
1
/ \
null null
*/
tree.root.left = new Node(2);
tree.root.right = new Node(3);
/* 2和3是1的左右子结点
1
/ \
2 3
/ \ / \
null null null null */
tree.root.left.left = new Node(4);
/* 4变成了2的左子结点
1
/ \
2 3
/ \ / \
4 null null null
/ \
null null
*/
}
private static class Node {
int key;
Node left, right;
public Node(int item) {
key = item;
left = right = null;
}
}
}
2. バイナリ ツリー トラバーサル: 深さが優先、幅が優先
走査するための論理的な方法が 1 つしかない線形データ構造 (配列、リンク リスト、キュー、スタックなど) とは異なり、ツリーはさまざまな方法で走査できます。
(1) 深さ優先探索(DFS)アルゴリズム
深さ優先トラバーサル (深さ優先トラバーサル) は、バイナリ ツリーのトラバーサル手法です。その中心的な考え方は、バイナリ ツリーのノードを、それ以上深くできなくなるまでできるだけ深く訪問し、その後、前のノードに戻ることです。横断を続けること。深さ優先トラバーサルには、事前順序トラバーサル、順序内トラバーサル、事後順序トラバーサルの 3 つの一般的な方法があります。// 重要なのは、最初にルート ノードにアクセスするか、後でルート ノードにアクセスすることです。
1) プリオーダー・トラバーサル(プリオーダー・トラバーサル)
事前順序走査のルールは、最初にルート ノードにアクセスし、次に左と右のサブツリーを左から右の順序で再帰的に走査することです。事前順序トラバーサルの順序は、ルート ノード -> 左サブツリー -> 右サブツリーです。
事前注文トラバーサルのコード実装:
public class BinaryTree {
// 根节点
Node root;
BinaryTree() {
root = null;
}
// 给定一棵二叉树,按顺序打印它的节点
void printPreorder(Node node) {
if (node == null) {
return;
}
// 打印当前节点的数据
System.out.print(node.key + " ");
// 遍历左子树
printPreorder(node.left);
// 遍历右子树
printPreorder(node.right);
}
// Driver code
public static void main(String[] args) {
BinaryTree tree = new BinaryTree();
tree.root = new Node(1);
tree.root.left = new Node(2);
tree.root.right = new Node(3);
tree.root.left.left = new Node(4);
tree.root.left.right = new Node(5);
// Function call
System.out.println("traversal of binary tree is ");
tree.printPreorder(tree.root);
}
private static class Node {
int key;
Node left, right;
public Node(int item) {
key = item;
left = right = null;
}
}
}
//输出
traversal of binary tree is
1 2 4 5 3
2) インオーダートラバーサル(Inorder Traversal)
順序トラバーサルのルールは、最初に左のサブツリーを左から右の順序で再帰的にトラバースし、次にルート ノードにアクセスし、最後に右のサブツリーを再帰的にトラバースすることです。インオーダートラバーサルの順序は、左サブツリー -> ルートノード -> 右サブツリーです。
インオーダートラバーサルは値の昇順でノードをトラバースするため、インオーダートラバーサルは二分探索ツリーに特に重要な用途があります。
インオーダートラバーサルのコード実装: // 上記のコードでトラバーサル順序を調整するだけです
// 给定一棵二叉树,按顺序打印它的节点
void printInorder(Node node) {
if (node == null) {
return;
}
// 遍历左子树
printInorder(node.left);
// 打印当前节点的数据
System.out.print(node.key + " ");
// 遍历右子树
printInorder(node.right);
}
//输出
traversal of binary tree is
4 2 5 1 3
3) 事後トラバーサル
事後探索のルールは、左のサブツリーと右のサブツリーを左から右の順序で再帰的に探索し、ルート ノードにアクセスすることです。事後探索の順序は、左サブツリー -> 右サブツリー -> ルート ノードです。
ポストオーダートラバーサルのコード実装: // 上記のコードでトラバーサル順序を調整するだけです
// 给定一棵二叉树,按顺序打印它的节点
void printPostorder(Node node) {
if (node == null) {
return;
}
// 遍历左子树
printPostorder(node.left);
// 遍历右子树
printPostorder(node.right);
// 打印当前节点的数据
System.out.print(node.key + " ");
}
//输出
traversal of binary tree is
4 5 2 3 1
(2) 幅優先探索 (BFS) アルゴリズム
レベル順序トラバーサルとも呼ばれるバイナリ ツリーの幅優先トラバーサルは、階層順序に従ってバイナリ ツリーのノードをレイヤごとに訪問するトラバーサル方法です。ルート ノードから始めて、すべてのノードが移動されるまで、左から右の順に各レイヤーのノードを参照します。// レイヤーごとに読み込みます
レイヤー順序トラバーサルのコード実装:
public class BinaryTree {
// 根节点
Node root;
BinaryTree() {
root = null;
}
/*层序遍历*/
void printLevelOrder() {
int h = height(root);
int i;
for (i = 1; i <= h; i++) {
printCurrentLevel(root, i);
}
}
/* 计算树的高度 -- 从根节点开始沿着最长路径的节点一直到最远的叶节点.*/
int height(Node root) {
if (root == null) {
return 0;
} else {
/* 计算每个子树的高度 */
int lheight = height(root.left);
int rheight = height(root.right);
/* use the larger one */
if (lheight > rheight) {
return (lheight + 1);
} else {
return (rheight + 1);
}
}
}
/* Print nodes at the current level */
void printCurrentLevel(Node root, int level) {
if (root == null) {
return;
}
if (level == 1) {
System.out.print(root.key + " ");
} else if (level > 1) {
printCurrentLevel(root.left, level - 1);
printCurrentLevel(root.right, level - 1);
}
}
// Driver code
public static void main(String[] args) {
BinaryTree tree = new BinaryTree();
tree.root = new Node(1);
tree.root.left = new Node(2);
tree.root.right = new Node(3);
tree.root.left.left = new Node(4);
tree.root.left.right = new Node(5);
// Function call
System.out.println("traversal of binary tree is ");
tree.printLevelOrder();
}
private static class Node {
int key;
Node left, right;
public Node(int item) {
key = item;
left = right = null;
}
}
}
//输出
traversal of binary tree is
1 2 3 4 5
4 つの走査メソッドは次のツリーを走査します。
- 事前注文トラバーサル結果: 1-2-4-5-3-6-7
- 順序トラバーサルの結果: 4-2-5-1-6-3-7
- 事後走査結果: 4-5-2-6-7-1
- レイヤー シーケンスのトラバーサル結果: 1-2-3-4-5-6-7
3. 二分木の性質の詳しい説明
バイナリ ツリーの例の図: //バイナリ ツリーには要素のサイズの順序はなく、子ノードの数が 2 であることだけが規定されていることに注意してください。
(1) 性質 1: 二分木 i 層には最大 2^(i-1) (i≥1) 個のノードが存在します。// 各レイヤーのノード
- 最初の層 (ルート ノード) の場合、層には最大 2^(1-1)、つまり 2^0 = 1 個のノードがあります。
- レイヤ 3 の場合、レイヤには最大 2^(3-1)、つまり 2^2 = 4 個のノードがあります。
(2) 特性 2 :深さ h の二分木には最大 2^h - 1 個のノードが含まれます。// ツリー全体のノード
- ツリーの深さが 1 (ルート ノードのみ) の場合、ツリー全体には最大 2^1-1、つまり 2 - 1 = 1 ノードがあります。
- ツリーの深さが 3 の場合、ツリー全体には最大 2^3 - 1、つまり 8 - 1 = 7 個のノードがあります。
(3) 性質 3: 二分木に n0 個の葉ノードと次数 2 の n2 個のノードがある場合、n0 = n2 + 1 が存在する必要があります。
たとえば、バイナリ ツリーの例の図では、図の葉ノードの数はそれぞれ 4 (子ノードのないノード) : 2、5、11、4、つまりn0 = 4、次数 2 のノードが 3 つあります。それぞれ : 2, 7, 6 ; その場合、常に次のようになります: n0 = n2 + 1、上記のバイナリ ツリーでは、つまり: 3 + 1 = 4 // これは、葉ノードの数が以前のすべての親ノードの数の合計 + 1 (完全なバイナリ ツリー)
// 補足:二分木の次数とは何ですか?
二分木の次数は、ツリー内のすべてのノードの次数の最大値です。次数 1 は、子ノードが 1 つだけ存在するか、サブツリーが 1 つであることを意味し、次数 2 は、子ノードが 2 つあるか、左右両方のサブツリーがあることを意味します。バイナリ ツリーの次数は以下です。 2。これは、バイナリ ツリーの定義では、バイナリ ツリー内のノードの次数 (ノードの分岐数) が 2 以下である必要があるためです。// 最大ノード数
(4) 特性 4: n 個のノードを持つ完全なバイナリ ツリーの深さは [log(2)n] + 1 です。([ ]は切り捨てを意味します)
以下の図に示すように、完全なバイナリ ツリーには 15 個のノードがあり、[log(2) 15] = 3 となり、このバイナリ ツリーの深さは 3 + 1 = 4 となります。
log(2)n の計算方法の例: 2^4 = 16 より、log(2) 16 = 4。
// 補足:完全二分木、完全二分木とは何ですか?
完全なバイナリ ツリー:子ノードのない最後のレベルを除き、各レベルのすべてのノードに 2 つの子ノードがあるバイナリ ツリー。
完全な二分木: k および n ノードの深さを持つ二分木。ツリー内のノードには上から下、左から右に番号が付けられます。番号が i (1≤i≤n) の場合、二分木のノード番号iが完全二分木のノード番号iと同じである場合、この二分木を完全二分木と呼びます。// 完全なバイナリ ツリーの小さいバージョン
(5) 特性 5 : n 個のノードを持つ完全な二分木に連続番号が付けられている場合 (1≤i≤n)、ノード番号が i (i≧1) の場合、次のようになります。
- i = 1 の場合、ノードはルートであり、親ノードはありません。
- i > 1 の場合、ノード i の親ノード番号は int_down(i/2) // int_down は切り捨てを意味します。たとえば、5 の親ノードは 2 -> int_down( 5 /2) = 2
- 2i ≤ n の場合、2i という番号が付けられた左ノードが存在します。それ以外の場合、左ノードは存在しません。// 以下の図のノード 2、4、および 6
- 2i + 1 ≤ n の場合、2i + 1 の番号が付けられた右側のノードが存在します。それ以外の場合、右側のノードは存在しません。// 以下の図のノード 3 と 5
4. 二分木の種類
(1) 完全なバイナリツリー
フル バイナリ ツリー (フル バイナリ ツリー) は、厳密なバイナリ ツリーとも呼ばれ、特別な種類のバイナリ ツリーです。完全なバイナリ ツリーでは、リーフ ノードを除き、各ノードには 2 つの子ノードがあり、すべてのリーフ ノードは同じレベルに配置されます。
完全なバイナリ ツリーには次の特性があります。
- すべての非リーフ ノードには 2 つの子があります。
- すべてのリーフ ノードは同じレベルにあります。
- 深さ h の完全なバイナリ ツリーには、合計 2^h - 1 個のノードがあります。
完全なバイナリ ツリーの構造は次のとおりです。
(2) 完全な二分木
完全な二分木 (完全な二分木) は特殊な二分木構造です。完全な二分木では、最後の層の葉ノードがいっぱいではないことを除いて、他の層のノードはいっぱいで、最後の層の葉ノードはレイヤーはすべて連続している ソート左。
二分木の深さをhとすると、h番目の層を除いて各層のノード数は最大数に達し、h番目の層の全てのノードが引き続き左端に集中する。
以下は完全なバイナリ ツリーの構造を示しています。
完全なバイナリ ツリーは、完全なバイナリ ツリーの特別な形式です。バイナリ ツリーが完全なバイナリ ツリーである場合、それは完全なバイナリ ツリーでなければなりません。
(3) 二分探索木 / 二分探索木
二分探索ツリー (BST) は順序付き二分木のデータ構造であり、どのノードでも、その左側のサブツリーのノード値はそのノードの値より小さく、右側のサブツリーのノード値はすべてノードの値より大きい。つまり、二分探索木は次の特性を満たします。
任意のノード N について:
- その左側のサブツリー内のすべてのノードの値が N の値より小さいです。
- その右側のサブツリー内のすべてのノードの値が N の値より大きくなります。
- その左右のサブツリーも二分探索ツリーです。
このプロパティにより、二分探索ツリーは次の特性を持つようになります。
- 二分探索木では、左側のサブツリーのノード値はルート ノードの値より小さく、右側のサブツリーのノード値はルート ノードの値より大きいため、特定の値を見つける操作は二分探索木はノード値を比較することで決定できます。二分探索を実行して検索効率を向上させます。
- 二分探索ツリーの順序トラバーサルの結果は、昇順に並べられます。
二分探索木の構造は次のとおりです。
二分探索ツリーの順序付けされた性質により、効率的なデータの保存および検索構造として使用できます。たとえば、データベースでは、二分探索ツリーを使用すると、検索、挿入、削除の操作が高速化され、データ アクセスの効率が向上します。// 二分探索については後ほど詳しく紹介します
(4) バランスの取れた二分木
Balanced Binary Tree (Balanced Binary Tree) は特殊なバイナリ ツリー構造であり、その目的は、ノードの挿入および削除時にツリーのバランスを維持し、検索、挿入、削除などの操作の効率を向上させることです。// 主に、バイナリ ツリーがリンク リストに劣化するのを防ぐためにツリーの高さを減らすためです。
バランスの取れた二分木には次の特性があります。
- どのノードでも、左のサブツリーと右のサブツリーの高さの差は 1 を超えません。つまり、左のサブツリーと右のサブツリーの高さの差の絶対値は 1 を超えません。
- 各サブツリーはバランスのとれた二分木です。
バランスの取れたバイナリ ツリーの一般的な実装には、AVL ツリーと赤黒ツリーが含まれます。これらのツリーは、ノードの挿入または削除時に自動的にバランス操作を実行して、ツリーのバランスを維持します。バランス操作には、左回転、右回転、二重回転などが含まれます。ノードの位置とバランス係数を調整することで、木の高さの差が許容範囲内に保たれます。
バランスの取れたバイナリ ツリーのバランスの取れた性質により、ツリーの高さが低くなり、検索、挿入、削除などの操作の時間計算量が O(log n) レベルに保たれます。不平衡二分探索木と比較して、平衡二分木は大規模なデータセットや頻繁な動的操作シナリオでのパフォーマンスが優れています。
ただし、バランスの取れたバイナリ ツリーの維持には追加のコストがかかります。たとえば、ノードの挿入および削除時のバランス操作は非常に複雑になる可能性があります。したがって、一部の特定のアプリケーション シナリオでは、パフォーマンスと複雑さの要件のバランスをとるために、 B ツリーやスキップ リストなど、他のより適切なデータ構造が選択される場合があります。
AVL ツリー の構造は次のとおりです: // AVL ツリーについては後ほど別の記事で詳しく紹介しますので、ここでは概要のみを説明します。
赤黒ツリー の構造は以下のとおりです: // AVL ツリーについては後ほど他の記事で詳しく紹介しますので、ここでは紹介のみです
赤黒ツリーは平衡二分探索ツリーの変形です。その左右のサブツリーの高さの差は 1 より大きい場合があるため、赤黒ツリーは厳密な意味では平衡二分木 (AVL) ではありません。ただし、バランスを取るためのコストは比較的低く、平均的な統計パフォーマンスは AVL ツリーよりも優れています。
重要なプロパティ: 任意のノードからその各リーフへのすべてのパスには、同じ数の黒いノードが含まれます。// 黒ノードのバランス
(5) 線分ツリー
線分木、各単位区間は線分木の葉ノードに相当します。線分ツリーを使用すると、複数の線分に特定のノードが出現する回数を迅速に求めることができ、時間計算量は O(logN) です。
線分ツリーは、各区間 [L, R] を [L, M] と [M+1, R] に分解します (ただし、M = (L+R) / 2 ここでの除算は整数除算、つまり、結果は全体として取得されます)、L == R まで。
最初は [1,n] の区間であり、再帰によって段階的に分解され、ルートの高さを 1 とすると、木の最大の高さは (n>1) になります。
線分ツリーは n 個の分解ごとに一意であるため、同じ n の線分ツリーは同じ構造を持ち、これが永続的な線分ツリーを実現するための基礎でもあります。
次の図は、区間 [1,13] の分解プロセスを示しています。
上の図では、各区間がノードであり、各ノードには対応する区間の統計情報が格納されます。
要約:
線分ツリーの中心的な考え方は、特定の間隔を複数の小さな部分間隔に分割し、各ノードで間隔に関連する情報を維持することです。通常、線分ツリーは平衡二分木であり、各リーフ ノードは元のデータの個々の要素を表し、他のノードは対応する間隔の集約情報を表します。
線分ツリーの主な利点は、間隔クエリと更新操作を O(log n) の時間計算量で実行できることです。線分ツリーの特殊な構造を利用することで、関係する区間を迅速に特定し、操作することができます。線分ツリーは、間隔最大値クエリ、間隔変更、間隔カバレッジなど、さまざまな間隔クエリの問題を解決するためによく使用されます。
線分ツリーに関する推奨記事については、「線分ツリー」をクリックしてください。