diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..21fcfb0 --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,29 @@ +version: 2 +mergeable: + - when: pull_request.*, pull_request_review.* + filter: + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none + validate: + - do: title + no_empty: + enabled: true + message: "The title should not be empty." + begins_with: + match: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] + message: "The name must begin with a capital letter." + + - do: description + no_empty: + enabled: true + message: "The description should not be empty." + + - do: approvals + min: + count: 1 + required: + assignees: true \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8b4e9bd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Test with coverage +on: + push: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout project sources + uses: actions/checkout@v2 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-version: current + gradle-home-cache-cleanup: true + - name: Run test + run: | + ./gradlew clean + ./gradlew test + - name: JaCoCo Coverage Report + env: + report_path: BinarySearchTrees/build/jacoco/report.csv + run: | + awk -F"," '{ instructions += $4 + $5; covered += $5; branches += $6 + $7; branches_covered +=$7 } END { print "Instructions covered:", covered"/"instructions, "--", 100*covered/instructions"%"; print "Branches covered:", branches_covered"/"branches, "--", 100*branches_covered/branches"%" }' $report_path + - uses: actions/upload-artifact@v3 + with: + name: binarysearchtree-test-and-coverage-reports + path: | + BinarySearchTrees/build/reports \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ac1b4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Out Package # +/out/ + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.idea \ No newline at end of file diff --git a/BinarySearchTrees/build.gradle.kts b/BinarySearchTrees/build.gradle.kts new file mode 100644 index 0000000..dde70d0 --- /dev/null +++ b/BinarySearchTrees/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.10" + jacoco +} + +repositories { + mavenCentral() +} + +dependencies { + // Use the Kotlin JDK 8 standard library. + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testImplementation(platform("org.junit:junit-bom:5.9.2")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + finalizedBy("jacocoTestReport") + useJUnitPlatform() + maxHeapSize = "2G" + testLogging { + events("passed", "skipped", "failed") + } + reports.html.outputLocation.set(file("${buildDir}/reports/test")) +} + +tasks.named("jacocoTestReport") { + dependsOn(tasks.test) + + classDirectories.setFrom(files(classDirectories.files.map { + fileTree(it) { + exclude("**/binarysearchtrees/TreesKt.*") + } + })) + + reports { + xml.required.set(false) + html.required.set(true) + html.outputLocation.set(file("${buildDir}/reports/jacoco")) + csv.required.set(true) + csv.outputLocation.set(file("${buildDir}/jacoco/report.csv")) + } +} diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt new file mode 100644 index 0000000..89d327e --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt @@ -0,0 +1,23 @@ +package binarysearchtrees + +interface BinarySearchTree, V> { + val size: Int + + fun isEmpty(): Boolean + + fun clear() + + fun getRoot(): MutableVertex? + + operator fun get(key: K): V? + + fun put(key: K, value: V): V? + + operator fun set(key: K, value: V) + + fun remove(key: K): V? + + fun remove(key: K, value: V): Boolean + + operator fun iterator(): Iterator> +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt new file mode 100644 index 0000000..8f9fa43 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt @@ -0,0 +1,19 @@ +package binarysearchtrees + +import binarysearchtrees.redblacktree.RedBlackTree + +// initializing functions + +fun , V> binarySearchTreeOf(): BinarySearchTree { + return RedBlackTree() +} + +fun , V> binarySearchTreeOf( + vararg args: Pair +): BinarySearchTree { + val tree = RedBlackTree() + for (it in args) { + tree.put(it.first, it.second) + } + return tree +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Vertices.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Vertices.kt new file mode 100644 index 0000000..569c1d8 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Vertices.kt @@ -0,0 +1,16 @@ +package binarysearchtrees + +//for extension functions like all, any, ... +interface Vertex { + val key: K + val value: V + val left: Vertex? + val right: Vertex? +} + +interface MutableVertex : Vertex { + override val left: MutableVertex? + override val right: MutableVertex? + + fun setValue(newValue: V): V +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt new file mode 100644 index 0000000..84f301e --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt @@ -0,0 +1,270 @@ +package binarysearchtrees.avltree + +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.MutableVertex +import binarysearchtrees.avltree.AVLTree +import binarysearchtrees.avltree.Vertex +import binarysearchtrees.avltree.Vertex as PublicVertex + +open class AVLTree, V> : BinarySearchTree { + final override var size: Int = 0 + protected set + protected var root: AVLVertex? = null + protected var modCount: Int = 0 + + override fun isEmpty(): Boolean = (root == null) + + override fun clear() { + size = 0 + root = null + ++modCount + } + + override fun getRoot(): PublicVertex? = root + + override fun iterator(): Iterator> { + return AVLTreeIterator(getRoot()) { modCount } + } + + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + + override fun put(key: K, value: V): V? { + var new = false + var vertex: AVLVertex = root ?: AVLVertex(key, value).also { + root = it + new = true + } + while (vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left ?: AVLVertex(key, value).also { + vertex.left = it + new = true + } + } else { + vertex = vertex.right ?: AVLVertex(key, value).also { + vertex.right = it + new = true + } + } + } + return if (new) { + ++size + ++modCount + balanceUp(vertex) + null + } else vertex.setValue(value) + } + + override operator fun set(key: K, value: V) { + put(key, value) + } + + override fun remove(key: K): V? { + var parent: AVLVertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + val oldValue = vertex?.value + if (vertex != null) { + if (parent == null) { + root = removeVertex(vertex) + } else { + if (parent.left == vertex) { + parent.left = removeVertex(vertex) + } else { + parent.right = removeVertex(vertex) + } + } + --size + ++modCount + } + return oldValue + } + + override fun remove(key: K, value: V): Boolean { + var parent: AVLVertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return if (vertex?.value == value) { + if (parent == null) { + root = vertex?.let { removeVertex(it) } + } else { + if (parent.left == vertex) { + parent.left = vertex?.let { removeVertex(it) } + } else { + parent.right = vertex?.let { removeVertex(it) } + } + } + --size + ++modCount + true + } else false + } + + private fun removeVertex(vertex: AVLVertex): AVLVertex? { + if (vertex.left == null && vertex.right == null) { + return null + } else if (vertex.left == null) { + return vertex.right + } else if (vertex.right == null) { + return vertex.left + } + + val successor = findMin(vertex.right!!) + vertex.key = successor.key + vertex.value = successor.value + vertex.right = removeVertex(successor) + + balanceUp(vertex) + + return vertex + } + + private fun findMin(vertex: AVLVertex): AVLVertex { + var current = vertex + while (current.left != null) { + current = current.left!! + } + return current + } + + private fun balanceUp(vertex: AVLVertex) { + var current: AVLVertex? = vertex + + while (current != null) { + val balanceFactor = getBalanceFactor(current) + if (balanceFactor > 1) { + if (getBalanceFactor(current.left!!) >= 0) { + current = rotateRight(current) + } else { + current.left = rotateLeft(current.left!!) + current = rotateRight(current) + } + } else if (balanceFactor < -1) { + if (getBalanceFactor(current.right!!) <= 0) { + current = rotateLeft(current) + } else { + current.right = rotateRight(current.right!!) + current = rotateLeft(current) + } + } + current = current.parent + } + } + + private fun rotateLeft(vertex: AVLVertex): AVLVertex { + val rightChild = vertex.right!! + vertex.right = rightChild.left + rightChild.left?.parent = vertex + rightChild.left = vertex + rightChild.parent = vertex.parent + vertex.parent = rightChild + + updateHeight(vertex) + updateHeight(rightChild) + + return rightChild + } + + private fun rotateRight(vertex: AVLVertex): AVLVertex { + val leftChild = vertex.left!! + vertex.left = leftChild.right + leftChild.right?.parent = vertex + leftChild.right = vertex + leftChild.parent = vertex.parent + vertex.parent = leftChild + + updateHeight(vertex) + updateHeight(leftChild) + + return leftChild + } + + private fun updateHeight(vertex: AVLVertex) { + val leftHeight = getHeight(vertex.left) + val rightHeight = getHeight(vertex.right) + vertex.height = 1 + maxOf(leftHeight, rightHeight) + } + + private fun getBalanceFactor(vertex: AVLVertex): Int { + val leftHeight = getHeight(vertex.left) + val rightHeight = getHeight(vertex.right) + return leftHeight - rightHeight + } + + private fun getHeight(vertex: AVLVertex?): Int { + return vertex?.height ?: 0 + } + + protected class AVLVertex( + override var key: K, + override var value: V, + override var left: AVLVertex? = null, + override var right: AVLVertex? = null, + var parent: AVLVertex? = null, + var height: Int = 1 + ) : PublicVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } + + protected class AVLTreeIterator( + root: PublicVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left + } + } + + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() + } + } + + override fun next(): PublicVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex + } + } + } +} diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt new file mode 100644 index 0000000..0caeebf --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt @@ -0,0 +1,8 @@ +package binarysearchtrees.avltree + +import binarysearchtrees.MutableVertex + +interface Vertex : MutableVertex { + override val left: Vertex? + override val right: Vertex? +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt new file mode 100644 index 0000000..c46b159 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt @@ -0,0 +1,194 @@ +package binarysearchtrees.binarysearchtree + +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.binarysearchtree.Vertex as PublicVertex + +open class SimpleBinarySearchTree, V> : BinarySearchTree { + final override var size: Int = 0 + protected set + protected var root: Vertex? = null + protected var modCount: Int = 0 + + override fun isEmpty(): Boolean = (root == null) + + override fun clear() { + size = 0 + root = null + ++modCount + } + + override fun getRoot(): PublicVertex? = root + + override fun iterator(): Iterator> { + return BinarySearchTreeIterator(getRoot()) { modCount } + } + + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + + override fun put(key: K, value: V): V? { + var new = false + var vertex: Vertex = root ?: Vertex(key, value).also { + root = it + new = true + } + while (vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left ?: Vertex(key, value).also { + vertex.left = it + new = true + } + } else { + vertex = vertex.right ?: Vertex(key, value).also { + vertex.right = it + new = true + } + } + } + return if (new) { + ++size + ++modCount + null + } else vertex.setValue(value) + } + + override operator fun set(key: K, value: V) { + put(key, value) + } + + override fun remove(key: K): V? { + var parent: Vertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + val oldValue = vertex?.value + if (vertex != null) { + if (parent == null) { + root = removeVertex(vertex) + } else { + if (parent.left == vertex) { + parent.left = removeVertex(vertex) + } else { + parent.right = removeVertex(vertex) + } + } + --size + ++modCount + } + return oldValue + } + + override fun remove(key: K, value: V): Boolean { + var parent: Vertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return if (vertex?.value == value) { + if (parent == null) { + root = vertex?.let { removeVertex(it) } + } else { + if (parent.left == vertex) { + parent.left = vertex?.let { removeVertex(it) } + } else { + parent.right = vertex?.let { removeVertex(it) } + } + } + --size + ++modCount + true + } else false + } + + private fun removeVertex(vertex: Vertex): Vertex? { + return vertex.left?.let { left -> + vertex.right?.let { right -> + //search of parent of Vertex with next key + var nextParent = vertex + var next = right // vertex.right + var nextLeft = next.left // left son of next vertex + while (nextLeft != null) { + nextParent = next + next = nextLeft + nextLeft = next.left + } + if (nextParent == vertex) { + next.left = left // vertex.left + } else { + nextParent.left = next.right + next.left = left // vertex.left + next.right = right // vertex.right + } + + next + } ?: left + } ?: vertex.right + } + + protected class Vertex( + override val key: K, + override var value: V, + override var left: Vertex? = null, + override var right: Vertex? = null + ) : PublicVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } + + protected class BinarySearchTreeIterator( + root: PublicVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left + } + } + + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() + } + } + + override fun next(): PublicVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex + } + } + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/Vertex.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/Vertex.kt new file mode 100644 index 0000000..7a04e31 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/Vertex.kt @@ -0,0 +1,8 @@ +package binarysearchtrees.binarysearchtree + +import binarysearchtrees.MutableVertex + +interface Vertex : MutableVertex { + override val left: Vertex? + override val right: Vertex? +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt new file mode 100644 index 0000000..90437d9 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt @@ -0,0 +1,528 @@ +package binarysearchtrees.redblacktree + +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.redblacktree.Vertex.Color +import binarysearchtrees.redblacktree.Vertex as PublicVertex + +open class RedBlackTree, V> : BinarySearchTree { + final override var size: Int = 0 + protected set + protected var root: Vertex? = null + protected var modCount: Int = 0 + + override fun isEmpty(): Boolean = (root == null) + + override fun clear() { + size = 0 + root = null + ++modCount + } + + override fun getRoot(): PublicVertex? = root + + override fun iterator(): Iterator> { + return RedBlackTreeIterator(getRoot()) { modCount } + } + + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + + override fun put(key: K, value: V): V? { + var new = false + val stack = mutableListOf>() + var vertex = root ?: Vertex(key, value, Color.BLACK).also { + root = it + new = true + } + while (vertex.key != key) { + stack.add(vertex) + if (vertex.key > key) { + vertex = vertex.left ?: Vertex(key, value).also { + vertex.left = it + new = true + } + } else { + vertex = vertex.right ?: Vertex(key, value).also { + vertex.right = it + new = true + } + } + } + + // balance and return + return if (new) { + stack.add(vertex) + balanceAfterInsert(stack) + stack.clear() + ++size + ++modCount + null + } else { + stack.clear() + vertex.setValue(value) + } + } + + override operator fun set(key: K, value: V) { + put(key, value) + } + + override fun remove(key: K): V? { + val stack = mutableListOf>() + var parentIndex = -1 + var vertex = root + while (vertex != null && vertex.key != key) { + stack.add(vertex) + ++parentIndex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + val oldValue = vertex?.value + if (vertex != null) { + val goner = getGoner(vertex, parentIndex, stack) + + // leaf removal + stack.add(goner) + removeLeaf(stack) + --size + ++modCount + } + stack.clear() + return oldValue + } + + override fun remove(key: K, value: V): Boolean { + val stack = mutableListOf>() + var parentIndex = -1 + var vertex = root + while (vertex != null && vertex.key != key) { + stack.add(vertex) + ++parentIndex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return if (vertex != null && vertex.value == value) { + val goner = getGoner(vertex, parentIndex, stack) + + // leaf removal + stack.add(goner) + removeLeaf(stack) + --size + ++modCount + stack.clear() + true + } else { + stack.clear() + false + } + } + + private fun rotateLeft(parent: Vertex, child: Vertex): Vertex { + parent.color = child.color.also { child.color = parent.color } + parent.right = child.left + child.left = parent + return child + } + + private fun rotateRight(parent: Vertex, child: Vertex): Vertex { + parent.color = child.color.also { child.color = parent.color } + parent.left = child.right + child.right = parent + return child + } + + private fun balanceAfterInsert(stack: MutableList>) { + val black = Color.BLACK + val red = Color.RED + var notEnd = true + + var vertex = stack.removeLast() + while (notEnd && stack.lastOrNull()?.color == red) { + var parent = stack.removeLast() + val grandparent = stack.removeLast() + if (grandparent.left == parent) { + if (grandparent.right?.color == red) { + grandparent.color = red + grandparent.right?.color = black + parent.color = black + vertex = grandparent + } else { + notEnd = false + if (parent.right == vertex) { + parent = rotateLeft(parent, vertex) + grandparent.left = parent + } + if (root == grandparent) { + root = rotateRight(grandparent, parent) + } else { + val ggparent = stack.last() + if (ggparent.left == grandparent) { + ggparent.left = rotateRight(grandparent, parent) + } else { + ggparent.right = rotateRight(grandparent, parent) + } + } + } + } else { + if (grandparent.left?.color == red) { + grandparent.color = red + grandparent.left?.color = black + parent.color = black + vertex = grandparent + } else { + notEnd = false + if (parent.left == vertex) { + parent = rotateRight(parent, vertex) + grandparent.right = parent + } + if (root == grandparent) { + root = rotateLeft(grandparent, parent) + } else { + val ggparent = stack.last() + if (ggparent.left == grandparent) { + ggparent.left = rotateLeft(grandparent, parent) + } else { + ggparent.right = rotateLeft(grandparent, parent) + } + } + } + } + } + + root?.color = black + } + + private fun getGoner(vertex: Vertex, parentIndex: Int, stack: MutableList>): Vertex { + return vertex.left?.let { left -> + vertex.right?.let { right -> + // swap with the vertex that is next by key + var nextParent: Vertex = vertex + stack.add(nextParent) + var next = right + var nextLeft = next.left + while (nextLeft != null) { + nextParent = next + stack.add(nextParent) + next = nextLeft + nextLeft = next.left + } + vertex.color = next.color.also { next.color = vertex.color } + if (root == vertex) { + root = next + } else { + val parent = stack[parentIndex] + if (parent.left == vertex) { + parent.left = next + } else { + parent.right = next + } + } + stack[parentIndex + 1] = next + next.left = left + vertex.left = null + vertex.right = next.right + if (nextParent == vertex) { + next.right = vertex + vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + next.right = right + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + } + } else { + next.right = right + nextParent.left = vertex + vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + nextParent.left = right + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + } + } + vertex + } ?: left.let { left -> + // swap with the single red leaf + vertex.color = left.color.also { left.color = vertex.color } + if (root == vertex) { + root = left + } else { + val parent = stack.last() + if (parent.left == vertex) { + parent.left = left + } else { + parent.right = left + } + } + left.right = vertex + vertex.left = null + vertex.right = null + stack.add(left) + vertex + } + } ?: vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + if (root == vertex) { + root = right + } else { + val parent = stack.last() + if (parent.left == vertex) { + parent.left = right + } else { + parent.right = right + } + } + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + vertex + } ?: vertex // vertex is already a leaf + } + + private fun removeLeaf(stack: MutableList>) { + val black = Color.BLACK + val red = Color.RED + var notEnd = true + + fun balanceLeft(parent: Vertex): Vertex { + fun balanceBlackRightBrother(parent: Vertex): Vertex { + var brother = parent.right + if (brother != null) { + var rightCousin = brother.right + val leftCousin = brother.left + + if ((rightCousin?.color ?: black) == black && (leftCousin?.color ?: black) == black) { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + return parent + } + + if ((rightCousin?.color ?: black) == black && leftCousin != null) { + rightCousin = brother + brother = rotateRight(brother, leftCousin) + parent.right = brother + } + + notEnd = false + rightCousin?.color = black + return rotateLeft(parent, brother) + } else { + throw Exception("Deletion error: height of right subtree must be at least 1") + } + } + + return parent.right?.let { right -> + if (right.color == red) { + val newParent = rotateLeft(parent, right) + newParent.left = balanceBlackRightBrother(parent) + if (notEnd) { + balanceBlackRightBrother(newParent) + } else { + newParent + } + } else { + balanceBlackRightBrother(parent) + } + } ?: balanceBlackRightBrother(parent) + } + + fun balanceRight(parent: Vertex): Vertex { + fun balanceBlackLeftBrother(parent: Vertex): Vertex { + parent.left?.let { it -> + var brother = it + var leftCousin = brother.left + val rightCousin = brother.right + + if ((leftCousin?.color ?: black) == black && (rightCousin?.color ?: black) == black) { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + return parent + } + + if ((leftCousin?.color ?: black) == black && rightCousin != null) { + leftCousin = brother + brother = rotateLeft(brother, rightCousin) + parent.left = brother + } + + notEnd = false + leftCousin?.color = black + return rotateRight(parent, brother) + } ?: throw Exception("Deletion error: height of left subtree must be at least 1") + } + + return parent.left?.let { left -> + if (left.color == red) { + val newParent = rotateRight(parent, left) + newParent.right = balanceBlackLeftBrother(parent) + if (notEnd) { + balanceBlackLeftBrother(newParent) + } else { + newParent + } + } else { + balanceBlackLeftBrother(parent) + } + } ?: balanceBlackLeftBrother(parent) + } + + val goner = stack.removeLast() + if (goner.color == red) { + val parent = stack.last() + if (parent.left == goner) { + parent.left = null + } else { + parent.right = null + } + return + } + + // removal black leaf + if (root == goner) { + root = null + return + } + + var parent = stack.removeLast() + if (root == parent) { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + root = parent + } else { + val grandparent = stack.last() + if (grandparent.left == parent) { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + grandparent.left = parent + } else { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + grandparent.right = parent + } + } + stack.add(parent) + + while (notEnd && stack.size >= 2) { + val vertex = stack.removeLast() + parent = stack.removeLast() + if (root == parent) { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + root = parent + } else { + val grandparent = stack.last() + if (grandparent.left == parent) { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + grandparent.left = parent + } else { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + grandparent.right = parent + } + } + stack.add(parent) + } + root?.color = black + } + + protected class Vertex( + override val key: K, + override var value: V, + override var color: Color = Color.RED, + override var left: Vertex? = null, + override var right: Vertex? = null + ) : PublicVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } + + protected class RedBlackTreeIterator( + root: PublicVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left + } + } + + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() + } + } + + override fun next(): PublicVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex + } + } + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/Vertex.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/Vertex.kt new file mode 100644 index 0000000..505378b --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/Vertex.kt @@ -0,0 +1,11 @@ +package binarysearchtrees.redblacktree + +import binarysearchtrees.MutableVertex + +interface Vertex : MutableVertex { + val color: Color + override val left: Vertex? + override val right: Vertex? + + enum class Color { RED, BLACK } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt new file mode 100644 index 0000000..58e4a1c --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt @@ -0,0 +1,9 @@ +package binarysearchtrees.avltree + +fun , V> isAVLTree(tree: AVLTree): Boolean { + fun checkAVLInvariant(vertex: Vertex): Boolean { + return vertex.left?.let { vertex.key > it.key && checkAVLInvariant(it) } ?: true + && vertex.right?.let { vertex.key < it.key && checkAVLInvariant(it) } ?: true + } + return tree.getRoot()?.let { checkAVLInvariant(it) } ?: true +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt new file mode 100644 index 0000000..1c8a12f --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt @@ -0,0 +1,146 @@ +package binarysearchtrees.avltree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random + +class AVLTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: AVLTree + + @BeforeEach + fun init() { + tree = AVLTree() + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isAVLTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, it.second) } + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) + assertEquals(values.distinctBy { it.first }.size, tree.size) + } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the some element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + var size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + + value = key * 95 + tree[key] = value + size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + var size = tree.size - 1 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) + assertEquals(0, tree.size) + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvariantChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvariantChecker.kt new file mode 100644 index 0000000..1d6be1e --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvariantChecker.kt @@ -0,0 +1,9 @@ +package binarysearchtrees.binarysearchtree + +fun , V> isBinarySearchTree(tree: SimpleBinarySearchTree): Boolean { + fun checkBSTInvariant(vertex: Vertex): Boolean { + return vertex.left?.let { vertex.key > it.key && checkBSTInvariant(it) } ?: true + && vertex.right?.let { vertex.key < it.key && checkBSTInvariant(it) } ?: true + } + return tree.getRoot()?.let { checkBSTInvariant(it) } ?: true +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTreeTest.kt new file mode 100644 index 0000000..ef171e4 --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTreeTest.kt @@ -0,0 +1,164 @@ +package binarysearchtrees.binarysearchtree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random + +class SimpleBinarySearchTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: SimpleBinarySearchTree + + @BeforeEach + fun init() { + tree = SimpleBinarySearchTree() + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isBinarySearchTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, it.second) } + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) + assertEquals(values.distinctBy { it.first }.size, tree.size) + } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the some element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + var size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + + value = key * 95 + tree[key] = value + size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + + @Test + fun `Function remove deletes the existing element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val elements = mutableListOf>() + values.reversed().distinctBy { it.first }.forEach { elements.add(it) } + elements.shuffle() + for (i in 0 until elements.size step 20) { + val key = elements[i].first + val value = elements[i].second + val size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + var size = tree.size - 1 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) + assertEquals(0, tree.size) + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvariantChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvariantChecker.kt new file mode 100644 index 0000000..c0c4459 --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvariantChecker.kt @@ -0,0 +1,30 @@ +package binarysearchtrees.redblacktree + +fun , V> isRedBlackTree(tree: RedBlackTree): Boolean { + var f = true + + fun checkRBTInvariant(vertex: Vertex): Int { + if (vertex.color == Vertex.Color.RED) { + f = f && (vertex.left?.color != Vertex.Color.RED) + f = f && (vertex.right?.color != Vertex.Color.RED) + } + val leftBlackHeight = vertex.left?.let { + f = f && (vertex.key > it.key) + checkRBTInvariant(it) + } ?: 1 + val rightBlackHeight = vertex.right?.let { + f = f && (vertex.key < it.key) + checkRBTInvariant(it) + } ?: 1 + f = f && (leftBlackHeight == rightBlackHeight) + return if (vertex.color == Vertex.Color.BLACK) + leftBlackHeight + 1 + else leftBlackHeight + } + + return tree.getRoot()?.let { + f = (it.color == Vertex.Color.BLACK) + checkRBTInvariant(it) + f + } ?: true +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RedBlackTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RedBlackTreeTest.kt new file mode 100644 index 0000000..36d791b --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RedBlackTreeTest.kt @@ -0,0 +1,165 @@ +package binarysearchtrees.redblacktree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random + +class RedBlackTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: RedBlackTree + + @BeforeEach + fun init() { + tree = RedBlackTree() + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isRedBlackTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, 245) } + values.forEach { tree.put(it.first, it.second) } + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) + assertEquals(values.distinctBy { it.first }.size, tree.size) + } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the some element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + var size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + + value = key * 95 + tree[key] = value + size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + + @Test + fun `Function remove deletes the existing element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val elements = mutableListOf>() + values.reversed().distinctBy { it.first }.forEach { elements.add(it) } + elements.shuffle() + for (i in 0 until elements.size step 20) { + val key = elements[i].first + val value = elements[i].second + val size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + var size = tree.size - 1 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) + assertEquals(0, tree.size) + } +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7ec2dde --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,154 @@ + +
+
+ + Logo + + +

