Kotlinでのバイナリツリーの実装
1. 概要
このチュートリアルでは、Kotlinプログラミング言語を使用してバイナリツリーの基本的な操作を実装します。
これと同じtutorialのJavaバージョンを自由に確認してください。
2. 定義
プログラミングでは、binary tree is a tree where every node has no more than two child nodes.すべてのノードには、キーと呼ばれるデータが含まれています。
一般性を失うことなく、キーが単なる整数である場合に限定して検討しましょう。
そのため、再帰的なデータ型を定義できます。
class Node(
var key: Int,
var left: Node? = null,
var right: Node? = null)
これには、値(整数値フィールドkey)と、親と同じタイプの左右の子へのオプションの参照が含まれます。
リンクされた性質のため、ルートツリーと呼ばれる1つのノードだけでバイナリツリー全体を記述できることがわかります。
ツリー構造にいくつかの制限を適用すると、事態はさらに面白くなります。 In this tutorial, we suppose that the tree is an ordered binary tree(バイナリ検索ツリーとも呼ばれます)。 これは、ノードが何らかの順序で配置されることを意味します。
次の条件はすべて、ツリーのinvariantの一部であると想定しています。
-
ツリーに重複キーが含まれていない
-
すべてのノードについて、そのキーはその左側のサブツリーノードのキーよりも大きい
-
すべてのノードについて、そのキーは正しいサブツリーノードのキーよりも小さい
3. 基本操作
最も一般的な操作には次のものがあります。
-
指定された値を持つノードの検索
-
新しい値の挿入
-
既存の値の削除
-
特定の順序でのノードの取得
3.1. 見上げる
When the tree is ordered, the lookup process becomes very efficient:検索する値が現在のノードの値と等しい場合、ルックアップは終了します。検索する値が現在のノードの値よりも大きい場合、左側のサブツリーを破棄して、右側のサブツリーのみを検討する場合があります。
fun find(value: Int): Node? = when {
this.value > value -> left?.findByValue(value)
this.value < value -> right?.findByValue(value)
else -> this
}
ツリーのキーの間に値が存在しない可能性があるため、ルックアップの結果がnull値を返す可能性があることに注意してください。
switch-caseステートメントのJavaアナログであるが、はるかに強力で柔軟性のあるKotlin keyword whenをどのように使用したかに注意してください。
3.2. 挿入
ツリーは重複キーを許可しないため、新しい値を挿入するのは非常に簡単です。
-
値が既に存在する場合、アクションは不要です
-
値が存在しない場合、空いている左または右の「スロット」を持つノードに挿入されます
そのため、値を収容するサブツリーを検索するためにツリーを再帰的に解析できます。 値が現在のノードのキーより小さい場合、左のサブツリーがあればそれを選択します。 存在しない場合、値を挿入する場所が見つかったことを意味します。これは現在のノードの左の子です。
同様に、値が現在のノードのキーより大きい場合。 残りの唯一の可能性は、値が現在のノードキーと等しい場合です。つまり、値がツリーに既に存在し、何もしないということです。
fun insert(value: Int) {
if (value > this.key) {
if (this.right == null) {
this.right = Node(value)
} else {
this.right.insert(value)
}
} else if (value < this.key) {
if (this.left == null) {
this.left = Node(value)
} else {
this.left.insert(value)
}
}
3.3. 除去
まず、指定された値を含むノードを識別する必要があります。 検索プロセスと同様に、ノードを検索してツリーをスキャンし、検索されたノードの親への参照を維持します。
fun delete(value: Int) {
when {
value > key -> scan(value, this.right, this)
value < key -> scan(value, this.left, this)
else -> removeNode(this, null)
}
}
バイナリツリーからノードを削除するときに直面する可能性のある3つの異なるケースがあります。 null以外の子ノードの数に基づいて分類します.
Both child nodes are nullこのケースは処理が非常に簡単で、ノードの削除に失敗する可能性があるのはこのケースだけです。ノードがルートの場合、ノードを削除することはできません。 それ以外の場合は、親の対応する子をnullに設定するだけで十分です。
このアプローチの実装は次のようになります。
private fun removeNoChildNode(node: Node, parent: Node?) {
if (parent == null) {
throw IllegalStateException("Can not remove the root node without child nodes")
}
if (node == parent.left) {
parent.left = null
} else if (node == parent.right) {
parent.right = null
}
}
One child is null, the other is not nullこの場合、削除するノードに唯一の子ノードを「シフト」するだけで十分なので、常に成功するはずです。
このケースは簡単に実装できます。
private fun removeSingleChildNode(parent: Node, child: Node) {
parent.key = child.key
parent.left = child.left
parent.right = child.right
}
Both child nodes are not nullこのケースは、削除するノードを置き換えるノードを見つける必要があるため、より複雑です。 この「置換」ノードを見つける1つの方法は、左のサブツリーで最大のキーを持つノードを選択することです(確かに存在します)。 もう1つの方法は対称的な方法です。最小のキーを持つノードを右サブツリーから選択する必要があります(同様に存在します)。 ここでは、最初のものを選択します。
置換ノードが見つかったら、その親からの参照を「リセット」する必要があります。 これは、置換ノードを検索するときに、その親も返す必要があることを意味します。
private fun removeTwoChildNode(node: Node) {
val leftChild = node.left!!
leftChild.right?.let {
val maxParent = findParentOfMaxChild(leftChild)
maxParent.right?.let {
node.key = it.key
maxParent.right = null
} ?: throw IllegalStateException("Node with max child must have the right child!")
} ?: run {
node.key = leftChild.key
node.left = leftChild.left
}
}
3.4. トラバーサル
ノードにアクセスする方法にはさまざまな方法があります。 Most common are depth-first, breadth-first, and level-first search. Here, we consider only depth-first searchは、次のいずれかになります。
-
事前注文(親ノード、次に左の子、次に右の子にアクセス)
-
順序どおり(左の子、次に親ノード、次に右の子にアクセス)
-
post-order(左の子、次に右の子、次に親ノードにアクセス)
Kotlinでは、これらすべての種類のトラバーサルを簡単な方法で実行できます。 たとえば、事前注文のトラバーサルの場合、次のようになります。
fun visit(): Array {
val a = left?.visit() ?: emptyArray()
val b = right?.visit() ?: emptyArray()
return a + arrayOf(key) + b
}
Kotlinでは、「â」演算子を使用して配列を連結できることに注意してください。 この実装は効率的な実装とはほど遠いです。末尾再帰ではなく、より深いツリーではスタックオーバーフロー例外が発生する可能性があります。
4. 結論
このチュートリアルでは、Kotlin言語を使用してバイナリ検索ツリーの基本操作を構築および実装する方法を検討しました。 Javaには存在せず、有用であると思われるKotlinコンストラクトをいくつか示しました。
いつものように、上記のアルゴリズムの完全な実装はover on Githubで利用できます。