diff --git a/README.md b/README.md index df75cb9..4c35c68 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ * Binary Search Tree * AVL Tree * Red-Black Tree +* Two-Three Tree ## 🛠️ Quick Start diff --git a/lib/src/main/kotlin/monke/nodes/TwoThreeTreeNode.kt b/lib/src/main/kotlin/monke/nodes/TwoThreeTreeNode.kt new file mode 100644 index 0000000..e2224d4 --- /dev/null +++ b/lib/src/main/kotlin/monke/nodes/TwoThreeTreeNode.kt @@ -0,0 +1,15 @@ +package monke.nodes + +data class Entry, V>( + val key: K, + var value: V, +) + +public class TwoThreeTreeNode, V>( + val entries: MutableList> = mutableListOf(), + val children: MutableList> = mutableListOf(), + var parent: TwoThreeTreeNode? = null, +) { + val isLeaf: Boolean + get() = children.isEmpty() +} diff --git a/lib/src/main/kotlin/monke/trees/TwoThreeTree.kt b/lib/src/main/kotlin/monke/trees/TwoThreeTree.kt new file mode 100644 index 0000000..c1d7437 --- /dev/null +++ b/lib/src/main/kotlin/monke/trees/TwoThreeTree.kt @@ -0,0 +1,333 @@ +package monke.trees + +import monke.nodes.Entry +import monke.nodes.TwoThreeTreeNode +import monke.trees.treeInterfaces.BTree + +/** + * Implementation of a 2-3 Tree data structure. + * + * A 2-3 Tree is a balanced search tree where every internal node + * can contain either one key (2-node) or two keys (3-node), and + * accordingly has 2 or 3 children. The tree guarantees logarithmic + * time complexity for search, insertion, and deletion operations. + * + * @param K the type of keys, must implement [Comparable] + * @param V the type of values stored in the tree + */ +public class TwoThreeTree, V> : BTree { + /** + * Current root of the tree, or `null` if the tree is empty. + */ + protected var root: TwoThreeTreeNode? = null + + /** + * Number of key-value pairs currently stored in the tree. + */ + var size = 0 + private set + + /** + * Searches for a value by the given [key]. + * + * @param key the key to search for + * @return the value associated with the key, or `null` if not found + */ + override fun search(key: K): V? = getRecursive(root, key) + + /** + * Inserts a new key-value pair into the tree. + * + * If the key already exists, its value will be replaced. + * Splitting is performed automatically when nodes overflow. + * + * @param key the key to insert + * @param value the value to associate with the key + * @return the value that was inserted + */ + override fun insert( + key: K, + value: V, + ): V? { + if (root == null) { + root = TwoThreeTreeNode(entries = mutableListOf(Entry(key, value))) + size = 1 + return value + } + + var node = root + + while (!node!!.isLeaf) { + node = chooseChild(node, key) + } + + val exsistingIndex = node.entries.indexOfFirst { it.key == key } + if (exsistingIndex != -1) { + node.entries[exsistingIndex].value = value + return value + } + + insertEntryInNode(node, Entry(key, value)) + size++ + + var current = node + while (current != null && current.entries.size == 3) { + splitNode(current) + current = current.parent + } + return value + } + + /** + * Deletes a key (and its associated value) from the tree. + * + * Balancing is performed automatically (borrowing or merging) + * if a node underflows. + * + * @param key the key to delete + * @return the value that was removed, or `null` if the key was not found + */ + override fun delete(key: K): V? { + val node = findNode(root, key) ?: return null + + if (node.isLeaf) { + val oldValue = removeEntry(node, key) + fixUnderFlow(node) + size-- + return oldValue + } + + val index = node.entries.indexOfFirst { it.key == key } + val leftChild = node.children[index] + var replacementNode = leftChild + + while (!replacementNode.isLeaf) { + replacementNode = replacementNode.children.last() + } + + val replacementEntry = replacementNode.entries.last() + + node.entries[index] = Entry(replacementEntry.key, replacementEntry.value) + + removeEntry(replacementNode, replacementEntry.key) + fixUnderFlow(replacementNode) + size-- + + return replacementEntry.value + } + + private fun getRecursive( + node: TwoThreeTreeNode?, + key: K, + ): V? { + if (node == null) return null + + node.entries.find { it.key == key }?.let { return it.value } + + return if (node.isLeaf) null else getRecursive(chooseChild(node, key), key) + } + + private fun chooseChild( + node: TwoThreeTreeNode, + key: K, + ): TwoThreeTreeNode { + val listOfKey = node.entries + require(node.children.size == listOfKey.size + 1) { "Invalid children count" } + return when (listOfKey.size) { + 1 -> if (key < listOfKey[0].key) node.children[0] else node.children[1] + 2 -> + when { + key < listOfKey[0].key -> node.children[0] + key < listOfKey[1].key -> node.children[1] + else -> node.children[2] + } + else -> throw IllegalArgumentException("incorrect number of key in node") + } + } + + private fun insertEntryInNode( + node: TwoThreeTreeNode, + entry: Entry, + ) { + val index = node.entries.indexOfFirst { it.key > entry.key } + if (index == -1) { + node.entries.add(entry) + } else { + node.entries.add(index, entry) + } + } + + private fun splitNode(node: TwoThreeTreeNode) { + if (node.entries.size != 3) return + + val leftEntry = node.entries[0] + val middleEntry = node.entries[1] + val rightEntry = node.entries[2] + + val leftNode = TwoThreeTreeNode(entries = mutableListOf(leftEntry), parent = node.parent) + val rightNode = TwoThreeTreeNode(entries = mutableListOf(rightEntry), parent = node.parent) + + if (!node.isLeaf) { + val mid = node.children.size / 2 + leftNode.children.addAll(node.children.subList(0, mid)) + rightNode.children.addAll(node.children.subList(mid, node.children.size)) + leftNode.children.forEach { it.parent = leftNode } + rightNode.children.forEach { it.parent = rightNode } + } + + if (node.parent == null) { + val newRoot = TwoThreeTreeNode(entries = mutableListOf(middleEntry)) + newRoot.children.add(leftNode) + newRoot.children.add(rightNode) + leftNode.parent = newRoot + rightNode.parent = newRoot + root = newRoot + } else { + val parent = node.parent + val index = parent!!.entries.indexOfFirst { it.key > middleEntry.key } + if (index == -1) { + parent.entries.add(middleEntry) + } else { + parent.entries.add(index, middleEntry) + } + + val childIndex = parent.children.indexOf(node) + parent.children.removeAt(childIndex) + parent.children.add(childIndex, leftNode) + parent.children.add(childIndex + 1, rightNode) + } + } + + private fun findNode( + node: TwoThreeTreeNode?, + key: K, + ): TwoThreeTreeNode? { + var currentNode = node + + while (currentNode != null) { + currentNode.entries.find { it.key == key }?.let { return currentNode } + + if (currentNode.isLeaf) return null + + currentNode = chooseChild(currentNode, key) + } + return null + } + + private fun removeEntry( + node: TwoThreeTreeNode, + key: K, + ): V? { + val index = node.entries.indexOfFirst { it.key == key } + + if (index != -1) { + val entry = node.entries.removeAt(index) + return entry.value + } else { + return null + } + } + + private fun fixUnderFlow(node: TwoThreeTreeNode) { + if (node.entries.size >= 1) return + + val parent = + node.parent ?: run { + if (node.entries.isEmpty() && node.children.isNotEmpty()) { + root = node.children.first() + root?.parent = null + } + return + } + + val index = parent.children.indexOf(node) + val leftSibling = if (index > 0) parent.children[index - 1] else null + val rightSibling = if (index < parent.children.size - 1) parent.children[index + 1] else null + + if (leftSibling != null && leftSibling.entries.size > 1) { + borrowFromLeft(node, leftSibling, parent, index) + return + } + if (rightSibling != null && rightSibling.entries.size > 1) { + borrowFromRight(node, rightSibling, parent, index) + return + } + if (leftSibling != null) { + mergeWithLeft(node, leftSibling, parent, index) + } else if (rightSibling != null) { + mergeWithRight(node, rightSibling, parent, index) + } + } + + private fun borrowFromLeft( + node: TwoThreeTreeNode, + leftSibling: TwoThreeTreeNode, + parent: TwoThreeTreeNode, + index: Int, + ) { + val borrowEntry = leftSibling.entries.removeAt(leftSibling.entries.lastIndex) + val parentEntry = parent.entries[index - 1] + + node.entries.add(0, parentEntry) + parent.entries[index - 1] = borrowEntry + + if (leftSibling.children.isNotEmpty()) { + val child = leftSibling.children.removeAt(leftSibling.children.lastIndex) + node.children.add(0, child) + child.parent = node + } + } + + private fun borrowFromRight( + node: TwoThreeTreeNode, + rightSibling: TwoThreeTreeNode, + parent: TwoThreeTreeNode, + index: Int, + ) { + val borrowEntry = rightSibling.entries.removeAt(0) + val parentEntry = parent.entries[index] + + node.entries.add(0, parentEntry) + parent.entries[index] = borrowEntry + + if (rightSibling.children.isNotEmpty()) { + val child = rightSibling.children.removeAt(0) + node.children.add(child) + child.parent = node + } + } + + private fun mergeWithLeft( + node: TwoThreeTreeNode, + leftSibling: TwoThreeTreeNode, + parent: TwoThreeTreeNode, + index: Int, + ) { + leftSibling.entries.add(parent.entries[index - 1]) + leftSibling.entries.addAll(node.entries) + leftSibling.children.addAll(node.children) + node.children.forEach { it.parent = leftSibling } + + parent.entries.removeAt(index - 1) + parent.children.removeAt(index) + + fixUnderFlow(parent) + } + + private fun mergeWithRight( + node: TwoThreeTreeNode, + rightSibling: TwoThreeTreeNode, + parent: TwoThreeTreeNode, + index: Int, + ) { + node.entries.add(0, parent.entries[index]) + node.entries.addAll(rightSibling.entries) + node.children.addAll(rightSibling.children) + rightSibling.children.forEach { it.parent = node } + + parent.entries.removeAt(index) + parent.children.removeAt(index + 1) + + fixUnderFlow(parent) + } +} diff --git a/lib/src/main/kotlin/monke/trees/treeInterfaces/BTree.kt b/lib/src/main/kotlin/monke/trees/treeInterfaces/BTree.kt new file mode 100644 index 0000000..fdbc040 --- /dev/null +++ b/lib/src/main/kotlin/monke/trees/treeInterfaces/BTree.kt @@ -0,0 +1,12 @@ +package monke.trees.treeInterfaces + +interface BTree, V> { + fun search(key: K): V? + + fun insert( + key: K, + value: V, + ): V? + + fun delete(key: K): V? +} diff --git a/lib/src/test/kotlin/monke/trees/TwoThreeTreeTest.kt b/lib/src/test/kotlin/monke/trees/TwoThreeTreeTest.kt new file mode 100644 index 0000000..fdf9c0a --- /dev/null +++ b/lib/src/test/kotlin/monke/trees/TwoThreeTreeTest.kt @@ -0,0 +1,53 @@ +package monke.trees + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class TwoThreeTreeTest { + @Test + fun `insert and search single element`() { + val tree = TwoThreeTree() + assertNull(tree.search(10)) + + tree.insert(10, "a") + assertEquals("a", tree.search(10)) + assertEquals(1, tree.size) + } + + @Test + fun `insert multiple elements and preserve order`() { + val tree = TwoThreeTree() + val values = (1..10).shuffled() + values.forEach { tree.insert(it, "val$it") } + + (1..10).forEach { + assertEquals("val$it", tree.search(it)) + } + assertEquals(10, tree.size) + } + + @Test + fun `delete leaf element`() { + val tree = TwoThreeTree() + tree.insert(1, "one") + tree.insert(2, "two") + tree.insert(3, "three") + + val removed = tree.delete(3) + assertEquals("three", removed) + assertNull(tree.search(3)) + assertEquals(2, tree.size) + } + + @Test + fun `delete internal node element`() { + val tree = TwoThreeTree() + (1..5).forEach { tree.insert(it, "val$it") } + + val removed = tree.delete(3) + assertEquals("val3", removed) + assertNull(tree.search(3)) + assertEquals(4, tree.size) + } +}