Search Trees Project

+ +

+ Working with the repository. +
+
+ Commits + · + Branches +

+
+ + + + +
+ Table of Contents +
    +
  1. Maintaining Сleanliness
  2. +
  3. Branch Names
  4. +
  5. Correct Commits
  6. +
  7. About Pull Requests
  8. +
  9. Some Tips
  10. +
+
+ + + + +## Maintaining Сleanliness + +Important rules for maintaining cleanliness in the repository: + +* Making well-considered major changes; +* Execution of any commands strictly according to the form and rules. +* Storing various garbage content on cloud services, adding only in the form of links. + +

(Back to top)

+ + + + +## Branch Names + +### The form of the branch name: + + + git checkout -b / + + +### The most important prefixes for branches: + +* `feat` - Developing new functionality; +* `docs` - Working with information files; +* `ci` - Actions related to CI. + +### Rules for working with branches: + +* A branch is a large logical block; +* The new functionality is being developed in a separate branch; +* The branch is deleted after the completion of work in it. + +About: Branches + +

(Back to top)

+ + + + +## Correct Commits + +### The form of the commit name: + + + git commit -m ": " + + +### The most important prefixes for commits: + +* `fix` - Fixing a bug in the code; +* `feat` - Adding new functionality; +* `docs` - Adding information files; +* `refactor` - Code refactoring; +* `test` - Adding testing modules; +* `struct` - Changing the file structure of the project; +* `ci` - Actions related to CI. + +### Rules for writing the body: + +* Past tense; +* English language; +* The dot at the end; +* Informative thesis; +* Capital letter at the beginning. + +About: Conventional Commits + +

