Kotlinで二分木を実装する

1.概要

このチュートリアルでは、Kotlinプログラミング言語を使用して、バイナリツリーの基本操作を実装します。

この同じhttps://www.baeldung.com/java-binary-tree[tutorial]の私たちのJavaバージョンをチェックしてください。

2.定義

プログラミングでは、 二分木とは、すべてのノードが子ノードを2つしか持たないツリーです。 すべてのノードには、キーと呼ばれるデータが含まれています。

一般性を失うことなく、キーが単に整数である場合に私たちの考慮を制限しましょう。

そのため、再帰的データ型を定義することができます。

class Node(
    var key: Int,
    var left: Node? = null,
    var right: Node? = null)

これには、値(整数値フィールド key )と、親と同じ型の左右の子へのオプションの参照が含まれています。

リンクされているため、バイナリツリー全体をルートノードと呼ぶ1つのノードで表すことができます。

ツリー構造にいくつかの制限を適用すると、状況はさらに面白くなります。 このチュートリアルでは、木が順序付き二分木 (二分探索木とも呼ばれる)であると仮定します。これは、ノードが何らかの順序で配置されていることを意味します。

次の条件はすべて私たちのツリーのhttps://en.wikipedia.org/wiki/Invariant (computer science)[invariant]の一部であると仮定します。

  1. ツリーに重複キーが含まれていません

  2. すべてのノードに対して、そのキーはその左のサブツリーのキーより大きい

ノード 。すべてのノードについて、そのキーは正しいサブツリーのキーよりも小さい

ノード

3.基本操作

最も一般的な操作には、次のものがあります。

  • 与えられた値を持つノードの検索

  • 新しい値の挿入

  • 既存の値の削除

  • そして一定の順序でノードを検索する

3.1. 見上げる

  • ツリーが順序付けられると、検索プロセスは非常に効率的になります**

検索する値が現在のノードの値と等しい場合、検索は終了します。検索する値が現在のノードの値よりも大きい場合は、左側のサブツリーを破棄して右側のサブツリーのみを考慮します。

fun find(value: Int): Node? = when {
    this.value > value -> left?.findByValue(value)
    this.value < value -> right?.findByValue(value)
    else -> this
}

値がツリーのキーの間に存在しない可能性があるため、ルックアップの結果が null 値を返すことがあります。

Kotlinキーワード when をどのように使用したかに注意してください。これは switch-case ステートメントのJavaアナログですが、はるかに強力で柔軟です。

3.2. 挿入

ツリーは重複キーを許可しないので、新しい値を挿入するのはとても簡単です。

  1. 値がすでに存在する場合は、対処は不要です.

  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でない子ノードの数に基づいて分類します

このケースは扱いが非常に簡単で、ノードを削除できない唯一のケースです。ノードがルートノードである場合、それを削除することはできません。それ以外の場合は、親の対応する子を null に設定すれば十分です。

リンク:/uploads/aa-100x39.png%20100w[]

このアプローチの実装は次のようになります。

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
    }
}

+この場合、唯一の子ノードを削除しているノードに「シフト」すれば十分であるため、常に成功するはずです。

リンク:/uploads/bb-100x77.png%20100w[]

私たちはこのケースを簡単に実装することができます。

private fun removeSingleChildNode(parent: Node, child: Node) {
    parent.key = child.key
    parent.left = child.left
    parent.right = child.right
}

削除したいノードを置き換えるノードを見つける必要があるため、このケースはより複雑です。この「置換」ノードを見つける1つの方法は、最大のキーを持つ左側のサブツリーのノードを選択することです(確実に存在します)。もう1つの方法は対称的な方法です。最小のキーを持つ正しいサブツリーのノードを選択する必要があります(それも存在します)。ここでは、最初のものを選びます。

リンク:/uploads/cc-100x29.png%20100w[]

置換ノードが見つかったら、その親からの参照を「リセット」する必要があります。つまり、置換ノードを検索するときには、その親も返す必要があります。

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. トラバーサル

ノードをどのように訪問することができるかについては様々な方法がある。 最も一般的なのは、深さ優先、幅優先、レベル優先の検索です。ここでは、深さ優先探索 のみを検討します。

  1. 事前注文(親ノード、次に左の子、次に右の順に移動します.

子) 。順番に(左の子、次に親のノード、そして右の順にアクセス

子) 。ポストオーダー(左の子、次に右の子、

親ノード)

Kotlinでは、これらすべての種類のトラバーサルは非常に簡単な方法で実行できます。たとえば、予約注文のトラバースの場合、次のようになります。

fun visit(): Array<Int> {
    val a = left?.visit() ?: emptyArray()
    val b = right?.visit() ?: emptyArray()
    return a + arrayOf(key) + b
}

Kotlinが "+"演算子を使用して配列を連結することを可能にする方法に注意してください。

この実装は効率的な実装とはほど遠いものです。末尾再帰ではなく、より深いツリーではスタックオーバーフロー例外が発生する可能性があります。

4.まとめ

このチュートリアルでは、Kotlin言語を使用して二分探索木の基本操作を構築し実装する方法を検討しました。私たちは、Javaには存在しない、そして有用であると思うかもしれないいくつかのKotlin構成体を示しました。

いつものように、上記のアルゴリズムの完全な実装はhttps://github.com/eugenp/tutorials/tree/master/core-kotlin/src/main/kotlin/com/baeldung/datastructures[Githubで入手可能]です。