Javaでのバイナリツリーの実装
1. 前書き
この記事では、Javaでのバイナリツリーの実装について説明します。
この記事のために、we’ll use a sorted binary tree that will contain int values。
2. 二分木
二分木は、各ノードが最大2つの子を持つことができる再帰的なデータ構造です。
一般的なタイプのバイナリツリーはバイナリ検索ツリーです。すべてのノードの値は、左のサブツリーのノード値以上で、右のサブツリーのノード値以下です。木。
このタイプの二分木の簡単な視覚的表現は次のとおりです。
実装では、int値を格納し、各子への参照を保持する補助Nodeクラスを使用します。
class Node {
int value;
Node left;
Node right;
Node(int value) {
this.value = value;
right = null;
left = null;
}
}
次に、通常はroot:と呼ばれるツリーの開始ノードを追加しましょう。
public class BinaryTree {
Node root;
// ...
}
3. 一般的な操作
それでは、二分木で実行できる最も一般的な操作を見てみましょう。
3.1. 要素を挿入する
ここで取り上げる最初の操作は、新しいノードの挿入です。
まず、we have to find the place where we want to add a new node in order to keep the tree sorted。 ルートノードから始めて、次のルールに従います。
-
新しいノードの値が現在のノードの値よりも低い場合は、左の子に移動します
-
新しいノードの値が現在のノードの値よりも大きい場合は、適切な子に移動します
-
現在のノードがnull,の場合、リーフノードに到達し、その位置に新しいノードを挿入できます。
まず、挿入を行うための再帰的なメソッドを作成します。
private Node addRecursive(Node current, int value) {
if (current == null) {
return new Node(value);
}
if (value < current.value) {
current.left = addRecursive(current.left, value);
} else if (value > current.value) {
current.right = addRecursive(current.right, value);
} else {
// value already exists
return current;
}
return current;
}
次に、rootノードから再帰を開始するパブリックメソッドを作成します。
public void add(int value) {
root = addRecursive(root, value);
}
次に、このメソッドを使用して、例からツリーを作成する方法を見てみましょう。
private BinaryTree createBinaryTree() {
BinaryTree bt = new BinaryTree();
bt.add(6);
bt.add(4);
bt.add(8);
bt.add(3);
bt.add(5);
bt.add(7);
bt.add(9);
return bt;
}
3.2. 要素を見つける
次に、ツリーに特定の値が含まれているかどうかを確認するメソッドを追加しましょう。
前と同じように、最初にツリーをトラバースする再帰メソッドを作成します。
private boolean containsNodeRecursive(Node current, int value) {
if (current == null) {
return false;
}
if (value == current.value) {
return true;
}
return value < current.value
? containsNodeRecursive(current.left, value)
: containsNodeRecursive(current.right, value);
}
ここでは、現在のノードの値と比較して値を検索し、それに応じて左または右の子で続行します。
次に、rootから始まるパブリックメソッドを作成しましょう。
public boolean containsNode(int value) {
return containsNodeRecursive(root, value);
}
次に、ツリーに挿入された要素が実際に含まれていることを確認する簡単なテストを作成しましょう。
@Test
public void givenABinaryTree_WhenAddingElements_ThenTreeContainsThoseElements() {
BinaryTree bt = createBinaryTree();
assertTrue(bt.containsNode(6));
assertTrue(bt.containsNode(4));
assertFalse(bt.containsNode(1));
}
追加されたすべてのノードがツリーに含まれている必要があります。
3.3. 要素の削除
別の一般的な操作は、ツリーからのノードの削除です。
まず、以前と同様の方法で削除するノードを見つける必要があります。
private Node deleteRecursive(Node current, int value) {
if (current == null) {
return null;
}
if (value == current.value) {
// Node to delete found
// ... code to delete the node will go here
}
if (value < current.value) {
current.left = deleteRecursive(current.left, value);
return current;
}
current.right = deleteRecursive(current.right, value);
return current;
}
削除するノードを見つけたら、主に3つの異なるケースがあります。
-
a node has no children –これは最も単純なケースです。このノードを親ノードのnullに置き換える必要があります
-
親ノードのa node has exactly one child –で、このノードをその唯一の子に置き換えます。
-
a node has two children –ツリーの再編成が必要なため、これは最も複雑なケースです。
ノードがリーフノードである場合の最初のケースを実装する方法を見てみましょう。
if (current.left == null && current.right == null) {
return null;
}
次に、ノードに子が1つある場合を続けましょう。
if (current.right == null) {
return current.left;
}
if (current.left == null) {
return current.right;
}
ここでは、non-nullの子を返しているので、親ノードに割り当てることができます。
最後に、ノードに2つの子がある場合を処理する必要があります。
最初に、削除されたノードを置き換えるノードを見つける必要があります。 削除するノードの右側のサブツリーの最小のノードを使用します。
private int findSmallestValue(Node root) {
return root.left == null ? root.value : findSmallestValue(root.left);
}
次に、削除するノードに最小値を割り当て、その後、右側のサブツリーから削除します。
int smallestValue = findSmallestValue(current.right);
current.value = smallestValue;
current.right = deleteRecursive(current.right, smallestValue);
return current;
最後に、rootからの削除を開始するパブリックメソッドを作成しましょう。
public void delete(int value) {
root = deleteRecursive(root, value);
}
それでは、削除が期待どおりに機能することを確認しましょう。
@Test
public void givenABinaryTree_WhenDeletingElements_ThenTreeDoesNotContainThoseElements() {
BinaryTree bt = createBinaryTree();
assertTrue(bt.containsNode(9));
bt.delete(9);
assertFalse(bt.containsNode(9));
}
4. ツリーを横断する
このセクションでは、深さ優先検索と幅優先検索について詳しく説明し、ツリーをトラバースするさまざまな方法を説明します。
以前に使用したものと同じツリーを使用し、各ケースの走査順序を示します。
4.1. 深さ優先検索
深さ優先探索は、次の兄弟を探索する前に、すべての子供を可能な限り深く探索するタイプのトラバーサルです。
深さ優先検索を実行する方法はいくつかあります。順序どおり、事前順序、および順序後です。
インオーダートラバーサルは、最初に左側のサブツリー、次にルートノード、最後に右側のサブツリーにアクセスすることで構成されます。
public void traverseInOrder(Node node) {
if (node != null) {
traverseInOrder(node.left);
System.out.print(" " + node.value);
traverseInOrder(node.right);
}
}
このメソッドを呼び出すと、コンソール出力に順序通りのトラバーサルが表示されます。
3 4 5 6 7 8 9
プレオーダートラバーサルは、最初にルートノード、次に左側のサブツリー、最後に右側のサブツリーにアクセスします。
public void traversePreOrder(Node node) {
if (node != null) {
System.out.print(" " + node.value);
traversePreOrder(node.left);
traversePreOrder(node.right);
}
}
そして、コンソール出力でプレオーダートラバーサルを確認しましょう。
6 4 3 5 8 7 9
ポストオーダートラバーサルは、左側のサブツリー、右側のサブツリー、および最後のルートノードにアクセスします。
public void traversePostOrder(Node node) {
if (node != null) {
traversePostOrder(node.left);
traversePostOrder(node.right);
System.out.print(" " + node.value);
}
}
ポストオーダーのノードは次のとおりです。
3 5 4 7 9 8 6
4.2. 幅優先検索
これは、visits all the nodes of a level before going to the next levelのもう1つの一般的なタイプのトラバーサルです。
この種のトラバーサルはレベル順序とも呼ばれ、ルートから左から右へツリーのすべてのレベルを訪問します。
実装では、Queueを使用して、各レベルのノードを順番に保持します。 リストから各ノードを抽出し、その値を出力してから、その子をキューに追加します。
public void traverseLevelOrder() {
if (root == null) {
return;
}
Queue nodes = new LinkedList<>();
nodes.add(root);
while (!nodes.isEmpty()) {
Node node = nodes.remove();
System.out.print(" " + node.value);
if (node.left != null) {
nodes.add(node.left);
}
if (node.right!= null) {
nodes.add(node.right);
}
}
}
この場合、ノードの順序は次のようになります。
6 4 8 3 5 7 9
5. 結論
この記事では、ソートされたバイナリツリーをJavaで実装する方法とその最も一般的な操作について説明しました。
例の完全なソースコードは、over on GitHubで入手できます。