(Back to top)

+ + + + +## About Pull Requests + +### Form for pull request: + +Name: + +`A thesis that conveys the general idea of changes, or the most important change.` + +* Issued in English; +* Begins with a capital letter; +* Ends with a dot. + +Description: + +`One or more abstracts describing important changes in the project in more detail.` + +* Issued in English; +* A '- ' is placed before each individual thesis. +* Each thesis begins with a capital letter and ends with a semicolon, if it is not the last one. \ + Otherwise it ends with just a dot. + +### Important rules: + +* Follow the form above; +* Write a meaningful title and description; +* Do not merge branches without a review. + +About: Pull Requests + +

(Back to top)

+ + + + +## Some Tips + +Some tips that can help in the joint development of the project: + +* Always check the repository for changes; +* Double-check any information before confirming; +* Work very carefully under someone else's branch; +* Each pull request must be an entire logical unit; +* Always double-check if you are in the correct branch. + +

(Back to top)

diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..513212c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Arsene Baitenov, Artem Gryaznov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb1cbff --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ + +
+
+ + Logo + + +

Search Trees Project

+ +

+ AVL, Red-Black and Binary Search Trees models. +
+
+ Report Bug + · + Request Feature +

+
+ + + + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. App Usage
  6. +
  7. License
  8. +
  9. Contact
  10. +
  11. Acknowledgments
  12. +
