Implementieren eines binären Baums in Kotlin

Implementieren eines binären Baums in Kotlin

1. Überblick

In diesem Tutorial implementieren wir die grundlegenden Operationen für einen Binärbaum mithilfe der Programmiersprache Kotlin.

Fühlen Sie sich frei, unsere Java-Version diesertutorial zu überprüfen.

2. Definition

Bei der Programmierung abinary tree is a tree where every node has no more than two child nodes. Jeder Knoten enthält einige Daten, die wir als Schlüssel bezeichnen.

Beschränken wir unsere Betrachtung ohne Verlust der Allgemeinheit auf den Fall, dass die Schlüssel nur Ganzzahlen sind.

Also können wir einen rekursiven Datentyp definieren:

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

Dieser enthält einen Wert (das ganzzahlige Feldkey) und optionale Verweise auf ein linkes und ein rechtes Kind, die vom gleichen Typ wie das übergeordnete Element sind.

Wir sehen, dass aufgrund der verknüpften Natur der gesamte Binärbaum durch nur einen Knoten beschrieben werden kann, den wir als Wurzelknoten bezeichnen.

Interessanter wird es, wenn wir die Baumstruktur einschränken. In this tutorial, we suppose that the tree is an ordered binary tree (auch als binärer Suchbaum bezeichnet). Dies bedeutet, dass die Knoten in einer bestimmten Reihenfolge angeordnet sind.

Wir nehmen an, dass alle folgenden Bedingungen Teil derinvariant unseres Baums sind:

  1. Der Baum enthält keine doppelten Schlüssel

  2. Für jeden Knoten ist sein Schlüssel größer als die Schlüssel der linken Teilbaumknoten

  3. Für jeden Knoten ist sein Schlüssel kleiner als die Schlüssel seiner rechten Teilbaumknoten

3. Grundoperationen

Einige der häufigsten Operationen umfassen:

  • Eine Suche nach einem Knoten mit einem bestimmten Wert

  • Einfügen eines neuen Wertes

  • Entfernen eines vorhandenen Wertes

  • Und das Abrufen von Knoten in bestimmter Reihenfolge

3.1. Sieh nach oben

When the tree is ordered, the lookup process becomes very efficient: Wenn der zu suchende Wert dem Wert des aktuellen Knotens entspricht, ist die Suche beendet. Wenn der zu suchende Wert größer als der Wert des aktuellen Knotens ist, verwerfen wir möglicherweise den linken Teilbaum und betrachten nur den rechten:

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

Beachten Sie, dass der Wert möglicherweise nicht in den Schlüsseln des Baums vorhanden ist und das Ergebnis der Suche daher möglicherweise einen Wert vonnullzurückgibt.

Beachten Sie, wie wirKotlin keyword when verwendet haben, ein Java-Analogon der Anweisungswitch-case, aber viel leistungsfähiger und flexibler.

3.2. Einfügung

Da der Baum keine doppelten Schlüssel zulässt, ist es ganz einfach, einen neuen Wert einzufügen:

  1. Wenn der Wert bereits vorhanden ist, ist keine Aktion erforderlich

  2. Wenn der Wert nicht vorhanden ist, muss er in einen Knoten eingefügt werden, dessen linker oder rechter "Steckplatz" frei ist.

Daher können wir den Baum rekursiv analysieren, um einen Teilbaum zu suchen, der den Wert aufnehmen soll. Wenn der Wert kleiner als der Schlüssel des aktuellen Knotens ist, wählen Sie den linken Teilbaum aus, falls vorhanden. Wenn es nicht vorhanden ist, bedeutet dies, dass der Ort zum Einfügen des Werts gefunden wurde: Dies ist das linke untergeordnete Element des aktuellen Knotens.

Ebenso in dem Fall, in dem der Wert größer als der Schlüssel des aktuellen Knotens ist. Die einzige verbleibende Möglichkeit ist, wenn der Wert dem aktuellen Knotenschlüssel entspricht: Dies bedeutet, dass der Wert bereits im Baum vorhanden ist und wir nichts tun:

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. Entfernung

Zunächst sollten wir den Knoten identifizieren, der den angegebenen Wert enthält. Ähnlich wie bei der Suche scannen wir den Baum auf der Suche nach dem Knoten und behalten die Referenz zum übergeordneten Knoten des gesuchten Knotens bei:

fun delete(value: Int) {
    when {
        value > key -> scan(value, this.right, this)
        value < key -> scan(value, this.left, this)
        else -> removeNode(this, null)
    }
}

Es gibt drei verschiedene Fälle, die auftreten können, wenn ein Knoten aus einem Binärbaum entfernt wird. Wir klassifizieren sie basierend auf der Anzahl der nicht null untergeordneten Knoten.

Both child nodes are null Dieser Fall ist recht einfach zu behandeln und der einzige, in dem der Knoten möglicherweise nicht entfernt werden kann: Wenn es sich bei dem Knoten um einen Root-Knoten handelt, können wir ihn nicht beseitigen. Andernfalls reicht es aus, das entsprechende Kind des Elternteils aufnull zu setzen.

image

Die Implementierung dieses Ansatzes könnte folgendermaßen aussehen:

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 In diesem Fall sollten wir immer erfolgreich sein, da es ausreicht, den einzigen untergeordneten Knoten in den Knoten zu verschieben, den wir entfernen:

image

Wir können diesen Fall einfach implementieren:

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 Dieser Fall ist komplizierter, da wir einen Knoten finden sollten, der den Knoten ersetzen soll, den wir entfernen möchten. Eine Möglichkeit, diesen "Ersatz" -Knoten zu finden, besteht darin, einen Knoten im linken Teilbaum mit dem größten Schlüssel auszuwählen (er ist sicher vorhanden). Ein anderer Weg ist symmetrisch: Wir sollten einen Knoten im rechten Teilbaum mit dem kleinsten Schlüssel auswählen (es gibt ihn auch). Hier wählen wir den ersten aus:

image

Sobald der Ersatzknoten gefunden wurde, sollten wir den Verweis von seinem übergeordneten Knoten auf diesen zurücksetzen. Dies bedeutet, dass wir bei der Suche nach dem Ersatzknoten auch dessen Elternknoten zurückgeben sollten:

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. Traversal

Es gibt verschiedene Möglichkeiten, wie die Knoten besucht werden können. Most common are depth-first, breadth-first, and level-first search. Here, we consider only depth-first search, die von einer dieser Arten sein können:

  1. Vorbestellung (besuchen Sie den Elternknoten, dann das linke Kind, dann das rechte Kind)

  2. in-order (besuchen Sie das linke Kind, dann den Elternknoten, dann das rechte Kind)

  3. Nachbestellung (besuchen Sie das linke Kind, dann das rechte Kind, dann den Elternknoten)

In Kotlin können alle diese Arten der Durchquerung auf ganz einfache Weise durchgeführt werden. Zum Beispiel haben wir für die Vorbestellungsdurchquerung:

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

Beachten Sie, wie Kotlin es uns ermöglicht, Arrays mit dem Operator "+" zu verketten. Diese Implementierung ist alles andere als effizient: Sie ist nicht rekursiv, und für einen tieferen Baum kann die Stack-Überlauf-Ausnahme auftreten.

4. Fazit

In diesem Tutorial haben wir uns überlegt, wie grundlegende Operationen für einen binären Suchbaum mit der Sprache Kotlin erstellt und implementiert werden. Wir haben einige Kotlin-Konstrukte demonstriert, die in Java nicht vorhanden sind und die wir möglicherweise nützlich finden.

Wie immer ist die vollständige Implementierung der obigen Algorithmenover on Github verfügbar.