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/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c4ba9b0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Kotlin CI With Gradle + +on: + push: + branches: [ "main", "test" ] + pull_request: + branches: [ "main", "test" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Build with Gradle Wrapper + run: ./gradlew build + + - name: Launch tests + run: ./gradlew :lib:test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54a5f5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b3b1854 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2024 Dmitry Sheiko, Anastasiia Kuzmina, Ilhom Kombaev + +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..3f4f918 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +## NamelessKitty.kt - library that appreciate your data ✨ + +**NamelessKitty.kt** is library that help you work with your data by simple interface, using binary search trees under the hood. + + + +## ⚡️Quick start + +```kotlin +import tree.RBTree + +fun main() { + val rbt = RBTree() + + rbt.set(1, "Hello world") + + println(rbt.search(1)) +} +``` + +Yes, that is really simple. \ +We Love KISS. + +## ✨ Features +- **Null-safety code** - We didn't use anything that could break your code +- **18 public methods** - Thanks to Dima +- **Simple interface** - Our library is easy to use and we proud of it +- **Easy to expand** - We provide simple abstact class that allow you create new type of trees and use it for your tasks +- **Generic types** - Use whatever you want types. We handle with it. + +## 👀 Examples + +#### 📖 **Add array of data** +```kotlin +import tree.RBTree + +fun main() { + val data = arrayOf(122 to "Homka", 21 to "Dima", 25 to "Nastya") + val dataKeys = data.map { it.first }.toTypedArray() + + val rbt = RBTree(data) + rbt.set(data) + rbt.remove(dataKeys) +} +``` + +#### 📖 **Print all data in ascending order** +```kotlin +import tree.RBTree + +fun main() { + val data = arrayOf(122 to "Homka", 21 to "Dima", 25 to "Nastya") + + val rbt = RBTree(data) + + rbt.inOrderTraversal { + println("key: ${it.first}, value: ${it.second}") + } +} +``` + +#### 📖 **Put only first incoming data** +```kotlin +import tree.RBTree + +fun main() { + val data = arrayOf(1 to "Homka", 2 to "Dima", 3 to "Nastya", 1 to "Homka") + + val rbt = RBTree(data) + + data.forEach { + val previousValue = rbt.setIfEmpty(it.first, it.second) + + if (previousValue != null) { + println("Data with key: ${it.first} already exists and contain: ${previousValue}") + } + } +} +``` + +## 🐤 Docs +[docs.md](https://github.com/spbu-coding-2023/trees-3/blob/main/docs.md) + +## 🐹 Gitbub stars +We love stars (ok, just me). I would be very pleased if you put a star on our project. \ +![image](https://github.com/spbu-coding-2023/trees-3/assets/39369841/bb81c1b0-5d69-4b63-9a9a-49b952625ad7) + +## Contacts +- [Dmitry Sheiko](https://github.com/Demon32123) +- [Anastasiia Kuzmina](https://github.com/far1yg) +- [Homa Kombaev](https://github.com/homka122) diff --git a/docs.md b/docs.md new file mode 100644 index 0000000..a64c5f7 --- /dev/null +++ b/docs.md @@ -0,0 +1,60 @@ +## 🐤 Docs +- We have three types of tree: BST, AVL, RBT +- Each tree has constructor that accept: + - Nothing (Empty tree) + - Key and Value + - Array of Pairs of Key\Value +- Each tree has these methods: + - `set(key: K, value: V): V?` \ + Stores the value for the given key. Return previous value. + + - `set(pairs: Array>): MutableList` \ + Stores the values for the given keys. Return previous values. + + - `setIfEmpty(key: K, value: V): V?` \ + Stores the value for the given key if there is no pair with that key. Return previous value. + + - `setIfEmpty(pairs: Array>): MutableList` \ + Stores the values for the given keys if there is no pair with that key. Return previous values. + + - `remove(key: K): V?` \ + Remove the value for the given key. Return previous value. + + - `remove(keys: Array): MutableList` \ + Remove the values for the given keys. Return previous values. + + - `search(key: K): V?` \ + Return the value for the given key. + + - `getKeys(): List` \ + Returns a complete list of keys. + + - `getValues(): List` \ + Returns a complete list of values. + + - `getEntities(): List>` \ + Returns a complete list of pairs key value. + + - `getMin(): Pair` \ + Returns pair with the minimum key. + + - `getMax(): Pair` \ + Returns pair with the maximum key. + + - `successor(key: K): Pair` \ + Returns the pair with next ascending key. + + - `predecessor(key: K): Pair` \ + Returns the pair with previous ascending key + + - `clear()` \ + Remove all keys in a tree. + + - `preOrderTraversal(action: (Pair) -> (Unit))` \ + Apply [action] on all pairs by preorder tree traversal. + + - `inOrderTraversal(action: (Pair) -> (Unit))` \ + Apply [action] on all pairs by inorder tree traversal. + + - `postOrderTraversal(action: (Pair) -> (Unit))` \ + Apply [action] on all pairs by postorder tree traversal. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d4d7e1f --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,15 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +commons-math3 = "3.6.1" +guava = "32.1.3-jre" +junit-jupiter-engine = "5.10.0" + +[libraries] +commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter-engine" } + +[plugins] +jvm = { id = "org.jetbrains.kotlin.jvm", version = "1.9.20" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 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..a80b22c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/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##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +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=SC2039,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=SC2039,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 + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +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..25da30d --- /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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 0000000..8cb3007 --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,61 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Kotlin library project to get you started. + * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.6/userguide/building_java_projects.html in the Gradle documentation. + */ + +plugins { + // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. + alias(libs.plugins.jvm) + + // Apply the java-library plugin for API and implementation separation. + `java-library` + jacoco +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // Use the Kotlin JUnit 5 integration. + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + + // Use the JUnit 5 integration. + testImplementation(libs.junit.jupiter.engine) + testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + // This dependency is exported to consumers, that is to say found on their compile classpath. + api(libs.commons.math3) + + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + implementation(libs.guava) +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +tasks.named("test") { + // Use JUnit Platform for unit tests. + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} + +tasks.jacocoTestReport { + dependsOn(tasks.named("test")) // tests are required to run before generating the report + + reports { + xml.required = false + csv.required = false + html.required = true + html.outputLocation = layout.buildDirectory.dir("jacocoHtml") + } +} diff --git a/lib/src/main/kotlin/Main.kt b/lib/src/main/kotlin/Main.kt new file mode 100644 index 0000000..54d3674 --- /dev/null +++ b/lib/src/main/kotlin/Main.kt @@ -0,0 +1,5 @@ +import tree.node.BinaryTreeNode + +fun main() { + println("Hello world!") +} \ No newline at end of file diff --git a/lib/src/main/kotlin/tree/AVLTree.kt b/lib/src/main/kotlin/tree/AVLTree.kt new file mode 100644 index 0000000..62bc384 --- /dev/null +++ b/lib/src/main/kotlin/tree/AVLTree.kt @@ -0,0 +1,161 @@ +package tree + +import tree.node.AVLTreeNode + + +class AVLTree, V> : SearchTree>() { + + private fun height(node: AVLTreeNode?): Int { + return node?.height ?: 0 + } + + private fun updateHeight(node: AVLTreeNode?) { + if (node != null) { + node.height = 1 + maxOf(height(node.left), height(node.right)) + } + } + + private fun getBalance(node: AVLTreeNode): Int { + return height(node.left) - height(node.right) + } + + private fun rotateRight(node: AVLTreeNode): AVLTreeNode? { + val nodeLeft = node.left + val nodeLeftRight = nodeLeft?.right + + nodeLeft?.right = node + node.left = nodeLeftRight + + updateHeight(node) + updateHeight(nodeLeft) + + return nodeLeft + } + + private fun rotateLeft(node: AVLTreeNode): AVLTreeNode? { + val nodeRight = node.right + val nodeRightLeft = nodeRight?.left + + nodeRight?.left = node + node.right = nodeRightLeft + + updateHeight(node) + updateHeight(nodeRight) + + return nodeRight + } + + private fun rebalanced(root: AVLTreeNode?, node: AVLTreeNode?): AVLTreeNode?{ + updateHeight(root) + val balance = root?.let { getBalance(it) } + val rootLeft = root?.left + val rootRight = root?.right + + if (node != null && balance != null){ + if (balance > 1 && rootLeft != null) { + if (node.key < rootLeft.key) { + + return rotateRight(root) + } + + root.left = root.left?.let { rotateLeft(it) } + return rotateRight(root) + } + + if (balance < -1 && rootRight != null) { + if (node.key > rootRight.key) { + return rotateLeft(root) + } + + root.right = root.right?.let { rotateRight(it) } + + return rotateLeft(root) + } + } + + return root + } + + private fun insertRecursive(root: AVLTreeNode?, node: AVLTreeNode?): AVLTreeNode? { + if (root == null) { + return node + } + + if (node != null) { + if (root.key > node.key) { + root.left = insertRecursive(root.left, node) + } else if (root.key < node.key) { + root.right = insertRecursive(root.right, node) + } + } + + return rebalanced(root, node) + } + + override fun insertNode(node: AVLTreeNode) { + root = insertRecursive(root, node) + } + + private fun minValueNode(node: AVLTreeNode?): AVLTreeNode? { + var current = node + + while (current?.left != null) current = current.left + + return current + } + private fun changeNode(oldNode: AVLTreeNode, newNode: AVLTreeNode?): AVLTreeNode? { + if (newNode == null) { + return null + } + + val node = createNode(newNode.key, newNode.value) + + node.right = oldNode.right + node.left = oldNode.left + node.height = oldNode.height + + return node + } + + private fun removeRecursive(root: AVLTreeNode?, node: AVLTreeNode): AVLTreeNode? { + var rootNode = root + + if (rootNode == null) return rootNode + + if (rootNode.key > node.key) { + rootNode.left = removeRecursive(rootNode.left, node) + } else if (rootNode.key < node.key) { + rootNode.right = removeRecursive(rootNode.right, node) + } + else{ + if (rootNode.left == null || rootNode.right == null) { + var newNode: AVLTreeNode? = null + newNode = rootNode.left ?: rootNode.right + if (newNode == null) { + rootNode = null + } else { + rootNode = newNode + } + } else { + val newNode = minValueNode(rootNode.right) + rootNode = changeNode(rootNode, newNode) + + if (rootNode != null) { + rootNode.right = newNode?.let { removeRecursive(rootNode.right, it) } + } + } + } + + if (rootNode == null) return rootNode + + return rebalanced(rootNode, node) + } + + override fun removeNode(node: AVLTreeNode) { + root = removeRecursive(root, node) + } + + override fun createNode(key: K, value: V): AVLTreeNode { + return AVLTreeNode(key, value) + } +} diff --git a/lib/src/main/kotlin/tree/BSTree.kt b/lib/src/main/kotlin/tree/BSTree.kt new file mode 100644 index 0000000..f42d707 --- /dev/null +++ b/lib/src/main/kotlin/tree/BSTree.kt @@ -0,0 +1,85 @@ +package tree + +import tree.node.BSTreeNode + +class BSTree, V> : SearchTree> { + + constructor() : super() + constructor(key: K, value: V) : super(key, value) + constructor(pairs: Array>) : super(pairs) + + override fun insertNode(node: BSTreeNode) { + var parentNode = root + var treeNode = root + + while (treeNode != null) { + parentNode = treeNode + if (node.key < treeNode.key) { + treeNode = treeNode.left + } else { + treeNode = treeNode.right + } + } + + when { + parentNode == null -> root = node + node.key < parentNode.key -> parentNode.left = node + else -> parentNode.right = node + } + } + + private fun searchParentNode(node: BSTreeNode, parentNode: BSTreeNode): BSTreeNode? { + if (parentNode.left == node || parentNode.right == node) { + return parentNode + } + + if (node.key < parentNode.key) { + return parentNode.left?.let { searchParentNode(node, it) } + } else { + return parentNode.right?.let { searchParentNode(node, it) } + } + } + + private fun getMinSubtree(node: BSTreeNode): BSTreeNode { + val nodeN = node.left + if (nodeN == null) { + return node + } else { + return getMinSubtree(nodeN) + } + } + + private fun identifyChild(parentNode: BSTreeNode?, node: BSTreeNode, value: BSTreeNode?) { + when { + parentNode == null -> root = value + parentNode.left == node -> parentNode.left = value + else -> parentNode.right = value + } + } + + override fun removeNode(node: BSTreeNode) { + val parentNode = root?.let { searchParentNode(node, it) } + val nodeRight = node.right + val nodeLeft = node.left + + if (nodeLeft == null && nodeRight == null) { + return identifyChild(parentNode, node, null) + } + + if (nodeLeft == null || nodeRight == null) { + val notNullNode = nodeLeft ?: nodeRight + + return identifyChild(parentNode, node, notNullNode) + } + + val successor = getMinSubtree(nodeRight) + removeNode(successor) + + successor.left = node.left + successor.right = node.right + + identifyChild(parentNode, node, successor) + } + + override fun createNode(key: K, value: V): BSTreeNode = BSTreeNode(key, value) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/tree/RBTree.kt b/lib/src/main/kotlin/tree/RBTree.kt new file mode 100644 index 0000000..0120901 --- /dev/null +++ b/lib/src/main/kotlin/tree/RBTree.kt @@ -0,0 +1,278 @@ +package tree + +import tree.node.RBTreeColor +import tree.node.RBTreeNode + +class RBTree, V> : SearchTree> { + constructor() : super() + constructor(key: K, value: V) : super(key, value) + constructor(pairs: Array>) : super(pairs) + + override fun insertNode(node: RBTreeNode) { + // put node like we are in BST + var tmpNode = root + var tmpNodeParent: RBTreeNode? = null + + while (tmpNode != null) { + tmpNodeParent = tmpNode + tmpNode = if (node.key < tmpNode.key) tmpNode.left else tmpNode.right + } + + // that mean tree is empty + if (tmpNodeParent == null) { + root = node + node.color = RBTreeColor.BLACK + return + } + + node.parent = tmpNodeParent + if (node.key < tmpNodeParent.key) { + tmpNodeParent.left = node + } else { + tmpNodeParent.right = node + } + + // if node has no grandpa all fine + if (tmpNodeParent.parent == null) { + return + } + + insertFix(node) + } + + private fun insertFix(newNode: RBTreeNode) { + var node = newNode + + while (node.parent?.color === RBTreeColor.RED) { + var parent = node.parent ?: throw IllegalStateException() + var grandpa = parent.parent ?: throw IllegalStateException() + + if (parent == grandpa.left) { + val uncle = grandpa.right + + // uncle is red + if (uncle != null && uncle.color == RBTreeColor.RED) { + uncle.color = RBTreeColor.BLACK + parent.color = RBTreeColor.BLACK + grandpa.color = RBTreeColor.RED + node = grandpa + continue + } + + // uncle is black (null node = black) + if (node == parent.right) { + node = parent + leftRotate(node) + parent = node.parent ?: throw IllegalStateException() + grandpa = parent.parent ?: throw IllegalStateException() + } + + parent.color = RBTreeColor.BLACK + grandpa.color = RBTreeColor.RED + rightRotate(grandpa) + continue + } + + // parent is right child of grandpa + val uncle = grandpa.left + + // uncle is red + if (uncle != null && uncle.color == RBTreeColor.RED) { + uncle.color = RBTreeColor.BLACK + parent.color = RBTreeColor.BLACK + grandpa.color = RBTreeColor.RED + node = grandpa + continue + } + + // uncle is black + if (node == parent.left) { + node = parent + rightRotate(node) + parent = node.parent ?: throw IllegalStateException() + grandpa = parent.parent ?: throw IllegalStateException() + } + + parent.color = RBTreeColor.BLACK + grandpa.color = RBTreeColor.RED + leftRotate(grandpa) + } + + root?.color = RBTreeColor.BLACK + } + + override fun removeNode(node: RBTreeNode) { + val transplantedNode: RBTreeNode? + + var originalColor = node.color + val leftNode = node.left + val rightNode = node.right + + if (leftNode == null) { + transplantedNode = rightNode + rbTransplant(node, rightNode) + } else if (rightNode == null) { + transplantedNode = leftNode + rbTransplant(node, leftNode) + } else { + val minNode = getMinNode(rightNode) + + originalColor = minNode.color + transplantedNode = minNode.right + if (minNode.parent == node) { + transplantedNode?.parent = minNode + } else { + rbTransplant(minNode, minNode.right) + minNode.right = node.right + minNode.right?.parent = minNode + } + + rbTransplant(node, minNode) + minNode.left = node.left + minNode.left?.parent = minNode + minNode.color = node.color + } + + if (originalColor == RBTreeColor.BLACK && transplantedNode != null) { + deleteFix(transplantedNode) + } + } + + override fun createNode(key: K, value: V): RBTreeNode { + return RBTreeNode(key, value) + } + + private fun isBlack(node: RBTreeNode?): Boolean { + return node == null || node.color == RBTreeColor.BLACK + } + + private fun deleteFix(transplantedNode: RBTreeNode) { + var node: RBTreeNode? = transplantedNode + + while (node != root && node?.color == RBTreeColor.BLACK) { + val parentNode = node.parent ?: throw IllegalStateException() + + if (node == parentNode.left) { + var uncle = parentNode.right ?: return + + if (uncle.color == RBTreeColor.RED) { + uncle.color = RBTreeColor.BLACK + parentNode.color = RBTreeColor.RED + leftRotate(parentNode) + uncle = parentNode.right ?: return + } + + if (isBlack(uncle.left) && isBlack(uncle.right)) { + uncle.color = RBTreeColor.RED + node = parentNode + } else { + if (isBlack(uncle.right)) { + uncle.left?.color = RBTreeColor.BLACK + uncle.color = RBTreeColor.RED + rightRotate(uncle) + uncle = parentNode.right ?: throw IllegalStateException() + } + + uncle.color = parentNode.color + parentNode.color = RBTreeColor.BLACK + uncle.right?.color = RBTreeColor.BLACK + leftRotate(parentNode) + node = root + } + } else { + var uncle = parentNode.left ?: return + + if (uncle.color == RBTreeColor.RED) { + uncle.color = RBTreeColor.BLACK + parentNode.color = RBTreeColor.RED + rightRotate(parentNode) + uncle = parentNode.left ?: return + } + + if (isBlack(uncle.left) && isBlack(uncle.right)) { + uncle.color = RBTreeColor.RED + node = parentNode + } else { + if (isBlack(uncle.left)) { + uncle.right?.color = RBTreeColor.BLACK + uncle.color = RBTreeColor.RED + leftRotate(uncle) + uncle = parentNode.left ?: throw IllegalStateException() + } + + uncle.color = parentNode.color + parentNode.color = RBTreeColor.BLACK + uncle.left?.color = RBTreeColor.BLACK + rightRotate(parentNode) + node = root + } + } + } + } + + private tailrec fun getMinNode(node: RBTreeNode): RBTreeNode { + val leftNode = node.left + + if (leftNode == null) { + return node + } else { + return getMinNode(leftNode) + } + } + + private fun rbTransplant(previous: RBTreeNode, curr: RBTreeNode?) { + val parent = previous.parent + + if (parent == null) { + root = curr + } else if (previous == parent.left) { + parent.left = curr + } else { + parent.right = curr + } + + if (curr != null) { + curr.parent = parent + } + } + + private fun leftRotate(node: RBTreeNode) { + val rightNode = node.right ?: throw IllegalStateException() + + node.right = rightNode.left + rightNode.left?.parent = node + rightNode.parent = node.parent + + val parent = node.parent + if (parent == null) { + root = rightNode + } else if (parent.left == node) { + parent.left = rightNode + } else { + parent.right = rightNode + } + + rightNode.left = node + node.parent = rightNode + } + + private fun rightRotate(node: RBTreeNode) { + val leftNode = node.left ?: throw IllegalStateException() + + node.left = leftNode.right + leftNode.right?.parent = node + leftNode.parent = node.parent + + val parent = node.parent + if (parent == null) { + root = leftNode + } else if (parent.left == node) { + parent.left = leftNode + } else { + parent.right = leftNode + } + + leftNode.right = node + node.parent = leftNode + } +} diff --git a/lib/src/main/kotlin/tree/SearchTree.kt b/lib/src/main/kotlin/tree/SearchTree.kt new file mode 100644 index 0000000..f43fbc9 --- /dev/null +++ b/lib/src/main/kotlin/tree/SearchTree.kt @@ -0,0 +1,291 @@ +package tree + +import tree.node.BinaryTreeNode + +abstract class SearchTree, V, Node : BinaryTreeNode>() { + protected var root: Node? = null + var size: Long = 0 + private set + var recentlyKey: K? = null + private set + + constructor(key: K, value: V) : this() { + set(key, value) + } + + constructor(pairs: Array>) : this() { + set(pairs) + } + + protected abstract fun insertNode(node: Node) + protected abstract fun removeNode(node: Node) + protected abstract fun createNode(key: K, value: V): Node + + /** + * Finding a node in a tree by key + */ + protected fun searchNode(key: K): Node? { + var node = root + + while (node != null) { + when { + key < node.key -> node = node.left + key > node.key -> node = node.right + else -> return node + } + } + + return null + } + + /** + * Stores the value for the given key. Return previous value. + */ + fun set(key: K, value: V): V? { + recentlyKey = key + val node = searchNode(key) + + if (node == null) { + insertNode(createNode(key, value)) + size++ + return null + } + + val result = node.value + node.value = value + return result + } + + /** + * Stores the values for the given keys. Return previous values. + */ + fun set(pairs: Array>): MutableList { + val listValue = mutableListOf() + + for (pair in pairs) { + listValue.add(set(pair.first, pair.second)) + } + + return listValue + } + + /** + * Stores the value for the given key if there is no pair with that key. Return previous value. + */ + fun setIfEmpty(key: K, value: V): V? { + val node = searchNode(key) + + if (node == null) { + recentlyKey = key + insertNode(createNode(key, value)) + size++ + return null + } + + return node.value + } + + /** + * Stores the values for the given keys if there is no pair with that key. Return previous values. + */ + fun setIfEmpty(pairs: Array>): MutableList { + val listValue = mutableListOf() + + for (pair in pairs) { + listValue.add(setIfEmpty(pair.first, pair.second)) + } + + return listValue + } + + /** + * Remove the value for the given key. Return previous value. + */ + fun remove(key: K): V? { + val node = searchNode(key) + + if (node != null) { + removeNode(node) + size-- + return node.value + } + + return null + } + + /** + * Remove the values for the given keys. Return previous values. + */ + fun remove(keys: Array): MutableList { + val listValue = mutableListOf() + + for (key in keys) { + listValue.add(remove(key)) + } + + return listValue + } + + /** + * Return the value for the given key. + */ + fun search(key: K): V? { + return searchNode(key)?.value + } + + /** + * Returns a complete list of keys. + */ + fun getKeys(): List { + val result = mutableListOf() + + inOrderTraversal { + result.add(it.first) + } + + return result + } + + /** + * Returns a complete list of values. + */ + fun getValues(): List { + val result = mutableListOf() + + inOrderTraversal { + result.add(it.second) + } + + return result + } + + /** + * Returns a complete list of pairs key value. + */ + fun getEntities(): List> { + val result = mutableListOf>() + + inOrderTraversal { result.add(it) } + + return result + } + + /** + * Returns pair with the minimum key. + */ + fun getMin(): Pair { + var node = root + while (node?.left != null) { + node = node.left + } + + return Pair(node?.key, node?.value) + } + + /** + * Returns pair with the maximum key. + */ + fun getMax(): Pair { + var node = root + while (node?.right != null) { + node = node.right + } + return Pair(node?.key, node?.value) + } + + /** + * Returns the pair with next ascending key + */ + fun successor(key: K): Pair { + var node = root + var successor: Node? = null + + while (node != null) { + if (node.key > key) { + successor = node + node = node.left + } else { + node = node.right + } + } + + return Pair(successor?.key, successor?.value) + } + + /** + * Returns the pair with previous ascending key + */ + fun predecessor(key: K): Pair { + var node = root + var predecessor: Node? = null + + while (node != null) { + if (node.key < key) { + predecessor = node + node = node.right + } else { + node = node.left + } + } + + return Pair(predecessor?.key, predecessor?.value) + } + + /** + * Remove all keys in a tree. + */ + fun clear() { + root = null + size = 0 + } + + /** + * Apply [action] on all pairs by preorder tree traversal. + */ + fun preOrderTraversal(action: (Pair) -> (Unit)) { + val root = this.root ?: return + + fun preOrder(node: Node?) { + if (node == null) return + + action(Pair(node.key, node.value)) + preOrder(node.left) + preOrder(node.right) + + } + + preOrder(root) + } + + /** + * Apply [action] on all pairs by inorder tree traversal. + */ + fun inOrderTraversal(action: (Pair) -> (Unit)) { + fun inOrder(node: Node?) { + if (node == null) return + + inOrder(node.left) + action(Pair(node.key, node.value)) + inOrder(node.right) + } + + inOrder(this.root) + } + + /** + * Apply [action] on all pairs by postorder tree traversal. + */ + fun postOrderTraversal(action: (Pair) -> (Unit)) { + val root = this.root ?: return + + fun helper(node: Node?) { + if (node == null) return + + helper(node.left) + helper(node.right) + action(Pair(node.key, node.value)) + } + + helper(root) + } +} diff --git a/lib/src/main/kotlin/tree/node/AVLTreeNode.kt b/lib/src/main/kotlin/tree/node/AVLTreeNode.kt new file mode 100644 index 0000000..bbd2c26 --- /dev/null +++ b/lib/src/main/kotlin/tree/node/AVLTreeNode.kt @@ -0,0 +1,4 @@ +package tree.node + +class AVLTreeNode, V>(key: K, value: V, var height: Int = 1) : + BinaryTreeNode>(key, value) \ No newline at end of file diff --git a/lib/src/main/kotlin/tree/node/BSTreeNode.kt b/lib/src/main/kotlin/tree/node/BSTreeNode.kt new file mode 100644 index 0000000..47b8fde --- /dev/null +++ b/lib/src/main/kotlin/tree/node/BSTreeNode.kt @@ -0,0 +1,3 @@ +package tree.node + +class BSTreeNode, V>(key: K, value: V) : BinaryTreeNode>(key, value) \ No newline at end of file diff --git a/lib/src/main/kotlin/tree/node/BinaryTreeNode.kt b/lib/src/main/kotlin/tree/node/BinaryTreeNode.kt new file mode 100644 index 0000000..fe7af4e --- /dev/null +++ b/lib/src/main/kotlin/tree/node/BinaryTreeNode.kt @@ -0,0 +1,8 @@ +package tree.node + +abstract class BinaryTreeNode, V, Node>( + val key: K, + var value: V, + var right: Node? = null, + var left: Node? = null +) \ No newline at end of file diff --git a/lib/src/main/kotlin/tree/node/RBTreeColor.kt b/lib/src/main/kotlin/tree/node/RBTreeColor.kt new file mode 100644 index 0000000..a8b0a8d --- /dev/null +++ b/lib/src/main/kotlin/tree/node/RBTreeColor.kt @@ -0,0 +1,5 @@ +package tree.node + +enum class RBTreeColor { + BLACK, RED +} \ No newline at end of file diff --git a/lib/src/main/kotlin/tree/node/RBTreeNode.kt b/lib/src/main/kotlin/tree/node/RBTreeNode.kt new file mode 100644 index 0000000..298b67e --- /dev/null +++ b/lib/src/main/kotlin/tree/node/RBTreeNode.kt @@ -0,0 +1,8 @@ +package tree.node + +class RBTreeNode, V>( + key: K, + value: V, + var color: RBTreeColor = RBTreeColor.RED, + var parent: RBTreeNode? = null +) : BinaryTreeNode>(key, value) \ No newline at end of file diff --git a/lib/src/test/kotlin/tree/AVLTreeTest.kt b/lib/src/test/kotlin/tree/AVLTreeTest.kt new file mode 100644 index 0000000..ef416b3 --- /dev/null +++ b/lib/src/test/kotlin/tree/AVLTreeTest.kt @@ -0,0 +1,154 @@ +package tree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.random.Random + +class AVLTreeTest { + private val tree = AVLTree() + + @Nested + inner class `set test` { + + @Test + fun `base test`(){ + tree.set(1, 1) + assertEquals(1, tree.size) + assertEquals(listOf(1), tree.getKeys()) + } + + @Test + fun `many node`() { + val entities = arrayOf( + Pair(35, 1), + Pair(21, 1), + Pair(25, 1), + Pair(62, 1), + Pair(12, 1), + Pair(62, 1), + Pair(122, 1), + Pair(621, 1), + Pair(121, 1), + Pair(362, 1), + Pair(35, 1), + Pair(523, 1), + ) + + tree.set(entities) + assertEquals(10, tree.size) + assertEquals(listOf( + 12, 21, 25, 35, 62, 121, 122, 362, 523, 621 + ), tree.getKeys()) + } + + @Test + fun `test right`() { + tree.set(arrayOf( + Pair(2, 1), + Pair(4, 1), + Pair(6, 1), + Pair(8, 1), + Pair(3, 1), + Pair(9, 1))) + + assertEquals(6, tree.size) + assertEquals( + listOf( + 2, 3, 4, 6, 8, 9 + ), tree.getKeys() + ) + } + + @Test + fun `test left`() { + tree.set(5, 1) + tree.set(4, 1) + tree.set(2, 1) + tree.set(1, 1) + assertEquals(4, tree.size) + assertEquals(listOf( + 1, 2, 4, 5 + ), tree.getKeys()) + } + + @Test + fun `stress test`() { + val tree = AVLTree() + val generator = Random(7) + val setWithNull: (Int) -> (Unit) = { key -> tree.set(key, null) } + + val randomKeys = mutableListOf() + for (i in 1..1000000) { + val randomValue = generator.nextInt() + randomKeys.add(randomValue) + setWithNull(randomValue) + } + + val randomKeysDistinct = randomKeys.distinct() + assertEquals(randomKeysDistinct.sorted(), tree.getKeys()) + assertEquals(randomKeysDistinct.size.toLong(), tree.size) + } + } + + @Nested + inner class `remove test`{ + @Test + fun `base test`() { + tree.set(1, 1) + tree.set(2, 1) + tree.remove(2) + assertEquals(1, tree.size) + assertEquals(listOf(1), tree.getKeys()) + } + + @Test + fun `remove root`(){ + tree.set(1, 1) + tree.set(2, 1) + tree.set(3, 1) + tree.remove(2) + assertEquals(2, tree.size) + assertEquals(listOf(1, 3), tree.getKeys()) + } + + @Test + fun `remove last node`(){ + tree.set(1, 1) + tree.set(2, 1) + tree.set(3, 1) + tree.remove(3) + assertEquals(2, tree.size) + assertEquals(listOf(1, 2), tree.getKeys()) + } + + @Test + fun `many node`() { + val entities = arrayOf( + Pair(35, 1), + Pair(21, 1), + Pair(25, 1), + Pair(62, 1), + Pair(12, 1), + Pair(122, 1), + Pair(621, 1), + Pair(121, 1), + Pair(362, 1), + Pair(523, 1), + ) + + tree.set(entities) + tree.remove( + arrayOf( + 35, 12, 523 + ) + ) + assertEquals(7, tree.size) + assertEquals( + listOf( + 21, 25, 62, 121, 122, 362, 621 + ), tree.getKeys() + ) + } + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/tree/BSTreeTest.kt b/lib/src/test/kotlin/tree/BSTreeTest.kt new file mode 100644 index 0000000..02f21e9 --- /dev/null +++ b/lib/src/test/kotlin/tree/BSTreeTest.kt @@ -0,0 +1,244 @@ +package tree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class BSTreeTest { + private lateinit var bst: BSTree + private val bstEmpty = BSTree() + + @Nested + inner class `Constructor tests` { + @Test + fun `tree without args`() { + val bst = BSTree() + assertEquals(0, bst.size) + } + + @Test + fun `tree with one arg`() { + val bst = BSTree(1, "A") + assertEquals(listOf(Pair(1, "A")), bst.getEntities()) + } + + @Test + fun `tree with some args`() { + val bst: BSTree = BSTree(arrayOf(Pair(1, "A"), Pair(2, "B"), Pair(3, "C"))) + + assertEquals(3, bst.size) + assertEquals(listOf(Pair(1, "A"), Pair(2, "B"), Pair(3, "C")), bst.getEntities()) + } + } + + + @Nested + inner class `Insert tests` { + @BeforeEach + fun setup() { + bst = BSTree( + arrayOf( + Pair(2, "B"), Pair(3, "C"), Pair(4, "D"), + Pair(6, "F"), Pair(7, "G"), Pair(8, "H") + ) + ) + } + + @Test + fun `set new min key`() { + bst.set(1, "A") + assertEquals(7, bst.size) + assertEquals( + listOf( + Pair(1, "A"), Pair(2, "B"), Pair(3, "C"), Pair(4, "D"), + Pair(6, "F"), Pair(7, "G"), Pair(8, "H") + ), bst.getEntities() + ) + } + + @Test + fun `set new max key`() { + bst.set(9, "I") + assertEquals(7, bst.size) + assertEquals( + listOf( + Pair(2, "B"), Pair(3, "C"), Pair(4, "D"), Pair(6, "F"), + Pair(7, "G"), Pair(8, "H"), Pair(9, "I") + ), bst.getEntities() + ) + } + + @Test + fun `set new key inside tree`() { + bst.set(5, "E") + assertEquals(7, bst.size) + assertEquals( + listOf( + Pair(2, "B"), Pair(3, "C"), Pair(4, "D"), Pair(5, "E"), + Pair(6, "F"), Pair(7, "G"), Pair(8, "H") + ), bst.getEntities() + ) + } + + @Test + fun `set the same key`() { + bst.set(7, "g") + assertEquals(6, bst.size) + assertEquals( + listOf( + Pair(2, "B"), Pair(3, "C"), Pair(4, "D"), + Pair(6, "F"), Pair(7, "g"), Pair(8, "H") + ), bst.getEntities() + ) + } + + @Test + fun `set key in empty tree`() { + bstEmpty.set(1, "A") + assertEquals(listOf(Pair(1, "A")), bstEmpty.getEntities()) + assertEquals(1, bstEmpty.size) + } + } + + @Nested + inner class `Remove tests` { + + @Test + fun `remove root without children`() { + bst = BSTree(2, "B") + + assertEquals("B", bst.remove(2)) + assertEquals(0, bst.size) + } + + @Test + fun `remove root with two children`() { + // successor root is root.right + bst = BSTree( + arrayOf( + Pair(2, "B"), Pair(3, "C"), Pair(1, "A"), + Pair(0, "Z"), Pair(5, "E"), Pair(4, "D") + ) + ) + + assertEquals("B", bst.remove(2)) + assertEquals(5, bst.size) + assertEquals(listOf(0, 1, 3, 4, 5), bst.getKeys()) + } + + @Test + fun `remove root with two children1`() { + // successor root is list + bst = BSTree( + arrayOf( + Pair(2, "B"), Pair(1, "A"), Pair(0, "Z"), + Pair(6, "F"), Pair(4, "D"), Pair(5, "E") + ) + ) + + assertEquals("B", bst.remove(2)) + assertEquals(5, bst.size) + assertEquals(listOf(0, 1, 4, 5, 6), bst.getKeys()) + } + + @Test + fun `remove root with two children2`() { + // successor root has one child + bst = BSTree( + arrayOf( + Pair(2, "B"), Pair(1, "A"), Pair(0, "Z"), Pair(6, "F"), + Pair(7, "G"), Pair(4, "D"), Pair(5, "E") + ) + ) + + assertEquals("B", bst.remove(2)) + assertEquals(6, bst.size) + assertEquals(listOf(0, 1, 4, 5, 6, 7), bst.getKeys()) + } + + @Test + fun `remove root with two children3`() { + // successor root has a subtree + bst = BSTree( + arrayOf( + Pair(2, "B"), Pair(1, "A"), Pair(0, "Z"), Pair(8, "K"), + Pair(3, "C"), Pair(5, "E"), Pair(4, "D"), Pair(6, "F") + ) + ) + + assertEquals("B", bst.remove(2)) + assertEquals(7, bst.size) + assertEquals(listOf(0, 1, 3, 4, 5, 6, 8), bst.getKeys()) + } + + @Test + fun `remove root without left child`() { + bst = BSTree(arrayOf(Pair(2, "B"), Pair(3, "C"), Pair(5, "E"), Pair(4, "D"))) + + assertEquals("B", bst.remove(2)) + assertEquals(3, bst.size) + assertEquals(listOf(3, 4, 5), bst.getKeys()) + } + + @Test + fun `remove root without right child`() { + bst = BSTree(arrayOf(Pair(6, "F"), Pair(3, "C"), Pair(5, "E"), Pair(4, "D"))) + + assertEquals("F", bst.remove(6)) + assertEquals(3, bst.size) + assertEquals(listOf(3, 4, 5), bst.getKeys()) + } + + @Test + fun `remove key without left child`() { + bst = BSTree(arrayOf(Pair(1, "A"), Pair(2, "B"), Pair(3, "C"))) + + assertEquals("B", bst.remove(2)) + assertEquals(2, bst.size) + assertEquals(listOf(Pair(1, "A"), Pair(3, "C")), bst.getEntities()) + } + + @Test + fun `remove key without right child`() { + bst = BSTree(arrayOf(Pair(3, "C"), Pair(2, "B"), Pair(1, "A"))) + + assertEquals("B", bst.remove(2)) + assertEquals(2, bst.size) + assertEquals(listOf(Pair(1, "A"), Pair(3, "C")), bst.getEntities()) + } + + @Test + fun `remove key with two children`() { + bst = BSTree(arrayOf(Pair(1, "A"), Pair(3, "C"), Pair(2, "B"), Pair(4, "D"))) + + assertEquals("C", bst.remove(3)) + assertEquals(3, bst.size) + assertEquals(listOf(Pair(1, "A"), Pair(2, "B"), Pair(4, "D")), bst.getEntities()) + } + + @Test + fun `remove key that is not in the tree`() { + bst = BSTree(arrayOf(Pair(2, "B"), Pair(3, "C"), Pair(0, "Z"))) + + assertEquals(null, bst.remove(1)) + assertEquals(3, bst.size) + assertEquals(listOf(Pair(0, "Z"), Pair(2, "B"), Pair(3, "C")), bst.getEntities()) + } + + @Test + fun `remove key from empty tree`() { + assertEquals(null, bstEmpty.remove(1)) + assertEquals(0, bstEmpty.size) + } + } + + @Nested + inner class `Create tests` { + @Test + fun `insert new node`() { + bst = BSTree(1, "A") + assertEquals(listOf(Pair(1, "A")), bst.getEntities()) + } + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/tree/RBTreeTest.kt b/lib/src/test/kotlin/tree/RBTreeTest.kt new file mode 100644 index 0000000..89635ed --- /dev/null +++ b/lib/src/test/kotlin/tree/RBTreeTest.kt @@ -0,0 +1,314 @@ +package tree + +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import kotlin.random.Random + +class RBTreeTest { + private lateinit var rbt: RBTree + + private fun checkSize(size: Long, rbt: RBTree = this.rbt) { + assertEquals(size, rbt.size) + checkCountNodes(size.toInt(), rbt) + } + + private fun checkCountNodes(count: Int, rbt: RBTree = this.rbt) { + assertEquals(count, rbt.getEntities().size) + } + + private fun checkValue(key: Int, value: String, rbt: RBTree = this.rbt) { + assertEquals(value, rbt.search(key)) + } + + private fun checkValues(data: Array>) { + data.distinctBy { it.first }.forEach { + checkValue(it.first, it.second) + } + } + + private fun checkKeys(keys: List, rbt: RBTree = this.rbt) { + assertEquals(keys.distinct().sorted(), rbt.getKeys()) + } + + private fun checkKeys(keys: Array>, rbt: RBTree = this.rbt) { + assertEquals(keys.map { it.first }.distinct().sorted(), rbt.getKeys()) + } + + @BeforeEach + fun setup() { + rbt = RBTree() + } + + @Nested + inner class `Constructor tests` { + @Test + fun `without args`() { + val rbt = RBTree() + checkSize(0, rbt) + } + + @Test + fun `one entity`() { + val rbt = RBTree(1, "Homka") + assertEquals("Homka", rbt.search(1)) + } + + @Test + fun `array of entities`() { + val entities = arrayOf( + Pair(35, "A"), + Pair(21, "A"), + Pair(25, "A"), + Pair(62, "A"), + Pair(12, "A"), + Pair(62, "A"), + Pair(122, "A"), + Pair(621, "A"), + Pair(121, "A"), + Pair(362, "A"), + Pair(35, "A"), + Pair(523, "A"), + ) + + val rbt: RBTree = RBTree(entities) + checkSize(10, rbt) + checkKeys(entities.map { it.first }.distinct().sorted(), rbt) + } + } + + @Nested + inner class `Set tests` { + fun setTest(data: Array>) { + rbt.set(data) + checkValues(data) + checkSize(data.size.toLong()) + } + + @Test + fun `set one key on empty tree`() { + val data = arrayOf(1 to "Homka") + setTest(data) + } + + @Test + fun `set second key as right child`() { + val data = arrayOf(1 to "Homka", 2 to "Dima") + setTest(data) + } + + @Test + fun `set second key as left child`() { + val data = arrayOf(2 to "Homka", 1 to "Dima") + setTest(data) + } + + @Test + fun `set third key as left child`() { + val data = arrayOf(2 to "Homka", 3 to "Dima", 1 to "Nastya") + setTest(data) + } + + @Test + fun `set third key as right child`() { + val data = arrayOf(2 to "Homka", 1 to "Dima", 3 to "Nastya") + setTest(data) + } + + @Test + fun `set third key as root from left`() { + val data = arrayOf(3 to "Homka", 2 to "Dima", 1 to "Nastya") + setTest(data) + } + + @Test + fun `set third key as root from right`() { + val data = arrayOf(1 to "Homka", 2 to "Dima", 3 to "Nastya") + setTest(data) + } + + @Test + fun `set key to the left when uncle is red`() { + val data = arrayOf(5 to "Homka", 10 to "Dima", 15 to "Nastya", 7 to "Rodion") + setTest(data) + } + + @Test + fun `set key to the right when uncle is red`() { + val data = arrayOf(5 to "Homka", 10 to "Dima", 15 to "Nastya", 17 to "Rodion") + setTest(data) + } + + @Test + fun `set key to the left when uncle is black and parent red`() { + val data = arrayOf(15 to "Homka", 10 to "Dima", 5 to "Nastya", 1 to "Rodion", 6 to "spisladqo") + setTest(data) + } + + @Test + fun `set key to the left when uncle is null and parent red`() { + val data = arrayOf( + 5 to "Homka", 10 to "Dima", 15 to "Nastya", 20 to "Rodion", + 25 to "spisladqo", 30 to "Vichislav Zorich", 27 to "Sibiri4ok" + ) + setTest(data) + } + + @Test + fun `set key to the right when uncle is black and parent red`() { + val data = arrayOf(1 to "Homka", 5 to "Dima", 10 to "Nastya", 15 to "Rodion", 14 to "spisladqo") + setTest(data) + } + + @Test + fun `set key to the right when uncle is null and parent red`() { + val data = arrayOf( + 30 to "Homka", 25 to "Dima", 20 to "Nastya", 15 to "Rodion", + 10 to "spisladqo", 5 to "Vichislav Zorich", 6 to "Sibiri4ok" + ) + setTest(data) + } + + @Test + fun `set some random keys`() { + val data = arrayOf( + 35 to "Homka", 21 to "Dima", 25 to "Nastya", 622 to "Rodion", + 12 to "spisladqo", 62 to "Vichislav Zorich", 122 to "Sibiri4ok", + 621 to "kotenok-barista", 121 to "vlad zavtra v zal", 362 to "karim", + 36 to "seriy cardinal", 523 to "katya", 251 to "sonechka", 9352 to "qrutyy misha", + 513 to "little hamster", 462 to "nameless kitty", 2395 to "therain7", + 3252 to "vacman", 1352 to "homerizde", 723 to "kvas" + ) + setTest(data) + } + + @Test + fun `stress test`() { + val rbt = RBTree() + val generator = Random(5) + val setWithNull: (Int) -> (Unit) = { key -> rbt.set(key, null) } + + val randomKeys = mutableListOf() + for (i in 1..1000000) { + val randomValue = generator.nextInt() + randomKeys.add(randomValue) + setWithNull(randomValue) + } + + val randomKeysDistinct = randomKeys.distinct() + assertEquals(randomKeysDistinct.sorted(), rbt.getKeys()) + assertEquals(randomKeysDistinct.size.toLong(), rbt.size) + } + } + + @Nested + inner class `Remove Tests` { + fun removeTest(data: Array>, key: Int) { + rbt.set(data) + rbt.remove(key) + checkValues(data.filter { it.first != key }.toTypedArray()) + checkSize((data.size - 1).toLong()) + + } + + @Test + fun `remove one key`() { + val data = arrayOf(1 to "Homka") + removeTest(data, 1) + } + + @Test + fun `remove leaf key from right`() { + val data = arrayOf(2 to "Homka", 3 to "Dima", 1 to "Nastya") + removeTest(data, 3) + } + + @Test + fun `remove single leaf key from right`() { + val data = arrayOf(2 to "Homka", 3 to "Dima") + removeTest(data, 3) + } + + @Test + fun `remove leaf key from left`() { + val data = arrayOf(2 to "Homka", 3 to "Dima", 1 to "Nastya") + removeTest(data, 1) + } + + @Test + fun `remove single leaf key from left`() { + val data = arrayOf(2 to "Homka", 1 to "Nastya") + removeTest(data, 1) + } + + @Test + fun `remove root in tree with 3 nodes`() { + val data = arrayOf(2 to "Homka", 3 to "Dima", 1 to "Nastya") + removeTest(data, 2) + } + + @Test + fun `remove root in tree with right node`() { + val data = arrayOf(2 to "Homka", 3 to "Dima") + removeTest(data, 2) + } + + @Test + fun `remove root in tree with left node`() { + val data = arrayOf(2 to "Homka", 1 to "Dima") + removeTest(data, 2) + } + + @Test + fun `remove node that is right subtree with nodes`() { + val data = arrayOf( + 35 to "Homka", 21 to "Dima", 25 to "Nastya", 622 to "Rodion", + 12 to "spisladqo", 62 to "Vichislav Zorich", 122 to "Sibiri4ok", + 621 to "kotenok-barista", 121 to "vlad zavtra v zal", 362 to "karim", + 36 to "seriy cardinal", 523 to "katya", 251 to "sonechka", + 6422 to "dinozavrik", 4621 to "ruslan", 2093 to "islam", + 235 to "miss mi", 682 to "liya", 2058 to "cold water", 2391 to "azamat", + 2269 to "graphblas" + ) + removeTest(data, 682) + } + + @Test + fun `remove node that is left subtree with nodes`() { + val data = arrayOf( + 35 to "Homka", 21 to "Dima", 25 to "Nastya", 622 to "Rodion", + 12 to "spisladqo", 62 to "Vichislav Zorich", 122 to "Sibiri4ok", + 621 to "kotenok-barista", 121 to "vlad zavtra v zal", 362 to "karim", + 36 to "seriy cardinal", 523 to "katya", 251 to "sonechka", + 6422 to "dinozavrik", 4621 to "ruslan", 2093 to "islam", + 235 to "miss mi" + ) + removeTest(data, 21) + } + + @Test + fun `stress test`() { + val rbt = RBTree() + val generator = Random(5) + val setWithDefaultValue: (Int) -> (Unit) = { key -> rbt.set(key, "") } + + val randomKeys = mutableListOf() + for (i in 1..1000000) { + val randomValue = generator.nextInt() + randomKeys.add(randomValue) + setWithDefaultValue(randomValue) + } + + val len = randomKeys.size + for (index in 1..len) { + val value: Int = randomKeys.removeLast() + rbt.remove(value) + } + + checkSize(0, rbt) + checkKeys(arrayOf(), rbt) + } + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/tree/SearchTreeTest.kt b/lib/src/test/kotlin/tree/SearchTreeTest.kt new file mode 100644 index 0000000..03e3311 --- /dev/null +++ b/lib/src/test/kotlin/tree/SearchTreeTest.kt @@ -0,0 +1,355 @@ +package tree + +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested + +class SearchTreeTest { + private lateinit var bst: BSTree + private val bstWithoutNodes = BSTree() + + @BeforeEach + fun setup() { + bst = BSTree( + arrayOf( + Pair(5, "A"), Pair(8, "A"), Pair(6, "A"), Pair(1, "A"), + Pair(2, "A"), Pair(4, "A"), Pair(3, "A"), Pair(10, "A") + ) + ) + } + + @Nested + inner class `Constructor tests` { + @Test + fun `tree without args`() { + val bst = BSTree() + assertEquals(0, bst.size) + } + + @Test + fun `tree with one arg`() { + val bst = BSTree(1, "A") + assertEquals(listOf(Pair(1, "A")), bst.getEntities()) + } + + @Test + fun `tree with some args`() { + val bst: BSTree = BSTree( + arrayOf( + Pair(1, "A"), + Pair(2, "B"), + Pair(3, "C"), + ) + ) + + assertEquals(3, bst.size) + assertEquals(listOf(Pair(1, "A"), Pair(2, "B"), Pair(3, "C")), bst.getEntities()) + } + } + + @Nested + inner class `Min and max tests` { + @Test + fun `getMin() return right pair`() { + assertEquals(Pair(1, "A"), bst.getMin()) + } + + @Test + fun `getMax() return right pair`() { + assertEquals(Pair(10, "A"), bst.getMax()) + } + + @Test + fun `getMin() return pair of nulls on empty tree`() { + assertEquals(Pair(null, null), bstWithoutNodes.getMin()) + } + + @Test + fun `getMax() return pair of nulls on empty tree`() { + assertEquals(Pair(null, null), bstWithoutNodes.getMax()) + } + } + + @Nested + inner class `Clear tests` { + @Test + fun `clear() removes all nodes from tree`() { + bst.clear() + assertEquals(0, bst.size) + assertEquals(emptyList(), bst.getValues()) + } + + @Test + fun `clear() works fine on empty tree`() { + bst.clear() + bst.clear() + assertEquals(0, bst.size) + assertEquals(emptyList(), bst.getValues()) + } + } + + @Nested + inner class `Successor and predecessor tests`() { + @Test + fun `successor() test with existed keys`() { + assertEquals(Pair(2, "A"), bst.successor(1)) + assertEquals(Pair(3, "A"), bst.successor(2)) + assertEquals(Pair(4, "A"), bst.successor(3)) + assertEquals(Pair(5, "A"), bst.successor(4)) + assertEquals(Pair(6, "A"), bst.successor(5)) + assertEquals(Pair(8, "A"), bst.successor(6)) + assertEquals(Pair(10, "A"), bst.successor(8)) + assertEquals(Pair(null, null), bst.successor(10)) + } + + @Test + fun `successor() if key don't exists it return next key`() { + assertEquals(Pair(8, "A"), bst.successor(7)) + } + + @Test + fun `successor() in empty tree`() { + assertEquals(Pair(null, null), bstWithoutNodes.successor(7)) + } + + @Test + fun `predecessor() in empty tree`() { + assertEquals(Pair(null, null), bstWithoutNodes.predecessor(7)) + } + + @Test + fun `predecessor() test with existed keys`() { + assertEquals(Pair(null, null), bst.predecessor(1)) + assertEquals(Pair(1, "A"), bst.predecessor(2)) + assertEquals(Pair(2, "A"), bst.predecessor(3)) + assertEquals(Pair(3, "A"), bst.predecessor(4)) + assertEquals(Pair(4, "A"), bst.predecessor(5)) + assertEquals(Pair(5, "A"), bst.predecessor(6)) + assertEquals(Pair(6, "A"), bst.predecessor(8)) + assertEquals(Pair(8, "A"), bst.predecessor(10)) + } + } + + @Nested + inner class `Getting test` { + @Test + fun `tree with node`() { + assertEquals(listOf(1, 2, 3, 4, 5, 6, 8, 10), bst.getKeys()) + assertEquals(listOf("A", "A", "A", "A", "A", "A", "A", "A"), bst.getValues()) + assertEquals( + listOf( + Pair(1, "A"), Pair(2, "A"), Pair(3, "A"), Pair(4, "A"), + Pair(5, "A"), Pair(6, "A"), Pair(8, "A"), Pair(10, "A") + ), + bst.getEntities() + ) + } + + @Test + fun `empty tree`() { + assertEquals(listOf(), bstWithoutNodes.getKeys()) + assertEquals(listOf(), bstWithoutNodes.getValues()) + assertEquals(listOf>(), bstWithoutNodes.getEntities()) + } + } + + @Nested + inner class `Setting tests` { + + @Test + fun `set new nodes`() { + assertEquals(null, bst.set(19, "B")) + + assertEquals(null, bst.setIfEmpty(11, "B")) + + assertEquals(listOf(null, null, null), bst.set(arrayOf(Pair(20, "B"), Pair(-5, "B"), Pair(7, "B")))) + assertEquals(7, bst.recentlyKey) + + assertEquals(listOf(null, null, null), bst.setIfEmpty(arrayOf(Pair(9, "B"), Pair(0, "B"), Pair(22, "B")))) + assertEquals(22, bst.recentlyKey) + assertEquals(16, bst.size) + assertEquals(listOf(-5, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 19, 20, 22), bst.getKeys()) + } + + @Test + fun `set the same nodes`() { + assertEquals("A", bst.set(10, "B")) + + assertEquals("A", bst.setIfEmpty(8, "B")) + + assertEquals(listOf("A", "A", "A"), bst.set(arrayOf(Pair(2, "B"), Pair(6, "B"), Pair(4, "B")))) + assertEquals(4, bst.recentlyKey) + + assertEquals(listOf("A", "A", "A"), bst.setIfEmpty(arrayOf(Pair(1, "B"), Pair(5, "B"), Pair(3, "B")))) + assertEquals(4, bst.recentlyKey) + assertEquals(8, bst.size) + assertEquals( + listOf( + Pair(1, "A"), Pair(2, "B"), Pair(3, "A"), Pair(4, "B"), + Pair(5, "A"), Pair(6, "B"), Pair(8, "A"), Pair(10, "B") + ), + bst.getEntities() + ) + } + + @Nested + inner class `set nodes in empty tree` { + @Test + fun `set new node`() { + assertEquals(null, bstWithoutNodes.set(19, "B")) + assertEquals(1, bstWithoutNodes.size) + assertEquals(listOf(Pair(19, "B")), bstWithoutNodes.getEntities()) + } + + @Test + fun `setIfEmpty new node`() { + assertEquals(null, bstWithoutNodes.setIfEmpty(19, "B")) + assertEquals(1, bstWithoutNodes.size) + assertEquals(listOf(Pair(19, "B")), bstWithoutNodes.getEntities()) + } + + @Test + fun `set new nodes`() { + assertEquals( + listOf(null, null, null), + bstWithoutNodes.set(arrayOf(Pair(12, "B"), Pair(0, "B"), Pair(7, "B"))) + ) + assertEquals(7, bstWithoutNodes.recentlyKey) + assertEquals(3, bstWithoutNodes.size) + assertEquals(listOf(Pair(0, "B"), Pair(7, "B"), Pair(12, "B")), bstWithoutNodes.getEntities()) + + } + + @Test + fun `setIfEmpty new nodes`() { + assertEquals( + listOf(null, null, null), + bstWithoutNodes.setIfEmpty(arrayOf(Pair(12, "B"), Pair(0, "B"), Pair(7, "B"))) + ) + assertEquals(7, bstWithoutNodes.recentlyKey) + assertEquals(3, bstWithoutNodes.size) + assertEquals(listOf(Pair(0, "B"), Pair(7, "B"), Pair(12, "B")), bstWithoutNodes.getEntities()) + } + } + } + + @Nested + inner class `Search tests` { + @Test + fun `search exist node`() { + assertEquals("A", bst.search(2)) + } + + @Test + fun `search don't exist node`() { + assertEquals(null, bst.search(0)) + } + + @Test + fun `empty tree`() { + assertEquals(null, bstWithoutNodes.search(2)) + } + } + + @Nested + inner class `Remove tests` { + @Test + fun `remove exist nodes`() { + assertEquals("A", bst.remove(2)) + assertEquals(listOf(1, 3, 4, 5, 6, 8, 10), bst.getKeys()) + assertEquals(7, bst.size) + + assertEquals(listOf("A", "A", "A"), bst.remove(arrayOf(1, 5, 10))) + assertEquals(listOf(3, 4, 6, 8), bst.getKeys()) + assertEquals(4, bst.size) + } + + @Test + fun `remove don't exist nodes`() { + assertEquals(null, bst.remove(0)) + + assertEquals(listOf(null, null, null), bst.remove(arrayOf(11, 35, 100))) + assertEquals(listOf(1, 2, 3, 4, 5, 6, 8, 10), bst.getKeys()) + assertEquals(8, bst.size) + } + + @Test + fun `remove nodes from empty tree`() { + assertEquals(null, bstWithoutNodes.remove(2)) + + assertEquals(listOf(null, null, null), bstWithoutNodes.remove(arrayOf(1, 3, 10))) + assertEquals(listOf(), bstWithoutNodes.getKeys()) + assertEquals(0, bstWithoutNodes.size) + } + } + + @Nested + inner class `Traversal methods` { + @Test + fun `inOrderTraversal return empty list on empty tree`() { + val rbt = BSTree() + val result = mutableListOf() + + rbt.inOrderTraversal { result.add(it.first) } + + assertEquals(listOf(), result) + } + + @Test + fun `inOrderTraversal return keys in inOrder order`() { + val bst = BSTree() + bst.set(arrayOf(1 to "Homka", 2 to "Dima", 3 to "Nastya")) + + val result = mutableListOf() + bst.inOrderTraversal { result.add(it.first) } + + assertEquals(listOf(1, 2, 3), bst.getKeys()) + } + + @Test + fun `preOrderTraversal return empty list on empty tree`() { + val rbt = RBTree() + val result = mutableListOf() + + rbt.preOrderTraversal { result.add(it.first) } + + assertEquals(listOf(), result) + } + + @Test + fun `preOrderTraversal return keys in preorder order`() { + val bst = BSTree() + bst.set(arrayOf(2 to "Homka", 1 to "Dima", 3 to "Nastya")) + + val result = mutableListOf() + bst.preOrderTraversal { result.add(it.first) } + + assertEquals(listOf(2, 1, 3), result) + } + + @Test + fun `postOrderTraversal return empty list on empty tree`() { + val rbt = RBTree() + val result = mutableListOf() + + rbt.preOrderTraversal { result.add(it.first) } + + assertEquals(listOf(), result) + } + + @Test + fun `postOrderTraversal return keys in preorder order`() { + val bst = BSTree() + bst.set(arrayOf(2 to "Homka", 1 to "Dima", 3 to "Nastya")) + + val result = mutableListOf() + bst.postOrderTraversal { result.add(it.first) } + + assertEquals(listOf(1, 3, 2), result) + } + } +} + + diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..0b90193 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,14 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.6/userguide/multi_project_builds.html in the Gradle documentation. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" +} + +rootProject.name = "trees-3" +include("lib")