+
+ + + + +## About The Project + +Many people use search engines to classify various information, as well as quickly obtain the necessary data. +Our task is to study this issue in practice, which implies the development of search trees with a detailed study of all the subtleties of each model. + +Types of search trees that we are going to implement: +* A simple Binary Search Tree. +* Red-Black Search Tree. +* AVL Search Tree. + +Of course, our task is not only to develop the algorithm of the application itself, but also to implement the user interface to work with it, create test coverage and decent documentation. + +

(Back to top)

+ + + +### Used Technologies + +Technologies used to develop the project: + +* [![gradle](https://img.shields.io/badge/gradle-FFFFFF?style=for-the-badge&logo=gradle&logoColor=black&)](https://gradle.org/) +* [![gradle](https://img.shields.io/badge/kotlin-FFFFFF?style=for-the-badge&logo=kotlin&logoColor=black&)](https://kotlinlang.org/) +* [![gradle](https://img.shields.io/badge/junit-FFFFFF?style=for-the-badge&logo=junit&logoColor=black&)](https://junit.org/) +* [![gradle](https://img.shields.io/badge/neo4j-FFFFFF?style=for-the-badge&logo=neo4j&logoColor=black&)](https://neo4j.com) +* [![gradle](https://img.shields.io/badge/sqlite-FFFFFF?style=for-the-badge&logo=sqlite&logoColor=black&)](https://www.sqlite.org/index.html) +* [![gradle](https://img.shields.io/badge/docker-FFFFFF?style=for-the-badge&logo=docker&logoColor=black&)](https://www.docker.com) +* [![gradle](https://img.shields.io/badge/compose-FFFFFF?style=for-the-badge&logo=compose&logoColor=black&)](https://www.jetbrains.com/ru-ru/lp/compose-multiplatform/) + +

(Back to top)

+ + + + +## Getting Started + +To start working with our development, you need to clone repository: + +* git + + ```sh + git clone https://github.com/spbu-coding-2022/trees-12.git + ``` + +To initialize the library and start working with it, you need to know the following lines: + +* Initializing BinarySearchTree (default RedBlackTree): + + ```kotlin + val tree = binarySearchTreeOf() + ``` + +* Initializing simple BinarySearchTree: + + ```kotlin + val tree = SimpleBinarySearchTree() + ``` + +* Initializing RedBlackTree: + + ```kotlin + val tree = RedBlackTree() + ``` + +* Initializing AVLTree: + + ```kotlin + val tree = AVLTree() + ``` + +To work with trees, you also need to know the management commands: + +* Inserting a value by key: + + ```kotlin + tree[key] = value + ``` + +* Getting a value by key: + + ```kotlin + val value = tree[key] + ``` + +* Deleting a value by key: + + ```kotlin + tree.remove(key) + ``` + + +## App Usage + +Before launching the application, you need to run "neo4j" via "docker": + +* This is done by the command in project repository: + +```sh +docker compose up -d +``` + +You also need to build the application and run it: + +* This is done by the command: + +```sh +./gradlew run +``` + +A little bit about the user interface: + +* After launching the application, a window will appear in which you can select the desired type of tree, as well as previously saved models of this type. + +Start + +* After you select the tree, a window will appear with functional buttons for adding, deleting and searching for values by key. Also, above the buttons you can see the type of the selected tree, and below them there will be buttons for saving and deleting the current model. + +Main + +

(Back to top)

+ + + + +## License + +Distributed under the MIT License. See `LICENSE` for more information. + +

(Back to top)

+ + + + +## Contact + +Baitenov Arsene • [Telegram](https://t.me/ASpectreTG) • arsenebaitenov@gmail.com \ +Gryaznov Artem • [Telegram](https://t.me/kkkebab_boy) • gryaznovasm@gmail.com + +Project Link • [https://github.com/spbu-coding-2022/trees-12](https://github.com/spbu-coding-2022/trees-12) + +

(Back to top)

+ + + + +## Acknowledgments + +The resources that we used to get information about binary search trees, their features and implementation possibilities: + +* [MIT License](https://mit-license.org) +* [Binary Search Tree](https://en.wikipedia.org/wiki/Search_tree) +* [AVL Search Tree](https://en.wikipedia.org/wiki/AVL_tree) +* [Red-Black Search Tree](https://en.wikipedia.org/wiki/Red–black_tree) +* [Gradle Documentation](https://docs.gradle.org/current/userguide/userguide.html) +* [JUnit Documentation](https://junit.org/junit5/docs/current/user-guide/) +* [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +* [Mergeable Documentation](https://mergeable.readthedocs.io/en/latest/configuration.html#basics) +* [Compose Documentation](https://developer.android.com/jetpack/compose/documentation) +* [Docker Desktop](https://docs.docker.com/desktop/) + +

(Back to top)

\ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..0bdce6e --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.8.20" + id("org.jetbrains.kotlin.plugin.noarg") version "1.8.20" + id("org.jetbrains.compose") version "1.4.0" + jacoco + application +} + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +dependencies { + // Use the Kotlin JDK 8 standard library. + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testImplementation(platform("org.junit:junit-bom:5.9.2")) + testImplementation("org.junit.jupiter:junit-jupiter") + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") + implementation("org.neo4j:neo4j-ogm-core:4.0.5") + implementation("org.neo4j:neo4j-ogm-bolt-driver:4.0.5") + implementation("org.slf4j:slf4j-simple:2.0.0") + + implementation(compose.desktop.currentOs) + implementation(compose.material3) + + implementation(project(":BinarySearchTrees")) +} + +noArg { + annotation("org.neo4j.ogm.annotation.NodeEntity") + annotation("org.neo4j.ogm.annotation.RelationshipEntity") +} + +tasks.test { + finalizedBy("jacocoTestReport") + useJUnitPlatform() + maxHeapSize = "2G" + testLogging { + events("passed", "skipped", "failed") + } + reports.html.outputLocation.set(file("${buildDir}/reports/test")) +} + +tasks.named("jacocoTestReport") { + dependsOn(tasks.test) + reports { + xml.required.set(false) + html.required.set(true) + html.outputLocation.set(file("${buildDir}/reports/jacoco")) + csv.required.set(true) + csv.outputLocation.set(file("${buildDir}/jacoco/report.csv")) + } +} + +application { + mainClass.set("app.AppKt") +} diff --git a/app/src/main/kotlin/app/App.kt b/app/src/main/kotlin/app/App.kt new file mode 100644 index 0000000..0589aca --- /dev/null +++ b/app/src/main/kotlin/app/App.kt @@ -0,0 +1,43 @@ +package app + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.application +import app.view.MainWindow +import app.view.Position +import app.view.defaultVertexSize +import app.view.setTreePositions +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.redblacktree.RedBlackTree + +fun main() { + application { + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy( + primary = Color(162, 32, 240), + secondary = Color.Magenta, + tertiary = Color(164, 0, 178), + background = Color(240, 240, 240) + ) + ) { + // + val tree = RedBlackTree() + tree["1"] = Position(0.dp, 0.dp) + tree["2"] = Position(0.dp, 0.dp) + tree["3"] = Position(0.dp, 0.dp) + tree["4"] = Position(0.dp, 0.dp) + tree["5"] = Position(0.dp, 0.dp) + tree["6"] = Position(0.dp, 0.dp) + tree["7"] = Position(0.dp, 0.dp) + tree["8"] = Position(0.dp, 0.dp) + tree["9"] = Position(0.dp, 0.dp) + tree["10"] = Position(0.dp, 0.dp) + // + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + // + MainWindow(tree, "Tree", ::exitApplication) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/controller/Repository.kt b/app/src/main/kotlin/app/controller/Repository.kt new file mode 100644 index 0000000..68a74f2 --- /dev/null +++ b/app/src/main/kotlin/app/controller/Repository.kt @@ -0,0 +1,143 @@ +package app.controller + +import app.model.repos.BSTRepository +import app.model.repos.RBTRepository +import app.view.Position +import app.view.ScrollDelta +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.redblacktree.RedBlackTree +import org.neo4j.ogm.config.Configuration + +class Repository() { + private lateinit var bstRepo: BSTRepository + private lateinit var rbtRepo: RBTRepository + + init { + try { + val dirPath = "./BSTRepo" + bstRepo = BSTRepository( + dirPath, + { serializePosition(it) }, + { deserializePosition(it) } + ) + } catch (e: Exception) { + throw Exception("BSTRepo Init Exception: " + e.message) + } + + try { + val configuration = Configuration.Builder() + .uri("bolt://localhost") + .credentials("neo4j", "qwerty") + .build() + rbtRepo = RBTRepository( + configuration, + { serializePosition(it) }, + { deserializePosition(it) } + ) + } catch (e: Exception) { + throw Exception("RBTRepo Init Exception: " + e.message) + } + } + + fun getNames(treeType: TreeType): List { + return when(treeType) { + TreeType.BST -> { + try { + bstRepo.getNames() + } catch (e: Exception) { + throw Exception("BSTRepo GetNames Exception: " + e.message) + } + } + + TreeType.RBT -> { + try { + rbtRepo.getNames() + } catch (e: Exception) { + throw Exception("RBTRepo GetNames Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + } + + fun save(name: String, tree: BinarySearchTree, scrollDelta: ScrollDelta) { + val settingsData = serializeScrollDelta(scrollDelta) + + when (tree) { + is SimpleBinarySearchTree -> { + try { + bstRepo.set(name, tree, settingsData) + } catch (e: Exception) { + throw Exception("BSTRepo Save Exception: " + e.message) + } + } + + is RedBlackTree -> { + try { + rbtRepo.set(name, tree, settingsData) + } catch (e: Exception) { + throw Exception("RBTRepo Save Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + } + + fun get(name: String, treeType: TreeType): Pair, ScrollDelta> { + val (tree: BinarySearchTree, settingsData: String) = when (treeType) { + TreeType.BST -> { + try { + bstRepo.get(name) + } catch (e: Exception) { + throw Exception("BSTRepo Get Exception: " + e.message) + } + } + + TreeType.RBT -> { + try { + rbtRepo.get(name) + } catch (e: Exception) { + throw Exception("RBTRepo Get Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + return Pair(tree, deserializeScrollDelta(settingsData)) + } + + fun delete(name: String, tree: BinarySearchTree): Boolean { + return when (tree) { + is SimpleBinarySearchTree -> { + try { + bstRepo.remove(name) + } catch (e: Exception) { + throw Exception("BSTRepo Delete Exception: " + e.message) + } + } + + is RedBlackTree -> { + try { + rbtRepo.remove(name) + } catch (e: Exception) { + throw Exception("RBTRepo Delete Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + } + + enum class TreeType { BST, RBT, AVL } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/controller/Serialization.kt b/app/src/main/kotlin/app/controller/Serialization.kt new file mode 100644 index 0000000..5df5d7d --- /dev/null +++ b/app/src/main/kotlin/app/controller/Serialization.kt @@ -0,0 +1,26 @@ +package app.controller + +import androidx.compose.ui.unit.dp +import app.view.Position +import app.view.ScrollDelta +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +fun serializePosition(pos: Position): String { + return Json.encodeToString(Pair(pos.x.value, pos.x.value)) +} + +fun deserializePosition(image: String): Position { + val floatImage = Json.decodeFromString>(image) + return Position(floatImage.first.dp, floatImage.first.dp) +} + +fun serializeScrollDelta(scrollDelta: ScrollDelta): String { + return Json.encodeToString(Pair(scrollDelta.x.value, scrollDelta.x.value)) +} + +fun deserializeScrollDelta(image: String): ScrollDelta { + val floatImage = Json.decodeFromString>(image) + return ScrollDelta(floatImage.first.dp, floatImage.first.dp) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repos/AVLRepo.kt b/app/src/main/kotlin/app/model/repos/AVLRepo.kt new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/kotlin/app/model/repos/BSTRepo.kt b/app/src/main/kotlin/app/model/repos/BSTRepo.kt new file mode 100644 index 0000000..6580177 --- /dev/null +++ b/app/src/main/kotlin/app/model/repos/BSTRepo.kt @@ -0,0 +1,78 @@ +package app.model.repos + +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.binarysearchtree.Vertex +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File + +class BSTRepository( + private val dirPath: String, + private val serializeValue: (ValueType) -> String, + private val deserializeValue: (String) -> ValueType +) { + init { + File(dirPath).mkdirs() + } + + fun getNames(): List { + return File(dirPath).listFiles { it -> + it.name.endsWith(".json") + }?.map { it.name.dropLast(5) } ?: listOf() + } + + fun get(name: String): Pair, String> { + val jsonTree = Json.decodeFromString(File(dirPath, "$name.json").readText()) + return BST().apply { + jsonTree.root?.let { buildTree(it, deserializeValue) } + } to jsonTree.settingsData + } + + fun set(name: String, tree: SimpleBinarySearchTree, settingsData: String) { + val file = File(dirPath, "$name.json") + file.createNewFile() + file.writeText(Json.encodeToString(JsonTree(settingsData, tree.getRoot()?.toJsonNode(serializeValue)))) + } + + fun remove(name: String): Boolean = File(dirPath, "$name.json").delete() + + private fun Vertex.toJsonNode(serializeValue: (ValueType) -> String): JsonVertex { + return JsonVertex( + key, + serializeValue(value), + left?.toJsonNode(serializeValue), + right?.toJsonNode(serializeValue) + ) + } +} + +@Serializable +data class JsonTree( + val settingsData: String, + val root: JsonVertex? +) + +@Serializable +data class JsonVertex( + val key: String, + val value: String, + val left: JsonVertex?, + val right: JsonVertex?, +) + +private class BST : SimpleBinarySearchTree() { + fun buildTree(jsonVertex: JsonVertex, deserializeValue: (String) -> ValueType) { + root = jsonVertex.toVertex(deserializeValue) + } + + private fun JsonVertex.toVertex(deserializeValue: (String) -> ValueType): Vertex { + val vertex = Vertex(key, deserializeValue(value)) + ++size + ++modCount + vertex.left = left?.toVertex(deserializeValue) + vertex.right = right?.toVertex(deserializeValue) + return vertex + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repos/RBTRepo.kt b/app/src/main/kotlin/app/model/repos/RBTRepo.kt new file mode 100644 index 0000000..9846bd6 --- /dev/null +++ b/app/src/main/kotlin/app/model/repos/RBTRepo.kt @@ -0,0 +1,116 @@ +package app.model.repos + +import binarysearchtrees.redblacktree.RedBlackTree +import binarysearchtrees.redblacktree.Vertex +import binarysearchtrees.redblacktree.Vertex.Color +import org.neo4j.ogm.annotation.* +import org.neo4j.ogm.config.Configuration +import org.neo4j.ogm.session.SessionFactory + +class RBTRepository( + configuration: Configuration, + private val serializeValue: (ValueType) -> String, + private val deserializeValue: (String) -> ValueType +) { + private val sessionFactory = SessionFactory(configuration, "app.model.repos") + private val session = sessionFactory.openSession() + + fun getNames(): List { + return session.loadAll( + TreeEntity::class.java, + 0 + ).map { it.name } + } + + fun get(name: String): Pair, String> { + val treeEntity = session.load( + TreeEntity::class.java, + name, + -1 + ) + return RBT().apply { + treeEntity.root?.let { buildTree(it, deserializeValue) } + } to treeEntity.settingsData + } + + fun set(name: String, tree: RedBlackTree, settingsData: String) { + remove(name) + session.save( + TreeEntity( + name, + settingsData, + tree.getRoot()?.toVertexEntity(serializeValue) + ) + ) + } + + fun remove(name: String): Boolean { + return session.query( + "MATCH r = (t:Tree{name : \$NAME})-[*]->() DETACH DELETE r", + mapOf("NAME" to name) + ).queryStatistics().containsUpdates() + } + + private fun Vertex.toVertexEntity(serializeValue: (ValueType) -> String): VertexEntity { + return VertexEntity( + key, + serializeValue(value), + color.toString()[0], + left?.toVertexEntity(serializeValue), + right?.toVertexEntity(serializeValue) + ) + } +} + +@NodeEntity("Tree") +data class TreeEntity( + @Id + val name: String, + + @Property + val settingsData: String, + + @Relationship(type = "ROOT") + val root: VertexEntity? +) + +@NodeEntity("Vertex") +data class VertexEntity( + @Property + val key: String, + + @Property + val value: String, + + @Property + val color: Char, + + @Relationship(type = "LEFT") + val left: VertexEntity?, + + @Relationship(type = "RIGHT") + val right: VertexEntity?, +) { + @Id + @GeneratedValue + var id: Long? = null +} + +private class RBT : RedBlackTree() { + fun buildTree(vertexEntity: VertexEntity, deserializeValue: (String) -> ValueType) { + root = vertexEntity.toVertex(deserializeValue) + } + + private fun VertexEntity.toVertex(deserializeValue: (String) -> ValueType): Vertex { + val vertex = Vertex( + key, + deserializeValue(value), + if (color == 'R') Color.RED else Color.BLACK + ) + ++size + ++modCount + vertex.left = left?.toVertex(deserializeValue) + vertex.right = right?.toVertex(deserializeValue) + return vertex + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/DefaultValues.kt b/app/src/main/kotlin/app/view/DefaultValues.kt new file mode 100644 index 0000000..0b1f172 --- /dev/null +++ b/app/src/main/kotlin/app/view/DefaultValues.kt @@ -0,0 +1,66 @@ +package app.view + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +val defaultPadding = 5.dp + +val defaultScrollDelta = ScrollDelta(0.dp, 0.dp) + +const val defaultScrollCf = 75 + +val defaultVertexSize = 60.dp + +val defaultBrush + @Composable + get() = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary)) + +val defaultVVBrush + @Composable + get() = Brush.linearGradient(listOf(MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.primary)) + +val defaultBlackBrush + @Composable + get() = Brush.linearGradient(listOf(Color.Black, Color.Black)) + +val defaultRedBrush + @Composable + get() = Brush.linearGradient(listOf(Color(139, 0, 0), Color(139, 0, 0))) + +val defaultEdgeColor + @Composable + get() = MaterialTheme.colorScheme.tertiary + +const val defaultEdgeWidht = 2f + +val defaultBackground + @Composable + get() = MaterialTheme.colorScheme.background + +val defaultTextStyle + @Composable + get() = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + +val defaultLargeTextStyle + @Composable + get() = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.primary, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + +val defaultOnPrimaryLargeTextStyle + @Composable + get() = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onPrimary, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/Edge.kt b/app/src/main/kotlin/app/view/Edge.kt new file mode 100644 index 0000000..a83ee9e --- /dev/null +++ b/app/src/main/kotlin/app/view/Edge.kt @@ -0,0 +1,35 @@ +package app.view + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.zIndex +import binarysearchtrees.Vertex + +@Composable +fun Edge( + start: Vertex, + end: Vertex, + vertexSize: Dp, + scrollDelta: ScrollDelta, + modifier: Modifier = Modifier +) { + val edgeColor = defaultEdgeColor + Canvas(modifier = modifier.zIndex(3f).fillMaxSize()) { + drawLine( + start = Offset( + ((start.value.x + vertexSize / 2) + scrollDelta.x).toPx(), + ((start.value.y + vertexSize / 2) + scrollDelta.y).toPx(), + ), + end = Offset( + ((end.value.x + vertexSize / 2) + scrollDelta.x).toPx(), + ((end.value.y + vertexSize / 2) + scrollDelta.y).toPx(), + ), + strokeWidth = defaultEdgeWidht, + color = edgeColor + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/MainWindow.kt b/app/src/main/kotlin/app/view/MainWindow.kt new file mode 100644 index 0000000..6136d54 --- /dev/null +++ b/app/src/main/kotlin/app/view/MainWindow.kt @@ -0,0 +1,301 @@ +package app.view + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.rememberWindowState +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.redblacktree.RedBlackTree +import java.awt.Dimension + +@OptIn(ExperimentalTextApi::class) +@Composable +fun MainWindow( + tree: BinarySearchTree, + treeName: String, + onCloseRequest: () -> Unit, + title: String = "Trees-12", + icon: Painter? = painterResource("treeIcon.png"), + state: WindowState = rememberWindowState( + position = WindowPosition(alignment = Alignment.Center), + size = DpSize(1100.dp, 815.dp), + ) +) { + Window( + onCloseRequest = onCloseRequest, + title = title, + icon = icon, + state = state + ) { + window.minimumSize = Dimension(800, 600) + Box(Modifier.fillMaxSize().background(defaultBackground).padding(defaultPadding)) { + val scrollDelta = defaultScrollDelta + val indicator = mutableStateOf(0) + + Row(Modifier.fillMaxSize()) { + Box(Modifier.width(300.dp).fillMaxHeight()) { + Column(Modifier.fillMaxSize()) { + Box( + Modifier.height(290.dp).fillMaxWidth().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Image( + painter = painterResource("treeIcon.png"), + contentDescription = "Logotype", + modifier = Modifier.height(200.dp) + .width(400.dp) + .padding(50.dp) + ) + + Text( + "Trees-12", fontSize = 36.sp, + modifier = Modifier.padding(top = 175.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Text( + "Arsene & Artem", fontSize = 19.sp, + modifier = Modifier.padding(top = 215.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + + Box( + Modifier.fillMaxSize().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Panel( + tree, + indicator, + treeName, + scrollDelta, + Modifier.fillMaxSize() + .padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + ) + } + } + } + Box(Modifier.fillMaxSize()) { + Surface( + modifier = Modifier.padding(defaultPadding), + border = BorderStroke(2.dp, defaultVVBrush), + shape = RoundedCornerShape(10.dp) + ) { + TreeView(tree, indicator, defaultVertexSize, scrollDelta) + } + } + } + } + } +} + +@Composable +fun Panel( + tree: BinarySearchTree, + indicator: MutableState, + treeName: String, + scrollDelta: ScrollDelta, + modifier: Modifier +) { + Box( + modifier = modifier + ) { + val stateVertical = rememberScrollState(0) + + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(stateVertical) + .padding(defaultPadding * 2) + ) { + Column( + ) { + TreeTitle(tree, treeName) + Spacer(modifier = Modifier.height(20.dp)) + + TreeButton("Add") { + if (tree[it] == null) { + tree[it] = Position(0.dp, 0.dp) + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + indicator.value = (indicator.value + 1) % 10 + } + } + Spacer(modifier = Modifier.height(20.dp)) + + TreeButton("Delete") { + val pos = tree.remove(it) + if (pos != null) { + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + indicator.value = (indicator.value + 1) % 10 + } + } + Spacer(modifier = Modifier.height(20.dp)) + + TreeButton("Find") { + tree[it]?.let { pos -> + scrollDelta.x = -pos.x + 335.dp + scrollDelta.y = -pos.y + 335.dp + } + } + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = { + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + scrollDelta.x = 0.dp + scrollDelta.y = 0.dp + }, + modifier = Modifier.width(260.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "Reset Positions", style = defaultOnPrimaryLargeTextStyle) + } + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = { TODO() }, + modifier = Modifier.width(260.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "Save Tree", style = defaultOnPrimaryLargeTextStyle) + } + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = { TODO() }, + modifier = Modifier.width(260.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "Delete Tree", style = defaultOnPrimaryLargeTextStyle) + } + } + } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(stateVertical) + ) + } +} + + +@Composable +fun TreeButton( + textButton: String, + action: (String) -> Unit +) { + var text by remember { mutableStateOf("") } + + Row() { + Button( + onClick = { + if (text != "") { + action(text) + text = "" + } + }, + modifier = Modifier.width(115.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = textButton, style = defaultOnPrimaryLargeTextStyle) + } + BasicTextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.width(145.dp).height(45.dp) + .border( + 1.dp, + defaultVVBrush, + RoundedCornerShape(5.dp) + ) + .padding(defaultPadding * 2), + textStyle = defaultLargeTextStyle, + singleLine = true + ) + } +} + +@Composable +fun TreeTitle( + tree: BinarySearchTree, + treeName: String +) { + val treeType: String = when (tree) { + is SimpleBinarySearchTree -> "BST" + is RedBlackTree -> "RBT" + else -> "AVL" + } + Row() { + Box( + modifier = Modifier.width(115.dp).height(45.dp) + .background(defaultBrush, RoundedCornerShape(5.dp)) + .padding(defaultPadding * 2) + ) { + Text( + text = treeName, + style = defaultOnPrimaryLargeTextStyle, + modifier = Modifier.align(Alignment.Center) + ) + } + Box( + modifier = Modifier.width(145.dp).height(45.dp) + .border( + 1.dp, + defaultVVBrush, + RoundedCornerShape(5.dp) + ) + .padding(defaultPadding * 2) + ) { + Text( + text = treeType, + style = defaultLargeTextStyle, + modifier = Modifier.align(Alignment.Center) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/Position.kt b/app/src/main/kotlin/app/view/Position.kt new file mode 100644 index 0000000..d3533c1 --- /dev/null +++ b/app/src/main/kotlin/app/view/Position.kt @@ -0,0 +1,41 @@ +package app.view + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.Vertex + +class Position( + x: Dp, + y: Dp +) { + var x by mutableStateOf(x) + var y by mutableStateOf(y) +} + +fun setTreePositions(tree: BinarySearchTree, vertexSize: Dp, offset: DpOffset) { + tree.getRoot()?.let { setVertexPosition(it, vertexSize, offset.y, offset.x) } +} + +private fun setVertexPosition( + vertex: Vertex, + vertexSize: Dp, + y: Dp, + xOffset: Dp +): Dp { + val leftWidth = vertex.left?.let { + setVertexPosition(it, vertexSize, y + vertexSize, xOffset) + } ?: 0.dp + + val rightWidth = vertex.right?.let { + setVertexPosition(it, vertexSize, y + vertexSize, xOffset + leftWidth + vertexSize) + } ?: 0.dp + + vertex.value.y = y + vertex.value.x = xOffset + leftWidth + return leftWidth + rightWidth + vertexSize +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/ScrollDelta.kt b/app/src/main/kotlin/app/view/ScrollDelta.kt new file mode 100644 index 0000000..8e33ca0 --- /dev/null +++ b/app/src/main/kotlin/app/view/ScrollDelta.kt @@ -0,0 +1,14 @@ +package app.view + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.Dp + +class ScrollDelta( + x: Dp, + y: Dp +) { + var x by mutableStateOf(x) + var y by mutableStateOf(y) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/SelectionWindow.kt b/app/src/main/kotlin/app/view/SelectionWindow.kt new file mode 100644 index 0000000..a63c8d5 --- /dev/null +++ b/app/src/main/kotlin/app/view/SelectionWindow.kt @@ -0,0 +1,264 @@ +package app.view + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberWindowState +import app.controller.Repository + +@Composable +fun SelectionWindow( + getNames: (Repository.TreeType) -> List, + onOpenRequest: (String, Repository.TreeType) -> Unit, + onCloseRequest: () -> Unit, + title: String = "Trees-12", + icon: Painter? = painterResource("treeIcon.png") +) { + Window( + onCloseRequest = onCloseRequest, + title = title, + icon = icon, + state = rememberWindowState( + position = WindowPosition(alignment = Alignment.Center), + size = DpSize(800.dp, 600.dp), + ), + undecorated = true, + resizable = false + ) { + Box(Modifier.fillMaxSize().background(defaultBackground).padding(defaultPadding)) { + Row(Modifier.fillMaxSize()) { + var indicator by remember { mutableStateOf(0) } + Box(Modifier.width(300.dp).fillMaxHeight()) { + Column(Modifier.fillMaxSize()) { + Box( + Modifier.height(290.dp).fillMaxWidth().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Image( + painter = painterResource("treeIcon.png"), + contentDescription = "Logotype", + modifier = Modifier.height(200.dp) + .width(400.dp) + .padding(50.dp) + ) + + Text( + "Trees-12", fontSize = 36.sp, + modifier = Modifier.padding(top = 175.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Text( + "Arsene & Artem", fontSize = 19.sp, + modifier = Modifier.padding(top = 215.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + + Box( + Modifier.fillMaxSize().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Column(Modifier.selectableGroup()) { + Row() { + RadioButton( + selected = (indicator == 1), + onClick = { indicator = 1 } + ) + Box( + modifier = Modifier.height(45.dp).width(245.dp) + .padding(top = defaultPadding * 2, bottom = defaultPadding * 2) + ) { + Text( + text = "Binary Search Tree", + style = defaultLargeTextStyle + ) + } + } + Row() { + RadioButton( + selected = (indicator == 2), + onClick = { indicator = 2 } + ) + Box( + modifier = Modifier.height(45.dp).width(245.dp) + .padding(top = defaultPadding * 2, bottom = defaultPadding * 2) + ) { + Text( + text = "Red Black Tree", + style = defaultLargeTextStyle + ) + } + } + // AVL Tree + } + } + } + } + Box(Modifier.fillMaxSize()) { + Box( + Modifier.fillMaxSize().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Box(Modifier.padding(top = 60.dp).fillMaxWidth().height(550.dp)) { + when (indicator) { + 1 -> Selection({getNames(Repository.TreeType.BST)}, {onOpenRequest(it, Repository.TreeType.BST)}) + 2 -> Selection({getNames(Repository.TreeType.BST)}, {onOpenRequest(it, Repository.TreeType.BST)}) + //3 -> AVLSelection + } + } + } + } + } + } + + IconButton( + onClick = onCloseRequest, + modifier = Modifier.size(40.dp) + .offset(740.dp, 20.dp) + .background(MaterialTheme.colorScheme.primary, CircleShape) + ) { + Icon( + Icons.Filled.Close, + modifier = Modifier.size(50.dp), + contentDescription = "Close application", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } +} + +@Composable +fun Selection( + getNames: () -> List, + onOpenRequest: (String) -> Unit +) { + val names = getNames() + Column(Modifier.fillMaxSize()) { + var indicator by remember { mutableStateOf(-1) } + var newName by remember { mutableStateOf("") } + + + Box(Modifier.fillMaxWidth().height(80.dp).align(Alignment.CenterHorizontally)) { + Button( + onClick = { onOpenRequest(if (indicator == -1) newName else names[indicator]) }, + modifier = Modifier.width(115.dp).height(45.dp).align(Alignment.Center), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "Open", style = defaultOnPrimaryLargeTextStyle) + } + + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + val stateVertical = rememberScrollState(0) + val stateHorizontal = rememberScrollState(0) + + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(stateVertical) + .padding(end = 12.dp, bottom = 12.dp) + .horizontalScroll(stateHorizontal) + ) { + Column(Modifier.selectableGroup()) { + Row() { + RadioButton( + selected = (indicator == -1), + onClick = { indicator = -1 } + ) + BasicTextField( + value = newName, + onValueChange = { if (it.length < 20) newName = it }, + modifier = Modifier.width(345.dp).height(45.dp) + .border( + 1.dp, + defaultVVBrush, + RoundedCornerShape(5.dp) + ) + .padding(defaultPadding * 2), + textStyle = defaultLargeTextStyle, + singleLine = true + ) + } + for (i in names.indices) { + Row() { + RadioButton( + selected = (indicator == i), + onClick = { indicator = i } + ) + Box( + modifier = Modifier.height(45.dp).width(345.dp) + .padding(top = defaultPadding * 2, bottom = defaultPadding * 2) + ) { + Text( + text = names[i], + style = defaultLargeTextStyle + ) + } + } + } + } + } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(stateVertical) + ) + HorizontalScrollbar( + modifier = Modifier.align(Alignment.BottomStart) + .fillMaxWidth() + .padding(end = 12.dp), + adapter = rememberScrollbarAdapter(stateHorizontal) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/TreeView.kt b/app/src/main/kotlin/app/view/TreeView.kt new file mode 100644 index 0000000..300b707 --- /dev/null +++ b/app/src/main/kotlin/app/view/TreeView.kt @@ -0,0 +1,37 @@ +package app.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.Vertex + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun TreeView( + tree: BinarySearchTree, + indicator: State, + vertexSize: Dp, + scrollDelta: ScrollDelta +) { + val scrollCf = defaultScrollCf + + Box(Modifier.fillMaxSize().background(defaultBackground) + .onPointerEvent(PointerEventType.Scroll) { + scrollDelta.x -= it.changes.first().scrollDelta.x.dp * scrollCf + scrollDelta.y -= it.changes.first().scrollDelta.y.dp * scrollCf + } + ) { + tree.getRoot()?.let { + val rootState = remember(indicator.value) { mutableStateOf(it as Vertex) } + VertexView(rootState, vertexSize, scrollDelta) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/VertexView.kt b/app/src/main/kotlin/app/view/VertexView.kt new file mode 100644 index 0000000..9705455 --- /dev/null +++ b/app/src/main/kotlin/app/view/VertexView.kt @@ -0,0 +1,104 @@ +package app.view + +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import binarysearchtrees.Vertex +import binarysearchtrees.redblacktree.Vertex as RBVertex +import binarysearchtrees.redblacktree.Vertex.Color as RBColor + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun VertexView( + vertexState: MutableState>, + vertexSize: Dp, + scrollDelta: ScrollDelta, + modifier: Modifier = Modifier +) { + val vertex by vertexState + + vertex.left?.let { + Edge(vertex, it, vertexSize, scrollDelta) + VertexView(mutableStateOf(it), vertexSize, scrollDelta) + } + vertex.right?.let { + Edge(vertex, it, vertexSize, scrollDelta) + VertexView(mutableStateOf(it), vertexSize, scrollDelta) + } + + val brush = if (vertex is RBVertex) { + if ((vertex as RBVertex).color == RBColor.BLACK) + defaultBlackBrush + else defaultRedBrush + } else defaultBrush + Box( + modifier.zIndex(4f) + .offset(vertex.value.x + scrollDelta.x, vertex.value.y + scrollDelta.y) + .size(vertexSize) + .background( + defaultBackground, + CircleShape + ) + .border(5.dp, brush, CircleShape) + .pointerInput(vertex) { + detectDragGestures { change, dragAmount -> + change.consume() + vertex.value.x += dragAmount.x.toDp() + vertex.value.y += dragAmount.y.toDp() + } + } + ) { + TooltipArea( + modifier = Modifier.zIndex(5f).align(Alignment.Center), + tooltip = { + Surface( + modifier = Modifier.shadow(10.dp), + color = defaultBackground, + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = vertex.key, + modifier = Modifier.padding(defaultPadding * 2), + style = defaultTextStyle + ) + } + }, + delayMillis = 600, // in milliseconds + tooltipPlacement = TooltipPlacement.CursorPoint( + alignment = Alignment.BottomEnd + ) + ) { + VertexText( + text = vertex.key, + modifier = Modifier, + ) + } + } +} + +@Composable +private fun VertexText( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = if (text.length < 6) text else (text.dropLast(text.length - 4) + ".."), + modifier = modifier, + style = defaultTextStyle + ) +} diff --git a/app/src/main/resources/treeIcon.png b/app/src/main/resources/treeIcon.png new file mode 100644 index 0000000..1b6dee9 Binary files /dev/null and b/app/src/main/resources/treeIcon.png differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..78833bb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.9" + +services: + neo4j: + image: neo4j:4.0.5 + container_name: neo4j + ports: + - "7474:7474" + - "7687:7687" + environment: + - NEO4J_AUTH=neo4j/qwerty \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bdc9a83 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..79a61d4 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3428b26 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "trees-12" +include("app", "BinarySearchTrees")