diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 0000000..f8d7e33 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,35 @@ +name: Kotlin CI with Gradle + +on: + push: + branches: + [main, CI] + pull_request: + branches: + [main] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: "oracle" + java-version: 21 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Testing + run: ./gradlew test + + - name: Run Test Coverage + run: ./gradlew jacocoTestReport + + - name: Generate JaCoCo Badge + uses: cicirello/jacoco-badge-generator@v2 + with: + generate-branches-badge: true + jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..951ff97 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 spbu-coding-2023 + +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..8865121 --- /dev/null +++ b/README.md @@ -0,0 +1,390 @@ +[![MIT License][license-shield]][license-url] +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/cicirello/jacoco-badge-generator?label=Marketplace&logo=GitHub)](https://github.com/marketplace/actions/jacoco-badge-generator) + +

Борцы Даня, Саша, Мухаммет

+ +## О проекте + +Добро пожаловать в библиотеку Kotlin Binary Tree! Эта библиотека разработана для предоставления разработчикам +эффективных и гибких реализаций трех различных типов бинарных деревьев: Binary Tree, AVL Tree и Red-Black Tree + +## Создано с использованием + +- Kotlin 1.9.22: Библиотека написана на Kotlin, используя его лаконичный синтаксис и мощные возможности. +- JDK 21: Построено с использованием Java Development Kit версии 21, обеспечивая совместимость с современными + средами Java и используя последние усовершенствования Java. + +## Главные особенности + +- #### Три Варианта Бинарных Деревьев: Выберите из различных типов бинарных деревьев, чтобы удовлетворить свои конкретные требования: + +- Binary Tree: Фундаментальная структура бинарного дерева, предоставляющая простые возможности вставки, удаления и поиска. +- AVL Tree: Самобалансирующееся бинарное дерево поиска, обеспечивающее оптимальную производительность для операций вставки, удаления и поиска. +- Red-Black Tree: Еще одно самобалансирующееся бинарное дерево поиска с логарифмической высотой, обеспечивающее эффективные операции для больших наборов данных. + +- #### Универсальные Операции: Каждая реализация дерева поддерживает основные операции для управления структурами деревьев: + - Поиск: Быстро находите узлы в дереве на основе ключевых значений. + - Вставка: Добавляйте новые узлы, сохраняя целостность и баланс дерева. + - Удаление: Удаляйте узлы из дерева, не нарушая его структурных свойств. + - Вывод на консоль: Визуализируйте структуру дерева через печать в консоли, помогая в отладке и визуализации задач. + +[//]: # (## Usage) + +## Как пользоватся + +### Запуск программы +```shell +./gradlew build +``` +### Инициализация +```kotlin +val binaryTree = BinaryTree() +val redBlackTree = RBTree() +val avlTree = AVLTree() +``` +### Базовые операции + +#### Добавление +```kotlin +val binaryTree = BinaryTree() + +binaryTree.add(1, "A") +binaryTree.add(2, "B") +binaryTree.add(3, "C") + +println(binaryTree) + +// output: [(1: "A"), (2: "B"), (3, "C")] +``` + +#### Поиск +```kotlin +val binaryTree = BinaryTree() + +/* + добавляем ноды +*/ + +// поиск существующего элемента +println(binaryTree.get(1)) +println(binaryTree[3]) +println(binaryTree.getOrDefault(2, "No such element")) + +// поиск не существующего элемента +println(binaryTree.get(4)) +println(binaryTree[5]) +println(binaryTree.getOrDefault(8, "No such element")) +``` +#### Вывод +```kotlin +A +C +B +null +null +No such element +``` + +#### Удаление +```kotlin +val binaryTree = BinaryTree() + +/* + добавляем ноды +*/ + +// удаление существующего элемента +binaryTree.delete(1) + +// удаление не существующего элемента ничего не делает +binaryTree.delete(4) + +println(binaryTree.toString()) +``` + +#### Вывод +```text +[(2: B), (3: C)] +``` + +#### Присваивание +```kotlin +val binaryTree = BinaryTree() + +/* + добавляем ноды +*/ + +println(binaryTree.toString()) + +// присвоить новое значение существующему элементу +binaryTree.set(2, "D") +binaryTree[3] = "E" + +println(binaryTree.toString()) + +// присвоить значение не существующему элементу +binaryTree.set(4, "Y") +binaryTree[5] = "X" + +println(binaryTree.toString()) +``` + +#### Вывод +```text +// изначальный вид +[(1: A), (2: B), (3: C)] + +// после присваивания +[(1: A), (2: D), (3: E)] + +// после присваивания значения не существующему элементу +[(1: A), (2: D), (3: E), (4: Y), (5: X)] +``` + +#### Минимум / Максимум +```kotlin +val binaryTree = BinaryTree() + +binaryTree.add(1, "A") +binaryTree.add(2, "B") +binaryTree.add(3, "C") + +println(binaryTree.min()) +println(binaryTree.max()) +``` + +#### Вывод +```text +(1: A) +(3: C) +``` + +#### Итерирование по дереву + +
+ Итерирование по ключу + + ```kotlin + val binaryTree = BinaryTree() + +binaryTree.add(2, "B") +binaryTree.add(1, "A") +binaryTree.add(3, "C") + +binaryTree.iterator().forEach { + println(it.key) +} + + println(binaryTree.toString()) + ``` +#### Вывод + ```text +1 +2 +3 + ``` +
+ +
+ BST итерирование + + ```kotlin + val binaryTree = BinaryTree() + +binaryTree.add(2, "B") +binaryTree.add(1, "A") +binaryTree.add(3, "C") + +binaryTree.iterateBFS().forEach { + println(it.key) +} + + println(binaryTree.toString()) + ``` +#### Вывод + ```text +2 +1 +3 + ``` +
+ +
+ DFS итерирование + + ```kotlin + val binaryTree = BinaryTree() +val keys = arrayOf(3, 2, 1, 0, 4, 5) + +keys.forEach { binaryTree.add(it, it.toString()) } + +// Стандартно iterateDFS работает с mode=Tree.ModeDFS.PREORDER +println("PREORDER: ") +binaryTree.iterateDFS().forEach { print(it.key.toString() + " ") } +println("INORDER: ") +binaryTree.iterateDFS(mode=Tree.ModeDFS.INORDER).forEach { print(it.key.toString() + " ") } +println("POSTORDER: ") +binaryTree.iterateDFS(mode=Tree.ModeDFS.POSTORDER).forEach { print(it.key.toString() + " ") } + ``` +#### Вывод + ```text +PREORDER: 3 2 1 0 4 5 +INORDER: 0 1 2 3 4 5 +POSTORDER: 0 1 2 5 4 3 + ``` +
+ +О методах обхода в глубину — [Tree traversal](https://en.wikipedia.org/wiki/Tree_traversal) + +#### Соединение двух деревьев + +Операция соединения двух деверьев доступна, только при условии, что их ключи сравнимы и все ключи основного дерева +меньше всех ключей присоединяемого дерева. + +```kotlin +val binaryTree = BinaryTree() +val secondBinaryTree = BinaryTree() + +binaryTree.add(2, "B") +binaryTree.add(1, "A") +binaryTree.add(3, "C") + +secondBinaryTree.add(4, "D") +secondBinaryTree.add(5, "E") +secondBinaryTree.add(6, "F") + + +binaryTree.merge(secondBinaryTree) + +println(binaryTree.toString()) +``` + +#### Вывод + ```text +[(1: A), (2: B), (3: C), (4: D), (5: E), (6: F)] + ``` + +#### Клонирование дерева +```kotlin +val binaryTree = BinaryTree() + +binaryTree.add(2, "B") +binaryTree.add(1, "A") +binaryTree.add(3, "C") + +val cloneTree = binaryTree.clone() + +println(cloneTree.toString()) + +``` + +#### Вывод + ```text +[(1: A), (2: B), (3: C)] + ``` + +### Виды выводов дерева +
+ Итерационный вывод + + ```kotlin + val binaryTree = BinaryTree() + + binaryTree.add(1, "A") + binaryTree.add(2, "B") + binaryTree.add(3, "C") + + println(binaryTree.toString()) + ``` +#### Вывод + ```text + [(1: A), (2: B), (3: C)] + ``` +
+ +
+ Вертикальный вывод рисунком + + ```kotlin + val binaryTree = BinaryTree() + + binaryTree.add(1, "A") + binaryTree.add(2, "B") + binaryTree.add(3, "C") + + println(binaryTree.toString(mode = Tree.TreeStringMode.WIDTH)) + ``` +#### Вывод + ```text + | ┌── (3: C) + | ┌── (2: B) + └── (1: A) + ``` +
+ +
+ Горизонтальный вывод рисунком + + ```kotlin + val binaryTree = BinaryTree() + + binaryTree.add(1, "A") + binaryTree.add(2, "B") + binaryTree.add(3, "C") + + println(binaryTree.toString(mode = Tree.TreeStringMode.WIDTH)) + ``` +#### Вывод + ```text +────────┐ + (1: A) + └─────┐ + (2: B) + └────┐ + (3: C) + ``` +
+
+ Цветной вывод Red-Black Tree +- Добавляйте параметр к дереву + +```kotlin +redBlackTree.setColored(true) +``` + +```kotlin + val redBlackTree = RBTree() + redBlackTree.setColored(true) + + redBlackTree.add(1, "A") + redBlackTree.add(2, "B") + redBlackTree.add(3, "C") + redBlackTree.add(4, "D") + redBlackTree.add(5, "E") + + println(redBlackTree.toString()) + println(redBlackTree.toString(mode = Tree.TreeStringMode.WIDTH)) + println(redBlackTree.toString(mode = Tree.TreeStringMode.HEIGHT)) +``` + +#### Вывод: + +Alt text + +
+ + + +## Лицензия + +Распространяется по лицензии MIT. См. файл `LICENSE.txt` для получения дополнительной информации. + +## Авторы + +- [AlexandrKudrya](https://github.com/AlexandrKudrya) +- [7gambit7](https://github.com/7gambit7) +- [VersusXX](https://github.com/VersusXX) + +[license-shield]: https://img.shields.io/github/license/othneildrew/Best-README-Template.svg?style=for-the-badge: +[license-url]: https://github.com/spbu-coding-2023/trees-11/blob/main/LICENSE.txt \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ce5f22f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + kotlin("jvm") version "1.9.22" + `java-library` + jacoco + application +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") +} + +tasks.test { + useJUnitPlatform() + + testLogging { + events("skipped", "failed") + afterSuite( + // spell + KotlinClosure2({ desc: TestDescriptor, result: TestResult -> + // Only execute on the outermost suite + if (desc.parent == null) { + println(" **** Result: ${result.resultType} ****") + println(" > Tests: ${result.testCount}") + println(" > Passed: ${result.successfulTestCount}") + println(" > Failed: ${result.failedTestCount}") + println(" > Skipped: ${result.skippedTestCount}") + } + }), + ) + } + + reports { + junitXml.required = true + } + + finalizedBy(tasks.jacocoTestReport) +} + +tasks.named("jacocoTestReport") { + dependsOn(tasks.test) + reports { + csv.required = true + xml.required = false + html.required = false + } +} + +kotlin { + jvmToolchain(21) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 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..7ad9c0a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Mar 15 18:28:58 MSK 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/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/master/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 \ + "$@" + +# 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..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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=. +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%" == "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%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d861336 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} +rootProject.name = "temp" diff --git a/src/main/kotlin/AVLTree.kt b/src/main/kotlin/AVLTree.kt new file mode 100644 index 0000000..242fbfc --- /dev/null +++ b/src/main/kotlin/AVLTree.kt @@ -0,0 +1,188 @@ +class AVLTree, V : Any>(root: Node.AVLNode? = null) : Tree>(root) { + override fun add( + key: K, + value: V, + ) { + this.root = insert(this.root as Node.AVLNode?, key, value) + } + + private fun height(node: Node.AVLNode?): Int { + if (node == null) return 0 + + return node.height + } + + private fun max( + a: Int, + b: Int, + ): Int { + return if ((a > b)) a else b + } + + private fun getBalance(node: Node.AVLNode?): Int { + if (node == null) return 0 + + return height(node.left as Node.AVLNode?) - height(node.right as Node.AVLNode?) + } + + private fun insert( + node: Node.AVLNode?, + key: K, + value: V, + ): Node.AVLNode? { + if (node == null) return Node.AVLNode(key, value) + + if (key < node.key) { + node.left = insert(node.left as Node.AVLNode?, key, value) + } else if (key > node.key) { + node.right = insert(node.right as Node.AVLNode?, key, value) + } else { + return node + } + + node.height = 1 + max(height(node.left as Node.AVLNode?), height(node.right as Node.AVLNode?)) + + val balance = getBalance(node) + + if (balance > 1 && key < node.left!!.key) return rightRotate(node) + + if (balance < -1 && key > node.right!!.key) return leftRotate(node) + + if (balance > 1 && key > node.left!!.key) { + node.left = leftRotate(node.left as Node.AVLNode?) + return rightRotate(node) + } + + if (balance < -1 && key < node.right!!.key) { + node.right = rightRotate(node.right as Node.AVLNode?) + return leftRotate(node) + } + + return node + } + + private fun deleteNode( + node: Node.AVLNode?, + key: K, + ): Node.AVLNode? { + var root = node + if (root == null) return null + + if (key < root.key) { + root.left = deleteNode(root.left as Node.AVLNode?, key) + } else if (key > root.key) { + root.right = deleteNode(root.right as Node.AVLNode?, key) + } else { + if ((root.left == null) || (root.right == null)) { + var temp: Node.AVLNode? = null + temp = + if (temp === root.left) { + root.right as Node.AVLNode? + } else { + root.left as Node.AVLNode? + } + + if (temp == null) { + temp = root + root = null + } else { + root = temp + } + } else { + val temp = minValueNode(root.right as Node.AVLNode?) + + root.key = temp!!.key + + root.right = deleteNode(root.right as Node.AVLNode?, temp.key) + } + } + + if (root == null) return root + + (root as Node.AVLNode?)?.height = + max(height(root.left as Node.AVLNode?), height(root.right as Node.AVLNode?)) + 1 + + val balance = getBalance(root) + + if (balance > 1 && getBalance(root.left as Node.AVLNode?) >= 0) return rightRotate(root) + + if (balance > 1 && getBalance(root.left as Node.AVLNode?) < 0) { + root.left = leftRotate(root.left as Node.AVLNode?) + return rightRotate(root) + } + + if (balance < -1 && getBalance(root.right as Node.AVLNode?) <= 0) return leftRotate(root) + + if (balance < -1 && getBalance(root.right as Node.AVLNode?) > 0) { + root.right = rightRotate(root.right as Node.AVLNode?) + return leftRotate(root) + } + + return root + } + + private fun minValueNode(node: Node.AVLNode?): Node.AVLNode? { + var current = node + + // loop down to find the leftmost leaf + while (current!!.left != null) current = current.left as Node.AVLNode? + + return current + } + + private fun rightRotate(node: Node.AVLNode?): Node.AVLNode { + val x = node!!.left as Node.AVLNode? + val T2 = x!!.right as Node.AVLNode? + + // Perform rotation + x.right = node + node.left = T2 + + // Update heights + node.height = max(height(node.left as Node.AVLNode?), height(node.right as Node.AVLNode?)) + 1 + x.height = max(height(x.left as Node.AVLNode?), height(x.right as Node.AVLNode?)) + 1 + + // Return new root + return x + } + + private fun leftRotate(node: Node.AVLNode?): Node.AVLNode? { + val y = node!!.right as Node.AVLNode? + val T2 = y!!.left as Node.AVLNode? + + y.left = node + node.right = T2 + + node.height = max(height(node.left as Node.AVLNode?), height(node.right as Node.AVLNode?)) + 1 + y.height = max(height(y.left as Node.AVLNode?), height(y.right as Node.AVLNode?)) + 1 + + return y + } + + override fun merge(tree: Tree>) { + if (this.root != null && tree.root != null) { + require(this.max()!!.key < tree.min()?.key!!) { + "Merge operation is defined only when attachable tree's keys is always bigger than base tree's keys" + } + } + if (this.root == null) { + this.root = tree.root + } else { + tree.forEach { + this.add(it.key, it.value) + } + } + } + + override fun max(): Node.AVLNode? { + return if (root == null) null else (root as Node.AVLNode).max() + } + + override fun min(): Node.AVLNode? { + return if (root == null) null else (root as Node.AVLNode).min() + } + + override fun delete(key: K) { + root = deleteNode(root as Node.AVLNode?, key) + } +} diff --git a/src/main/kotlin/BinaryTree.kt b/src/main/kotlin/BinaryTree.kt new file mode 100644 index 0000000..56be611 --- /dev/null +++ b/src/main/kotlin/BinaryTree.kt @@ -0,0 +1,4 @@ +class BinaryTree, V : Any>(root: Node.BinaryNode? = null) : + Tree>(root) + +// Not sure, that this file is needed, because BinaryTree is just Tree. diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..fa9b2a2 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,13 @@ +fun main() { + val binaryTree = BinaryTree() + val keys = arrayOf(3, 2, 1, 0, 4, 5) + + keys.forEach { binaryTree.add(it, it.toString()) } + + // Стандартно iterateDFS работает с mode=Tree.ModeDFS.PREORDER + binaryTree.iterateDFS().forEach { print(it.key.toString() + " ") } + println() + binaryTree.iterateDFS(mode = Tree.ModeDFS.INORDER).forEach { print(it.key.toString() + " ") } + println() + binaryTree.iterateDFS(mode = Tree.ModeDFS.POSTORDER).forEach { print(it.key.toString() + " ") } +} diff --git a/src/main/kotlin/Nodes.kt b/src/main/kotlin/Nodes.kt new file mode 100644 index 0000000..c7b82cd --- /dev/null +++ b/src/main/kotlin/Nodes.kt @@ -0,0 +1,99 @@ +open class Node, V, T : Node> internal constructor( + var key: K, + var value: V, + internal var left: Node? = null, + internal var right: Node? = null, + internal var parent: Node? = null, +) { + class BinaryNode, V>( + key: K, + value: V, + left: BinaryNode? = null, + right: BinaryNode? = null, + parent: BinaryNode? = null, + ) : Node>(key, value, left, right, parent) + + class RBNode, V>( + key: K, + value: V, + left: RBNode? = null, + right: RBNode? = null, + parent: RBNode? = null, + var color: Color = Color.RED, + ) : Node>(key, value, left, right, parent) { + enum class Color { RED, BLACK } + + companion object { + private const val RED_COLOR: String = "\u001B[31m" + private const val RESET_COLOR: String = "\u001B[0m" + var colored: Boolean = false + } + + override fun toString(): String { + return if (colored && color == Color.RED) { + "${RED_COLOR}($key: $value)${RESET_COLOR}" + } else { + "($key: $value)" + } + } + + val isOnLeft: Boolean + get() = this == parent?.left + + fun sibling(): RBNode? { + if (parent == null) return null + + if (isOnLeft) return parent!!.right as RBNode? + + return parent!!.left as RBNode? + } + + fun hasRedChild(): Boolean { + return (left != null && (left as RBNode).color == Color.RED) || + (right != null && (right as RBNode).color == Color.RED) + } + } + + class AVLNode, V>( + key: K, + value: V, + left: AVLNode? = null, + right: AVLNode? = null, + parent: AVLNode? = null, + var height: Int = 1, + ) : Node>(key, value, left, right, parent) { + /*override fun toString(): String { + val rightHeight = if (right != null) (right as AVLNode).height else 0 + val leftHeight = if (left != null) (left as AVLNode).height else 0 + val balance = leftHeight - rightHeight + return "($key: $balance)" + }*/ + fun max(): AVLNode { + var current = this + while (current.right != null) { + current = current.right as AVLNode + } + return current + } + + fun min(): AVLNode { + var current = this + while (current.left != null) { + current = current.left as AVLNode + } + return current + } + } + + override fun toString(): String { + return "($key: $value)" + } + + override fun equals(other: Any?): Boolean { + return if (other !is Node<*, *, *>) false else (this.key == other.key && this.value == other.value) + } + + override fun hashCode(): Int { + return super.hashCode() + } +} diff --git a/src/main/kotlin/RBTree.kt b/src/main/kotlin/RBTree.kt new file mode 100644 index 0000000..ea5b69d --- /dev/null +++ b/src/main/kotlin/RBTree.kt @@ -0,0 +1,416 @@ +import kotlin.math.ceil +import kotlin.math.floor + +class RBTree, V : Any>(root: Node.RBNode? = null) : Tree>(root) { + override fun add( + key: K, + value: V, + ) { + val node: Node.RBNode = Node.RBNode(key, value) + this.addNode(node) + } + + override fun addNode(node: Node>) { + super.addNode(node) + balanceRBTree(node as Node.RBNode) + } + + override fun merge(tree: Tree>) { + if (this.root != null && tree.root != null) { + require(this.max()!!.key < tree.min()?.key!!) { + "Merge operation is defined only when attachable tree's keys is always bigger than base tree's keys" + } + } + if (this.root == null) { + this.root = tree.root + } else { + tree.forEach { + this.add(it.key, it.value) + } + } + } + + override fun delete(key: K) { + val node = this.getNode(key) as Node.RBNode? ?: return + + deleteNode(node) + } + + private fun deleteNode(nodeToDelete: Node.RBNode) { + val replacementNode = BSTreplace(nodeToDelete) + + // Determine if both 'replacementNode' and 'nodeToDelete' are black + val areBothBlack = + (replacementNode == null || replacementNode.color == Node.RBNode.Color.BLACK) && (nodeToDelete.color == Node.RBNode.Color.BLACK) + + val parentNode = nodeToDelete.parent + + // Case 1: 'nodeToDelete' has no replacement node + if (replacementNode == null) { + if (nodeToDelete == root) { + root = null + } else { + if (areBothBlack) { + // Both 'replacementNode' and 'nodeToDelete' are black, so fix double black at 'nodeToDelete' + fixDoubleBlack(nodeToDelete) + } else if (nodeToDelete.sibling() != null) { + nodeToDelete.sibling()!!.color = Node.RBNode.Color.RED + } + // Delete 'nodeToDelete' from the tree + if (nodeToDelete.isOnLeft) { + parentNode!!.left = null + } else { + parentNode!!.right = null + } + } + return + } + + // Case 2: 'nodeToDelete' has only one child + if (nodeToDelete.left == null || nodeToDelete.right == null) { + if (nodeToDelete == root) { + // 'nodeToDelete' is the root, so assign the value of 'replacementNode' to 'nodeToDelete' and delete 'replacementNode' + nodeToDelete.key = replacementNode.key + nodeToDelete.right = null + nodeToDelete.left = null + // Delete 'replacementNode' + } else { + // Detach 'nodeToDelete' from the tree and move 'replacementNode' up + if (nodeToDelete.isOnLeft) { + parentNode!!.left = replacementNode + } else { + parentNode!!.right = replacementNode + } + + replacementNode.parent = parentNode + + if (areBothBlack) { + // Both 'replacementNode' and 'nodeToDelete' are black, so fix double black at 'replacementNode' + fixDoubleBlack(replacementNode) + } else { + // Either 'replacementNode' or 'nodeToDelete' is red, so color 'replacementNode' black + replacementNode.color = Node.RBNode.Color.BLACK + } + } + return + } + + // Case 3: 'nodeToDelete' has two children, swap values with successor and recurse + swapValues(replacementNode, nodeToDelete) + deleteNode(replacementNode) + } + + private fun swapValues( + nodeA: Node.RBNode, + nodeB: Node.RBNode, + ) { + val tempKey = nodeA.key + val tempValue = nodeA.value + + nodeA.key = nodeB.key + nodeA.value = nodeB.value + + nodeB.key = tempKey + nodeB.value = tempValue + } + + private fun fixDoubleBlack(doubleBlackNode: Node.RBNode?) { + if (doubleBlackNode == null || doubleBlackNode == root) return + + // Retrieve the sibling and parent of the double black node + val sibling = doubleBlackNode.sibling() + val parent = doubleBlackNode.parent as Node.RBNode? + + // If there is no sibling, the double black is pushed up to its parent + if (sibling == null) { + fixDoubleBlack(parent) + } else { + if (parent == null) return + if (sibling.color == Node.RBNode.Color.RED) { + // If the sibling is red, perform rotation and color changes + parent.color = Node.RBNode.Color.RED + sibling.color = Node.RBNode.Color.BLACK + + if (sibling.isOnLeft) { + parent.rightRotate() // Right case + } else { + parent.leftRotate() // Left case + } + + fixDoubleBlack(doubleBlackNode) // Recursively fix double black + } else { + // If the sibling is black + if (sibling.hasRedChild()) { + // If at least one child of the sibling is red + val siblingLeft = sibling.left as Node.RBNode? + val siblingRight = sibling.right as Node.RBNode? + if (sibling.isOnLeft) { + // Left subtree of the sibling + if (siblingLeft?.color == Node.RBNode.Color.RED) { + // Left-left case + siblingLeft.color = sibling.color + sibling.color = parent.color + parent.rightRotate() + } else { + // Left-right case + siblingRight?.color = parent.color + sibling.leftRotate() + parent.rightRotate() + } + } else { + // Right subtree of the sibling + if (siblingRight?.color == Node.RBNode.Color.RED) { + // Right-right case + siblingRight.color = sibling.color + sibling.color = parent.color + parent.leftRotate() + } else { + // Right-left case + siblingLeft?.color = parent.color + sibling.rightRotate() + parent.leftRotate() + } + } + parent.color = Node.RBNode.Color.BLACK + } else { + // If both children of the sibling are black + sibling.color = Node.RBNode.Color.RED + if (parent.color == Node.RBNode.Color.BLACK) { + fixDoubleBlack(parent) + } else { + parent.color = Node.RBNode.Color.BLACK + } + } + } + } + } + + private fun successor(x: Node.RBNode): Node.RBNode { + var temp = x + while (temp.left != null) temp = temp.left as Node.RBNode + return temp + } + + // find node that replaces a deleted node in BST + private fun BSTreplace(node: Node.RBNode): Node.RBNode? { + // If the node has two children + if (node.left != null && node.right != null) { + return successor(node.right as Node.RBNode) + } + + if (node.left == null && node.right == null) { + return null // There is no replacement + } + + return if (node.left != null) { + node.left as Node.RBNode? + } else { + node.right as Node.RBNode? + } + } + + fun setColored(value: Boolean) { + Node.RBNode.colored = value + } + + private fun balanceRBTree(node: Node.RBNode) { + if (node.parent == null) { + node.color = Node.RBNode.Color.BLACK + return + } + + val parentNode = node.parent as? Node.RBNode ?: return + + if (parentNode.color == Node.RBNode.Color.BLACK) { + return + } + val grandparentNode = parentNode.parent as? Node.RBNode ?: return + + if (parentNode == grandparentNode.left) { + val uncle = grandparentNode.right as? Node.RBNode + if (uncle?.color == Node.RBNode.Color.RED) { + parentNode.color = Node.RBNode.Color.BLACK + uncle.color = Node.RBNode.Color.BLACK + grandparentNode.color = Node.RBNode.Color.RED + balanceRBTree(grandparentNode) + } else { + if (node == parentNode.right) { + parentNode.leftRotate() + balanceRBTree(node.left as Node.RBNode) + } else { + grandparentNode.rightRotate() + parentNode.color = Node.RBNode.Color.BLACK + (parentNode.right as Node.RBNode?)?.color = Node.RBNode.Color.RED + } + } + } else { + val uncle = grandparentNode.left as? Node.RBNode + if (uncle?.color == Node.RBNode.Color.RED) { + parentNode.color = Node.RBNode.Color.BLACK + uncle.color = Node.RBNode.Color.BLACK + grandparentNode.color = Node.RBNode.Color.RED + balanceRBTree(grandparentNode) + } else { + if (node == parentNode.left) { + parentNode.rightRotate() + balanceRBTree((node.right as Node.RBNode)) + } else { + grandparentNode.leftRotate() + parentNode.color = Node.RBNode.Color.BLACK + (parentNode.left as Node.RBNode?)?.color = Node.RBNode.Color.RED + } + } + } + } + + override fun toStringBeautifulHeight(ofSide: Int): String { + if (this.root == null) { + return "" + } else { + val buffer: StringBuilder = StringBuilder() + + val lines: MutableList> = mutableListOf() + + var level: MutableList?> = mutableListOf() + var next: MutableList?> = mutableListOf() + + level.add(this.root as Node.RBNode?) + + var nodeNumber = 1 + var widtest = 0 + + while (nodeNumber != 0) { + val line: MutableList = mutableListOf() + + nodeNumber = 0 + + for (node in level) { + if (node == null) { + line.add(null) + + next.add(null) + next.add(null) + } else { + val strNode: String = node.toString() + line.addLast(strNode) + + val extra = if (node.color == Node.RBNode.Color.RED) 18 else 0 + + if (strNode.length > widtest + extra) widtest = strNode.length + + next.add(node.left as Node.RBNode?) + next.add(node.right as Node.RBNode?) + + if (node.left != null) nodeNumber++ + if (node.right != null) nodeNumber++ + } + } + + widtest += widtest % 2 + + lines.add(line) + val swap = level + level = next + next = swap + next.clear() + } + + var perpiece: Int = lines[lines.size - 1].size * (widtest + ofSide) + + for (i in 1..perpiece / 2) buffer.append("─") + buffer.append("┐\n") + + for (i in 0.. = lines[i] + + val hpw: Int = floor(perpiece / 2f - 1).toInt() + + if (i > 0) { + for (j in 0...leftRotate() { + val newRoot = this.right + this.right = newRoot?.left + if (newRoot?.left != null) { + newRoot.left!!.parent = this + } + newRoot?.parent = this.parent + if (this.parent == null) { + root = newRoot + } else if (this == this.parent?.left) { + this.parent?.left = newRoot + } else { + this.parent?.right = newRoot + } + newRoot?.left = this + this.parent = newRoot + } + + private fun Node.RBNode.rightRotate() { + val newRoot = this.left + this.left = newRoot?.right + if (newRoot?.right != null) { + newRoot.right!!.parent = this + } + newRoot?.parent = this.parent + if (this.parent == null) { + root = newRoot + } else if (this == this.parent?.right) { + this.parent?.right = newRoot + } else { + this.parent?.left = newRoot + } + newRoot?.right = this + this.parent = newRoot + } +} diff --git a/src/main/kotlin/Tree.kt b/src/main/kotlin/Tree.kt new file mode 100644 index 0000000..67a986a --- /dev/null +++ b/src/main/kotlin/Tree.kt @@ -0,0 +1,563 @@ +import java.util.* +import kotlin.math.ceil +import kotlin.math.floor + +open class Tree, V : Any, T : Node>( + var root: Node? = null, +) : Iterable> { + open fun add( + key: K, + value: V, + ) { + val node: Node = Node(key, value) + this.addNode(node) + } + + open fun addNode(node: Node) { + require(this.root?.key != node.key) { + throw IllegalArgumentException("Multiple uses of key: ${node.key}. To modify value use set function") + } + if (this.root == null) { + this.root = node + } else { + val root: Node = this.root!! + // not-null assertion operator because null check + val isLeft: Boolean = root.key > node.key + addNode( + node, + (if (isLeft) root.left else root.right), + root, + isLeft, + ) + } + } + + private tailrec fun addNode( + node: Node, + current: Node?, + parent: Node, + isLeft: Boolean, + ) { + if (current == null) { + if (isLeft) { + parent.left = node + } else { + parent.right = node + } + + node.parent = parent + } else { + require(current.key != node.key) { + throw IllegalArgumentException("Multiple uses of key: ${node.key}. To modify value use set function") + } + val isAddedLeft: Boolean = current.key > node.key + addNode( + node, + (if (isAddedLeft) current.left else current.right), + current, + isAddedLeft, + ) + } + } + + operator fun set( + key: K, + value: V, + ) { + if (!this.isKey(key)) { + this.add(key, value) + } else { + this.getNode(key)!!.value = value + // not-null assertion operator because key is exist in tree + } + } + + operator fun get(key: K): V? = getNode(key)?.value + + fun getNode(key: K): Node? { + return getNode(key, this.root) + } + + private tailrec fun getNode( + key: K, + current: Node?, + ): Node? { + return if (current == null) { + null + } else if (current.key == key) { + current + } else if (current.key > key) { + getNode(key, current.left) + } else { + getNode(key, current.right) + } + } + + fun getOrDefault( + key: K, + default: V, + ): V { + val node: Node? = getNode(key) + return node?.value ?: default + } + + fun getOrDefaultNode( + key: K, + default: Node, + ): Node { + return getNode(key, root) ?: default + } + + open fun delete(key: K) { + if (this.getNode(key) != null) this.delete(this.getNode(key)!!) + } + + private fun delete(current: Node) { + val parent = current.parent + if (current.left == null && current.right == null) { + if (parent == null) { + this.root = null + } else if (parent.left === current) { + parent.left = null + } else { + parent.right = null + } + } else if (current.left == null || current.right == null) { + if (current.left == null) { + if (parent == null) { + this.root = current.right as Node + } else { + if (parent.left === current) { + parent.left = current.right + } else { + parent.right = current.right + } + } + (current.right as Node).parent = parent + } else { + if (parent == null) { + this.root = current.left as Node + } else { + if (parent.left === current) { + parent.left = current.left + } else { + parent.right = current.left + } + (current.left as Node).parent = parent + } + } + } else { + val next = getNext(current)!! + current.key = next.key + current.value = next.value + if (next.parent!!.left === next) { + next.parent!!.left = next.right + if (next.right != null) { + next.right!!.parent = next.parent + } + } else { + next.parent!!.right = next.right + if (next.right != null) { + next.right!!.parent = next.parent + } + } + + next.left = current.left + if (next.left != null) next.left!!.parent = next + next.right = current.right + if (next.right != null) next.right!!.parent = next + next.parent = current.parent + if (next.parent != null) { + if (next.parent!!.left === current) { + next.parent!!.left = next + } else { + next.parent!!.right = next + } + } else { + this.root = next + next.parent = null + } + } + } + + fun isKey(key: K): Boolean { + return isKey(key, this.root) + } + + private tailrec fun isKey( + key: K, + current: Node?, + ): Boolean { + return if (current == null) { + false + } else if (current.key == key) { + true + } else if (current.key > key) { + isKey(key, current.left) + } else { + isKey(key, current.right) + } + } + + open fun min(): Node? { + val rootNow = this.root + return if (rootNow == null) null else min(rootNow) + } + + internal fun min(node: Node): Node? { + val left = node.left + return if (left == null) node else min(left) + } + + open fun max(): Node? { + val rootNow = this.root + return if (rootNow == null) null else max(rootNow) + } + + internal fun max(node: Node): Node? { + val right = node.right + return if (right == null) node else max(right) + } + + fun getNext(node: Node): Node? { + val right = node.right + if (right != null) { + // If the right subtree is not null, the successor is the leftmost node in the right subtree + return min(right) + } + + // If the right subtree is null, the successor is the first ancestor whose left child is also an ancestor + var current = node + var parent = node.parent + while (parent != null && current == parent.right) { + current = parent + parent = parent.parent + } + return parent + } + + fun getPrev(node: Node): Node? { + val left = node.left + if (left != null) { + // If the left subtree is not null, the predecessor is the rightmost node in the left subtree + return max(left) + } + + // If the left subtree is null, the predecessor is the first ancestor whose right child is also an ancestor + var current = node + var parent = node.parent + while (parent != null && current == parent.left) { + current = parent + parent = parent.parent + } + return parent + } + + inner class ByKeyIterator( + private val tree: Tree, + ) : Iterator> { + private var current: Node? = tree.min() + + override fun hasNext(): Boolean { + return current != null + } + + override fun next(): Node { + val result = current ?: throw NoSuchElementException("No more elements") + current = tree.getNext(result) + return result + } + } + + inner class BFSIterator(tree: Tree) : Iterator> { + private var queue: Queue> = LinkedList() + + init { + val BSIqueue: Queue> = LinkedList() + if (tree.root != null) { + BSIqueue.add(tree.root) + queue.add(tree.root) + while (!BSIqueue.isEmpty()) { + val current: Node = BSIqueue.poll() + if (current.left != null) { + BSIqueue.add(current.left) + queue.add(current.left) + } + if (current.right != null) { + BSIqueue.add(current.right) + queue.add(current.right) + } + } + } + } + + override fun hasNext(): Boolean { + return !queue.isEmpty() + } + + override fun next(): Node { + return queue.poll() + } + } + + enum class ModeDFS { PREORDER, POSTORDER, INORDER } + + inner class DFSIterator(tree: Tree, mode: ModeDFS = ModeDFS.PREORDER) : Iterator> { + private var stack: Queue> = LinkedList() + + init { + when (mode) { + ModeDFS.PREORDER -> addToStackPreOrder(tree.root) + ModeDFS.INORDER -> addToStackInOrder(tree.root) + ModeDFS.POSTORDER -> addToStackPostOrder(tree.root) + } + } + + private fun addToStackPreOrder(current: Node?) { + if (current != null) { + stack.add(current) + this.addToStackPreOrder(current.left) + this.addToStackPreOrder(current.right) + } + } + + private fun addToStackPostOrder(current: Node?) { + if (current != null) { + this.addToStackPostOrder(current.left) + this.addToStackPostOrder(current.right) + stack.add(current) + } + } + + private fun addToStackInOrder(current: Node?) { + if (current != null) { + this.addToStackInOrder(current.left) + stack.add(current) + this.addToStackInOrder(current.right) + } + } + + override fun hasNext(): Boolean { + return !stack.isEmpty() + } + + override fun next(): Node { + return stack.poll() + } + } + + override fun iterator(): Iterator> { + return ByKeyIterator(this) + } + + fun iterateBFS(): BFSIterator { + return BFSIterator(this) + } + + fun iterateDFS(mode: ModeDFS = ModeDFS.PREORDER): DFSIterator { + return DFSIterator(this, mode = mode) + } + + open fun merge(tree: Tree) { + if (this.root != null && tree.root != null) { + require(this.max()!!.key < tree.min()!!.key) { + "Merge operation is defined only when attachable tree's keys is always bigger than base tree's keys" + } + } + if (this.root == null) { + this.root = tree.root + } else { + this.max()!!.right = tree.root + } + } + + fun split(x: K): Pair, Tree> { + val lowerTree: Tree = Tree() + val biggerTree: Tree = Tree() + + for (i in this.iterateBFS()) { + if (i.key < x) { + lowerTree.addNode(i) + } else { + biggerTree.addNode(i) + } + } + return Pair(lowerTree, biggerTree) + } + + fun clone(): Tree { + val clonedTree: Tree = Tree() + + for (i in this.iterateBFS()) clonedTree.add(i.key, i.value) + return clonedTree + } + + enum class TreeStringMode { NORMAL, WIDTH, HEIGHT } + + fun toString(mode: TreeStringMode = TreeStringMode.NORMAL): String { + return when (mode) { + TreeStringMode.NORMAL -> this.toString() + TreeStringMode.WIDTH -> this.toStringBeautifulWidth() + TreeStringMode.HEIGHT -> this.toStringBeautifulHeight() + } + } + + override fun toString(): String { + val buffer = StringBuilder() + + buffer.append("[") + this.forEach { buffer.append("$it, ") } + val preResult = buffer.removeSuffix(", ").toString() + + val result = StringBuilder() + result.append(preResult) + result.append("]") + + return result.toString() + } + + open fun toStringBeautifulWidth(): String { + return if (this.root == null) { + "" + } else { + this.toStringBeautifulWidth(StringBuilder(), true, StringBuilder(), this.root!!).toString() + } + // not-null assertion operator because null check + } + + private fun toStringBeautifulWidth( + prefix: StringBuilder, + isTail: Boolean, + buffer: StringBuilder, + current: Node, + ): StringBuilder { + if (current.right != null) { + val newPrefix = StringBuilder() + newPrefix.append(prefix) + newPrefix.append(if (isTail) "| " else " ") + this.toStringBeautifulWidth(newPrefix, false, buffer, current.right as Node) + } + buffer.append(prefix) + buffer.append(if (isTail) "└── " else "┌── ") + buffer.append(current.toString()) + buffer.append("\n") + if (current.left != null) { + val newPrefix = StringBuilder() + newPrefix.append(prefix) + newPrefix.append(if (!isTail) "| " else " ") + this.toStringBeautifulWidth(newPrefix, true, buffer, current.left as Node) + } + + return buffer + } + + open fun toStringBeautifulHeight(ofSide: Int = 4): String { + if (this.root == null) { + return "" + } else { + val buffer: StringBuilder = StringBuilder() + + val lines: MutableList> = mutableListOf() + + var level: MutableList?> = mutableListOf() + var next: MutableList?> = mutableListOf() + + level.add(this.root) + + var nodeNumber = 1 + var widtest = 0 + + while (nodeNumber != 0) { + val line: MutableList = mutableListOf() + + nodeNumber = 0 + + for (node in level) { + if (node == null) { + line.add(null) + + next.add(null) + next.add(null) + } else { + val strNode: String = node.toString() + line.addLast(strNode) + + if (strNode.length > widtest) widtest = strNode.length + + next.add(node.left) + next.add(node.right) + + if (node.left != null) nodeNumber++ + if (node.right != null) nodeNumber++ + } + } + + widtest += widtest % 2 + + lines.add(line) + val swap = level + level = next + next = swap + next.clear() + } + + var perpiece: Int = lines[lines.size - 1].size * (widtest + ofSide) + + for (i in 1..perpiece / 2) buffer.append("─") + buffer.append("┐\n") + + for (i in 0.. = lines[i] + + val hpw: Int = floor(perpiece / 2f - 1).toInt() + + if (i > 0) { + for (j in 0.. + + @BeforeEach + fun setup() { + tree = AVLTree() + } + + // max and min tests + @Test + fun `max should return if empty`() { + assert(tree.max()?.key == null) + } + + @Test + fun `min should return if empty`() { + assert(tree.min()?.key == null) + } + + @Test + fun `max should return max key`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + tree.add(4, "D") + tree.add(5, "E") + tree.add(6, "F") + tree.add(7, "G") + tree.add(8, "H") + tree.add(9, "I") + tree.add(10, "J") + + assert(tree.max()?.key == 10) + } + + @Test + fun `min should return min key`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + tree.add(4, "D") + tree.add(5, "E") + tree.add(6, "F") + tree.add(7, "G") + tree.add(8, "H") + tree.add(9, "I") + tree.add(10, "J") + + assert(tree.min()?.key == 1) + } +} diff --git a/src/test/kotlin/AVLTreeTest.kt b/src/test/kotlin/AVLTreeTest.kt new file mode 100644 index 0000000..1a514ae --- /dev/null +++ b/src/test/kotlin/AVLTreeTest.kt @@ -0,0 +1,359 @@ +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class AVLTreeTest { + private lateinit var tree: AVLTree + + @BeforeEach + fun setup() { + tree = AVLTree() + } + + @Nested + inner class AddTests { + @Test + fun `Inserting a node into an empty tree`() { + val key = 1 + val value = "0" + + tree.add(key, value) + assertEquals(tree[key], value) + } + + @Test + fun `Inserting a duplicate node`() { + tree.add(1, "0") + tree.add(1, "0") + + val root = tree.root + assert(root == tree.getNode(1)) + assert(root?.right == null) + assert(root?.left == null) + } + + @Test + fun `Inserting with left rotate`() { + val key1 = 0 + val key2 = 1 + val key3 = 2 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + + val root = tree.root + assert(root?.key == key2) + assert(root?.left!!.key == key1) + assert(root.right!!.key == key3) + } + + @Test + fun `Inserting with right rotate`() { + val key1 = 2 + val key2 = 1 + val key3 = 0 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + + val root = tree.root + assert(root?.key == key2) + assert(root?.left!!.key == key3) + assert(root.right!!.key == key1) + } + + @Test + fun `Inserting with big left rotate (last node)`() { + val key1 = 2 + val key2 = 1 + val key3 = 5 + val key4 = 3 + val key5 = 6 + val key6 = 4 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + tree.add(key4, key4.toString()) + tree.add(key5, key5.toString()) + tree.add(key6, key6.toString()) + + val root = tree.root + assert(root == tree.getNode(3)) + } + + @Test + fun `Inserting with big right rotate (last node)`() { + val key1 = 5 + val key2 = 6 + val key3 = 4 + val key4 = 2 + val key5 = 3 + val key6 = 1 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + tree.add(key4, key4.toString()) + tree.add(key5, key5.toString()) + tree.add(key6, key6.toString()) + + val root = tree.root + assert(root == tree.getNode(3)) + } + } + + @Nested + inner class MinMaxTests() { + @Test + fun `Max should return max node`() { + for (i in 1..10) { + tree.add(i, "") + } + assert(tree.max()?.key == 10) + } + + @Test + fun `Max from null tree returns null`() { + assert(tree.max() == null) + } + + @Test + fun `Min should return min node`() { + for (i in 1..10) { + tree.add(i, "") + } + assert(tree.min()?.key == 1) + } + + @Test + fun `Min from null tree returns null`() { + assert(tree.min() == null) + } + } + + @Nested + inner class DeleteTests { + @Test + fun `Deletion of the non-existent node should do nothing`() { + tree.add(1, "0") + tree.add(2, "890") + + tree.delete(4) + + assert(tree.getNode(1) != null) + assert(tree.getNode(2) != null) + } + + @Test + fun `Deletion of tree's root`() { + val keyLeft = 0 + val keyRoot = 1 + val keyRight = 2 + + tree.add(keyLeft, keyLeft.toString()) + tree.add(keyRoot, keyRoot.toString()) + tree.add(keyRight, keyRight.toString()) + + tree.delete(1) + + val root = tree.root + assert(tree.getNode(1) == null) + assert(tree.getNode(keyRight) == root) + assert(root?.right == null) + } + + @Test + fun `Deletion of leaf(node with no children)`() { + val keyRoot = 0 + val key = 1 + val childKey = 2 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.add(childKey, childKey.toString()) + + tree.delete(2) + + val root = tree.root + assert(tree.getNode(childKey) == null) + assert(root?.right == null) + assert(root?.left == tree.getNode(0)) + } + + @Test + fun `Deletion of node with one child`() { + val key1 = 3 + val key2 = 2 + val key3 = 1 + val key4 = 0 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + tree.add(key4, key4.toString()) + + tree.delete(1) + + val root = tree.root + assert(tree.getNode(1) == null) + assert(tree.getNode(0) == root?.left) + } + + @Test + fun `Deletion of node with two children`() { + val key1 = 2 + val key2 = 1 + val key3 = 5 + val key4 = 3 + val key5 = 6 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + tree.add(key4, key4.toString()) + tree.add(key5, key5.toString()) + + tree.delete(5) + + val root = tree.root + assert(tree.getNode(5) == null) + assert(root?.right == tree.getNode(key5)) + + val newNode = tree.getNode(key5) + val childNewNode = newNode?.left + assert(newNode?.key!! > childNewNode?.key!!) + } + + @Test + fun `Deletion of node with two children, which also have child`() { + val key1 = 2 + val key2 = 1 + val key3 = 5 + val key4 = 3 + val key5 = 6 + val key6 = 0 + val key7 = 4 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + tree.add(key4, key4.toString()) + tree.add(key5, key5.toString()) + tree.add(key6, key6.toString()) + tree.add(key7, key7.toString()) + + tree.delete(5) + + val newNode = tree.getNode(key7) + val root = tree.root + + assert(tree.getNode(5) == null) + assert(newNode == root?.right) + assert(newNode?.right != null && newNode.left != null) + } + + @Test + fun `Deletion of node with two children, which also have child (version 2)`() { + val key1 = 5 + val key2 = 2 + val key3 = 6 + val key4 = 0 + val key5 = 3 + val key6 = 7 + val key7 = 4 + + tree.add(key1, key1.toString()) + tree.add(key2, key2.toString()) + tree.add(key3, key3.toString()) + tree.add(key4, key4.toString()) + tree.add(key5, key5.toString()) + tree.add(key6, key6.toString()) + tree.add(key7, key7.toString()) + + tree.delete(6) + + val newNode = tree.getNode(key1) + val root = tree.root + + assert(tree.getNode(6) == null) + assert(newNode == root?.right) + assert(newNode?.right != null && newNode.left != null) + } + } + + @Nested + inner class MergeTests { + @Test + fun `Merge should work correctly on 2 empty trees`() { + val secondTree = AVLTree() + + tree.merge(secondTree) + + assert(tree.root == null) + } + + @Test + fun `should merge second tree with empty tree`() { + val secondTree = AVLTree() + secondTree.add(1, "value1") + secondTree.add(2, "value2") + + tree.merge(secondTree) + + assert(secondTree.root == tree.root) + } + + @Test + fun `should merge empty second tree with initial tree`() { + val secondTree = AVLTree() + tree.add(1, "value1") + tree.add(2, "value2") + + tree.merge(secondTree) + + assert(tree.root == tree.root) + } + + @Test + fun `should merge non empty trees`() { + val secondTree = AVLTree() + tree.add(1, "value1") + tree.add(2, "value2") + secondTree.add(3, "value3") + secondTree.add(4, "value4") + + tree.merge(secondTree) + + // Check if the resulting tree contains all elements from both trees + var treeNodeCount = 0 + tree.iterator().forEach { _ -> treeNodeCount++ } + assert("value1" == tree.getNode(1)?.value) + assert("value2" == tree.getNode(2)?.value) + assert("value3" == tree.getNode(3)?.value) + assert("value4" == tree.getNode(4)?.value) + } + + @Test + fun `Merge should throw IllegalArgumentException on attempt to merge tree that isn't bigger than this`() { + val secondTree = AVLTree() + val keys = arrayOf(0, 1, -1) + val secondKeys = arrayOf(3, 4, 0) + + keys.forEach { tree.add(it, it.toString()) } + secondKeys.forEach { secondTree.add(it, it.toString()) } + + val message = + "Merge operation is defined only when attachable tree's keys is always bigger than base tree's keys" + + val exception = + assertFailsWith { + tree.merge(secondTree) + } + assert(exception.message == message) + } + } +} diff --git a/src/test/kotlin/BFSIteratorTest.kt b/src/test/kotlin/BFSIteratorTest.kt new file mode 100644 index 0000000..7feb5ec --- /dev/null +++ b/src/test/kotlin/BFSIteratorTest.kt @@ -0,0 +1,29 @@ +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BFSIteratorTest { + private lateinit var tree: BinaryTree + + @BeforeEach + fun setup() { + tree = BinaryTree() + } + + @Test + fun `BFS should work correctly empty tree`() { + var counter = 0 + for (i in tree.iterateBFS()) counter++ + assert(counter == 0) + } + + @Test + fun `DFS should work correctly simple tree sample`() { + val keys = arrayOf(2, 1, 3, 0, 4) + val result = mutableListOf() + val expectedResult = arrayOf(2, 1, 3, 0, 4) + keys.forEach { tree.add(it, it.toString()) } + for (i in tree.iterateBFS()) result.add(i.key) + + for (i in keys.indices) assert(result[i] == expectedResult[i]) + } +} diff --git a/src/test/kotlin/BinaryTreeTest.kt b/src/test/kotlin/BinaryTreeTest.kt new file mode 100644 index 0000000..541eb4b --- /dev/null +++ b/src/test/kotlin/BinaryTreeTest.kt @@ -0,0 +1,770 @@ +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertFailsWith + +class BinaryTreeTest { + private lateinit var tree: BinaryTree + + @BeforeEach + fun setup() { + tree = BinaryTree() + } + + // Positive + + @Test + fun `value should be added to empty tree`() { + val expectedResult = "A" + val key = 1 + + tree.add(key, expectedResult) + + assert(tree[key] contentEquals expectedResult) + } + + @Test + fun `root shouldn't be null after adding element to empty tree`() { + val expectedResult = "A" + val key = 1 + + tree.add(key, expectedResult) + + assert(tree.root != null) + } + + @Test + fun `value should added correctly left-left`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(firstKey, firstKey.toString()) + + assert( + tree.root?.key == thirdKey && + tree.root?.left?.key == secondKey && + tree.root?.left?.left?.key == firstKey, + ) + } + + @Test + fun `value should added correctly left-right`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(firstKey, firstKey.toString()) + tree.add(secondKey, secondKey.toString()) + + assert( + tree.root?.key == thirdKey && + tree.root?.left?.key == firstKey && + tree.root?.left?.right?.key == secondKey, + ) + } + + @Test + fun `value should added correctly right-left`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + + assert( + tree.root?.key == firstKey && + tree.root?.right?.key == thirdKey && + tree.root?.right?.left?.key == secondKey, + ) + } + + @Test + fun `value should added correctly right-right`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + + assert( + tree.root?.key == firstKey && + tree.root?.right?.key == secondKey && + tree.root?.right?.right?.key == thirdKey, + ) + } + + @Test + fun `Value should be added after set if it not in tree`() { + val expectedResult = "A" + val key = 1 + + tree[key] = expectedResult + + assert(tree[key] contentEquals expectedResult) + } + + @Test + fun `Value should be changed after set if it in tree`() { + val firstValue = "B" + val expectedResult = "A" + val key = 1 + tree[key] = firstValue + tree[key] = expectedResult + + assert(tree[key] contentEquals expectedResult) + } + + @Test + fun `Get should return value`() { + val key = 1 + val expectedResult = "A" + tree.add(key, expectedResult) + assert(tree[key] contentEquals expectedResult) + } + + @Test + fun `Get should return value left-left`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(firstKey, firstKey.toString()) + + assert(tree[firstKey] == firstKey.toString()) + } + + @Test + fun `Get should return value left-right`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(firstKey, firstKey.toString()) + tree.add(secondKey, secondKey.toString()) + + assert(tree[secondKey] == secondKey.toString()) + } + + @Test + fun `Get should return value right-left`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + + assert(tree[secondKey] == secondKey.toString()) + } + + @Test + fun `Get should return value right-right`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + + assert(tree[thirdKey] == thirdKey.toString()) + } + + @Test + fun `GetOrDefault should return value if it exist`() { + val key = 1 + val expectedResult = "A" + + tree.add(key, expectedResult) + assert(tree.getOrDefault(key, "B") == "A") + } + + @Test + fun `GetOrDefault should return default if it's not exist`() { + val key = 1 + val fakeKey = 2 + val expectedResult = "A" + + tree.add(key, expectedResult) + assert(tree.getOrDefault(fakeKey, "B") == "B") + } + + @Test + fun `IsKey should return true if key is exist (zig-zag)`() { + val keys = arrayOf(5, 1, 4, 2, 3) + keys.forEach { tree.add(it, it.toString()) } + + assert(tree.isKey(3)) + } + + @Test + fun `IsKey should return false if key isn't exist (zig-zag)`() { + val keys = arrayOf(6, 1, 5, 2, 4) + keys.forEach { tree.add(it, it.toString()) } + + assert(!tree.isKey(3)) + } + + @Test + fun `Min should return null from empty tree`() { + assert(tree.min() == null) + } + + @Test + fun `Min should return min from nonempty tree`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(firstKey, firstKey.toString()) + + assert(tree.min()?.key == firstKey) + } + + @Test + fun `Max should return null from empty tree`() { + assert(tree.max() == null) + } + + @Test + fun `Max should return min from nonempty tree`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(firstKey, firstKey.toString()) + + assert(tree.max()?.key == thirdKey) + } + + @Test + fun `getNext should return min of right child if it exist`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + + val root = tree.root + val right = root?.right + if (right != null) { + assert(tree.getNext(root) === tree.min(right)) + } else { + assert(false) + } + } + + @Test + fun `getNext should return parent if right child isn't exist`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(firstKey, firstKey.toString()) + tree.add(secondKey, secondKey.toString()) + + val secondNode = tree.getNode(secondKey) + val thirdNode = tree.getNode(thirdKey) + + if (secondNode != null) { + assert(tree.getNext(secondNode) === thirdNode) + } else { + assert(false) + } + } + + @Test + fun `getPrev should return max of left child if it exist`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + tree.add(firstKey, firstKey.toString()) + + val root = tree.root + val left = root?.left + if (left != null) { + assert(tree.getPrev(root) === tree.max(left)) + } else { + assert(false) + } + } + + @Test + fun `getPrev should return parent if left child isn't exist`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + + val secondNode = tree.getNode(secondKey) + val firstNode = tree.getNode(firstKey) + + if (secondNode != null) { + assert(tree.getPrev(secondNode) === firstNode) + } else { + assert(false) + } + } + + @Test + fun `Merge should make other root to this root if this root is null`() { + val secondTree = BinaryTree() + + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + + secondTree.merge(tree) + + assert(secondTree.root === tree.root) + } + + @Test + fun `Merge should make other root to this max if this root isn't null`() { + val secondTree = BinaryTree() + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + + secondTree.add(firstKey - 3, firstKey.toString()) + secondTree.add(secondKey - 3, secondKey.toString()) + secondTree.add(thirdKey - 3, thirdKey.toString()) + + val max = secondTree.max() + secondTree.merge(tree) + + assert(max?.right === tree.root) + } + + @Test + fun `Merge should work correctly on 2 empty trees`() { + val secondTree = BinaryTree() + + tree.merge(secondTree) + + assert(tree.root == null) + } + + @Test + fun `Split should work correctly`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + + val (small, big) = tree.split(secondKey) + + assert(small.root === tree.getNode(firstKey) && big.root === tree.getNode(thirdKey)) + } + + @Test + fun `Clone should clone only content not nodes`() { + val firstKey = -1 + val secondKey = 0 + val thirdKey = 1 + + tree.add(firstKey, firstKey.toString()) + tree.add(thirdKey, thirdKey.toString()) + tree.add(secondKey, secondKey.toString()) + + val clonedTree = tree.clone() + + assert( + tree.getNode(firstKey) == clonedTree.getNode(firstKey) && + tree.getNode(firstKey) !== clonedTree.getNode(firstKey) && + tree.getNode(secondKey) == clonedTree.getNode(secondKey) && + tree.getNode(secondKey) !== clonedTree.getNode(secondKey) && + tree.getNode(thirdKey) == clonedTree.getNode(thirdKey) && + tree.getNode(thirdKey) !== clonedTree.getNode(thirdKey), + ) + } + + @Test + fun `Delete from empty tree if key isn't in tree should work correctly(do nothing)`() { + tree.delete(15) + assert(true) + } + + @Test + fun `Delete should work correctly no children root`() { + val key = 1 + + tree.add(key, key.toString()) + tree.delete(key) + + assert(tree.root == null) + } + + @Test + fun `Delete should work correctly no children not-root left`() { + val keyRoot = 0 + val key = -1 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.delete(key) + + val root = tree.root + assert(tree.getNode(keyRoot) === root && root != null && root.left == null) + } + + @Test + fun `Delete should work correctly no children not-root right`() { + val keyRoot = 0 + val key = 1 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.delete(key) + + val root = tree.root + assert(tree.getNode(keyRoot) === root && root != null && root.right == null) + } + + @Test + fun `Delete should work correctly left child root`() { + val keyRoot = 0 + val key = -1 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + + tree.delete(keyRoot) + + assert(tree.getNode(key) === tree.root) + } + + @Test + fun `Delete should work correctly right child root`() { + val keyRoot = 0 + val key = 1 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.delete(keyRoot) + + val root = tree.root + assert(tree.getNode(key) === root) + } + + @Test + fun `Delete should work correctly left child not-root left`() { + val keyRoot = 0 + val key = -1 + val childKey = -2 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.add(childKey, childKey.toString()) + + val root = tree.root + tree.delete(key) + + assert(tree.getNode(keyRoot) === root) + assert(root != null && root.left === tree.getNode(childKey)) + assert(tree.getNode(childKey)?.parent === root) + } + + @Test + fun `Delete should work correctly right child not-root left`() { + val keyRoot = 0 + val key = -2 + val childKey = -1 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.add(childKey, childKey.toString()) + + val root = tree.root + tree.delete(key) + + assert(tree.getNode(keyRoot) === root) + assert(root != null && root.left === tree.getNode(childKey)) + assert(tree.getNode(childKey)?.parent === root) + } + + @Test + fun `Delete should work correctly left child not-root right`() { + val keyRoot = 0 + val key = 2 + val childKey = 1 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.add(childKey, childKey.toString()) + + val root = tree.root + tree.delete(key) + + assert(tree.getNode(keyRoot) === root) + assert(root != null && root.right === tree.getNode(childKey)) + assert(tree.getNode(childKey)?.parent === root) + } + + @Test + fun `Delete should work correctly right child not-root right`() { + val keyRoot = 0 + val key = 1 + val childKey = 2 + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + tree.add(childKey, childKey.toString()) + + val root = tree.root + tree.delete(key) + + assert( + tree.getNode(keyRoot) === root && + root != null && + root.right === tree.getNode(childKey) && + tree.getNode(childKey)?.parent === root, + ) + } + + // Madness is following... + + @Test + fun `Delete should work correctly 2 child root where next haven't children`() { + val keyRoot = 0 + val keyRight = 1 + val keyLeft = -1 + tree.add(keyRoot, keyRoot.toString()) + tree.add(keyRight, keyRight.toString()) + tree.add(keyLeft, keyLeft.toString()) + + tree.delete(keyRoot) + + val root = tree.root + assert(tree.getNode(keyRight) === root) + assert(root != null && root.left === tree.getNode(keyLeft)) + assert(tree.getNode(keyLeft)?.parent === root) + } + + @Test + fun `Delete should work correctly 2 child root where next has not left children`() { + // scenario explanation on picture + // + // ┌── 3 ┌── 3 + // | | ┌── 2 | └── 2 + // | └── 1 ──> 1 + // 0 (0 delete) └── -1 + // └── -1 + + val keys = arrayOf(0, -1, 3, 1, 2) + + keys.forEach { tree.add(it, it.toString()) } + + val next = tree.getNext(tree.root!!) + + tree.delete(keys[0]) + + val root = tree.root + if (root != null) { + assert(root == next) + assert(root.left === tree.getNode(keys[1])) + assert(tree.getNode(keys[1])?.parent === root) + assert(root.right === tree.getNode(keys[2])) + assert(tree.getNode(keys[2])?.parent === root) + assert(root.right != null && root.right?.left === tree.getNode(keys[4])) + assert(tree.getNode(keys[4])?.parent === root.right) + } else { + assert(false) + } + } + + @Test + fun `delete should work correctly 2 child not-root`() { + // scenario explanation on picture + // ┌── 3 ┌── 3 + // ┌── 2 | └── 1 + // | └── 1 ──> 0 + // 0 (2 delete) └── -1 + // └── -1 + + val keys = arrayOf(0, -1, 2, 1, 3) + keys.forEach { tree.add(it, it.toString()) } + + val node = tree.getNode(keys[4]) + tree.delete(keys[2]) + + val root = tree.root + if (root != null) { + assert(root.left === tree.getNode(keys[1])) + assert(tree.getNode(keys[1])?.parent === root) + assert(node == root.right && root.right?.left === tree.getNode(keys[3])) + } else { + assert(false) + } + } + + @Test + fun `delete should work correctly 2 child not-root but next is left child of its parent`() { + // scenario explanation on picture + // 0 0 + // | ┌── -1 | ┌── -1 + // | ┌── -2 ──> └── -2 + // └── -3 (-3 delete) └── -4 + // └── -4 + + val keys = arrayOf(0, -3, -2, -4, -1) + keys.forEach { tree.add(it, it.toString()) } + + tree.delete(keys[1]) + + val root = tree.root + val node = tree.getNode(keys[2]) + if (root != null && node != null) { + assert(root.left === node) + assert(node.parent === root) + assert(node.left === tree.getNode(keys[3])) + assert(tree.getNode(keys[3])?.parent === node) + assert(node.right === tree.getNode(keys[4])) + assert(tree.getNode(keys[4])?.parent === node) + } else { + assert(false) + } + } + + // Yes, writing test on this function is very easy + + @Test + fun `toStringBeautifulWidth should work correctly on empty tree`() { + assert(tree.toStringBeautifulWidth() == "") + } + + @Test + fun `toStringBeautifulWidth should work correctly on sample`() { + // no-child, left, right and 2 child node example + // because it works similar on all this scenarios + val keys = arrayOf(0, 1, -1, 2, -2) + keys.forEach { tree.add(it, it.toString()) } + + val strTree = tree.toStringBeautifulWidth() + val expectedResult = """| ┌── (2: 2) +| ┌── (1: 1) +└── (0: 0) + └── (-1: -1) + └── (-2: -2) +""" + assert(strTree == expectedResult) + } + + @Test + fun `toStringBeautifulHeight should work correctly on empty tree`() { + assert(tree.toStringBeautifulHeight() == "") + } + + @Test + fun `toStringBeautifulHeight should work correctly on sample`() { + // no-child, left, right and 2 child node example + // because it works similar on all this scenarios + val keys = arrayOf(0, 1, -1, 2, -2) + keys.forEach { tree.add(it, it.toString()) } + + val strTree = tree.toStringBeautifulHeight() + val expectedResult = """────────────────────────┐ + (0: 0) + ┌───────────┴───────────┐ + (-1: -1) (1: 1) + ┌─────┘ └─────┐ + (-2: -2) (2: 2) +""" + assert(strTree == expectedResult) + } + + @Test + fun `toString should work correctly on empty tree`() { + assert(tree.toString() == "[]") + } + + @Test + fun `toString should work correctly on sample`() { + // no-child, left, right and 2 child node example + // because it works similar on all this scenarios + val keys = arrayOf(0, 1, -1, 2, -2) + keys.forEach { tree.add(it, it.toString()) } + + val strTree = tree.toString() + val expectedResult = "[(-2: -2), (-1: -1), (0: 0), (1: 1), (2: 2)]" + assert(strTree == expectedResult) + } + + // Negative + + @Test + fun `Add should throw IllegalArgumentException on attempt to add key to root that already exist `() { + val key = 1 + val message = + "Multiple uses of key: $key. To modify value use set function" + + tree.add(key, key.toString()) + + val exception = + assertFailsWith { + tree.add(key, key.toString()) + } + assert(exception.message == message) + } + + @Test + fun `Add should throw IllegalArgumentException on attempt to add key to not-root that already exist`() { + val keyRoot = 0 + val key = 1 + val message = + "Multiple uses of key: $key. To modify value use set function" + tree.add(keyRoot, keyRoot.toString()) + tree.add(key, key.toString()) + + val exception = + assertFailsWith { + tree.add(key, key.toString()) + } + assert(exception.message == message) + } + + @Test + fun `Merge should throw IllegalArgumentException on attempt to merge tree that isn't bigger than this`() { + val secondTree = BinaryTree() + val keys = arrayOf(0, 1, -1) + val secondKeys = arrayOf(3, 4, 0) + + keys.forEach { tree.add(it, it.toString()) } + secondKeys.forEach { secondTree.add(it, it.toString()) } + + val message = + "Merge operation is defined only when attachable tree's keys is always bigger than base tree's keys" + + val exception = + assertFailsWith { + tree.merge(secondTree) + } + assert(exception.message == message) + } +} diff --git a/src/test/kotlin/ByKeyIteratorTest.kt b/src/test/kotlin/ByKeyIteratorTest.kt new file mode 100644 index 0000000..65fc303 --- /dev/null +++ b/src/test/kotlin/ByKeyIteratorTest.kt @@ -0,0 +1,29 @@ +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ByKeyIteratorTest { + private lateinit var tree: BinaryTree + + @BeforeEach + fun setup() { + tree = BinaryTree() + } + + @Test + fun `BFS should work correctly empty tree`() { + var counter = 0 + for (i in tree) counter++ + assert(counter == 0) + } + + @Test + fun `DFS should work correctly simple tree sample`() { + val keys = arrayOf(2, 1, 3, 0, 4) + val result = mutableListOf() + val expectedResult = arrayOf(0, 1, 2, 3, 4) + keys.forEach { tree.add(it, it.toString()) } + for (i in tree) result.add(i.key) + + for (i in keys.indices) assert(result[i] == expectedResult[i]) + } +} diff --git a/src/test/kotlin/DFSIteratorTest.kt b/src/test/kotlin/DFSIteratorTest.kt new file mode 100644 index 0000000..0a0ee34 --- /dev/null +++ b/src/test/kotlin/DFSIteratorTest.kt @@ -0,0 +1,51 @@ +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class DFSIteratorTest { + private lateinit var tree: BinaryTree + + @BeforeEach + fun setup() { + tree = BinaryTree() + } + + @Test + fun `DFS should work correctly empty tree`() { + var counter = 0 + for (i in tree.iterateDFS()) counter++ + assert(counter == 0) + } + + @Test + fun `DFS should work correctly simple tree sample simple mode`() { + val keys = arrayOf(2, 1, 3, 0, 4) + val result = mutableListOf() + val expectedResult = arrayOf(2, 1, 0, 3, 4) + keys.forEach { tree.add(it, it.toString()) } + for (i in tree.iterateDFS()) result.add(i.key) + + for (i in keys.indices) assert(result[i] == expectedResult[i]) + } + + @Test + fun `DFS should work correctly simple tree sample symmetric mode`() { + val keys = arrayOf(2, 1, 3, 0, 4) + val result = mutableListOf() + val expectedResult = arrayOf(0, 1, 2, 3, 4) + keys.forEach { tree.add(it, it.toString()) } + for (i in tree.iterateDFS(mode = Tree.ModeDFS.INORDER)) result.add(i.key) + + for (i in keys.indices) assert(result[i] == expectedResult[i]) + } + + @Test + fun `DFS should work correctly simple tree sample reverse mode`() { + val keys = arrayOf(2, 1, 3, 0, 4) + val result = mutableListOf() + val expectedResult = arrayOf(0, 1, 4, 3, 2) + keys.forEach { tree.add(it, it.toString()) } + for (i in tree.iterateDFS(mode = Tree.ModeDFS.POSTORDER)) result.add(i.key) + + for (i in keys.indices) assert(result[i] == expectedResult[i]) + } +} diff --git a/src/test/kotlin/NodeTest.kt b/src/test/kotlin/NodeTest.kt new file mode 100644 index 0000000..8051a60 --- /dev/null +++ b/src/test/kotlin/NodeTest.kt @@ -0,0 +1,63 @@ +import org.junit.jupiter.api.Test + +class NodeTest { + @Test + fun `Node should be equal if values and keys are equal`() { + val key = 15 + val value = "Mitochondria" + + val nodeOne = Node>(key, value) + val nodeSecond = Node>(key, value) + + assert(nodeOne == nodeSecond) + } + + @Test + fun `Node should be equal if values aren't equal`() { + val key = 15 + val valueOne = "Mitochondria" + val valueTwo = "Mit0ch0ndr1a" + + val nodeOne = Node>(key, valueOne) + val nodeSecond = Node>(key, valueTwo) + + assert(nodeOne != nodeSecond) + } + + @Test + fun `Node should be equal if keys aren't equal`() { + val keyOne = 15 + val keyTwo = 150 + val value = "Mitochondria" + + val nodeOne = Node>(keyOne, value) + val nodeSecond = Node>(keyTwo, value) + + assert(nodeOne != nodeSecond) + } + + @Test + fun `Node should be equal if keys aren't equal and values aren't equal`() { + val keyOne = 15 + val keyTwo = 150 + val valueOne = "Mitochondria" + val valueTwo = "Mit0ch0ndr1a" + + val nodeOne = Node>(keyOne, valueOne) + val nodeSecond = Node>(keyTwo, valueTwo) + + assert(nodeOne != nodeSecond) + } + + @Test + fun `Node shouldn't be equal to notNode`() { + val keyOne = 15 + val valueOne = "Mitochondria" + + val nodeOne = Node>(keyOne, valueOne) + + val notNode = arrayOf(keyOne, valueOne) + + assert(nodeOne != notNode) + } +} diff --git a/src/test/kotlin/RBNodeTest.kt b/src/test/kotlin/RBNodeTest.kt new file mode 100644 index 0000000..7b7b250 --- /dev/null +++ b/src/test/kotlin/RBNodeTest.kt @@ -0,0 +1,126 @@ +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class RBNodeTest { + private lateinit var tree: RBTree + + @BeforeEach + fun setup() { + tree = RBTree() + } + + // toString tests + @Test + fun `should return null if tree is empty`() { + assert(tree.root?.toString() == null) + } + + @Test + fun `should return node with black color`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + + assert(tree.getNode(1)?.toString() == "(1: A)") + } + + @Test + fun `should return node with red color`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + tree.setColored(true) + + assert(tree.getNode(1)?.toString() == "\u001B[31m(1: A)\u001B[0m") + } + + // isOnLeft tests + @Test + fun `isOnLeft should return false to root node`() { + tree.add(1, "A") + assert(!(tree.root as Node.RBNode).isOnLeft) + } + + @Test + fun `isOnLeft should return true`() { + tree.add(1, "A") + tree.add(2, "D") + tree.add(3, "C") + + assert((tree.getNode(1) as Node.RBNode).isOnLeft) + } + + @Test + fun `isOnLeft should return false`() { + tree.add(1, "A") + tree.add(2, "D") + tree.add(3, "C") + + assert(!(tree.getNode(3) as Node.RBNode).isOnLeft) + } + + // sibling tests + @Test + fun `sibling should return null if parent is null`() { + tree.add(1, "A") + assert((tree.root as Node.RBNode).sibling() == null) + } + + @Test + fun `sibling should return right node`() { + tree.add(1, "A") + tree.add(2, "D") + tree.add(3, "C") + + assert((tree.getNode(1) as Node.RBNode).sibling()?.value == "C") + } + + @Test + fun `sibling should return left node`() { + tree.add(1, "A") + tree.add(2, "D") + tree.add(3, "C") + + assert((tree.getNode(3) as Node.RBNode).sibling()?.value == "A") + } + + // hasRedChild tests + @Test + fun `should return false if has no child`() { + tree.add(1, "A") + + assert(!(tree.getNode(1) as Node.RBNode).hasRedChild()) + } + + @Test + fun `should return false if both children are black`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + + // because Red-Black tree is self-balancing, it balances the nodes automatically, that is why for the sake of + // the test we need to set the color of the nodes to black manually + tree.getNode(1)?.let { (it as Node.RBNode).color = Node.RBNode.Color.BLACK } + tree.getNode(3)?.let { (it as Node.RBNode).color = Node.RBNode.Color.BLACK } + + assert(!(tree.getNode(2) as Node.RBNode).hasRedChild()) + } + + @Test + fun `should return true if left child is red`() { + tree.add(2, "B") + tree.add(1, "A") + tree.setColored(true) + + assert((tree.getNode(2) as Node.RBNode).hasRedChild()) + } + + @Test + fun `should return true if right child is red`() { + tree.add(1, "A") + tree.add(2, "B") + tree.setColored(true) + + assert((tree.getNode(1) as Node.RBNode).hasRedChild()) + } +} diff --git a/src/test/kotlin/RedBlackTreeTest.kt b/src/test/kotlin/RedBlackTreeTest.kt new file mode 100644 index 0000000..fa3e514 --- /dev/null +++ b/src/test/kotlin/RedBlackTreeTest.kt @@ -0,0 +1,685 @@ +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class RedBlackTreeTest { + private lateinit var tree: RBTree + + @BeforeEach + fun setup() { + tree = RBTree() + } + + @Nested + inner class AddTests { + @Test + fun `root should be black if has single node`() { + tree.add(1, "") + assert((tree.root as Node.RBNode?)?.color == Node.RBNode.Color.BLACK) + } + + @Test + fun `root should be black if has multiple nodes`() { + for (i in 1..10) { + tree.add(i, "") + } + assert((tree.root as Node.RBNode?)?.color == Node.RBNode.Color.BLACK) + } + + @Test + fun `root should be black if has multiple nodes and root is deleted`() { + for (i in 1..10) { + tree.add(i, "") + } + tree.root?.let { tree.delete(it.key) } + assert((tree.root as Node.RBNode?)?.color == Node.RBNode.Color.BLACK) + } + + @Test + fun `should right rotate when add, then change root`() { + tree.add(85, "A") + tree.add(56, "B") + tree.add(27, "C") + + val root = tree.root as Node.RBNode? + val left = root?.left as Node.RBNode? + val right = root?.right as Node.RBNode? + + assert(root != null && root.key == 56 && root.color == Node.RBNode.Color.BLACK) + assert(left?.key == 27 && left.color == Node.RBNode.Color.RED) + assert(right?.key == 85 && right.color == Node.RBNode.Color.RED) + } + } + + @Nested + inner class MergeTests { + @Test + fun `Merge should work correctly on 2 empty trees`() { + val secondTree = RBTree() + + tree.merge(secondTree) + + assert(tree.root == null) + } + + @Test + fun `should merge second tree with empty tree`() { + val secondTree = RBTree() + secondTree.add(1, "value1") + secondTree.add(2, "value2") + + tree.merge(secondTree) + + assert(secondTree.root == tree.root) + } + + @Test + fun `should merge empty second tree with initial tree`() { + val secondTree = RBTree() + tree.add(1, "value1") + tree.add(2, "value2") + + tree.merge(secondTree) + + assert(tree.root == tree.root) + } + + @Test + fun `should merge non empty trees`() { + val secondTree = RBTree() + tree.add(1, "value1") + tree.add(2, "value2") + secondTree.add(3, "value3") + secondTree.add(4, "value4") + + tree.merge(secondTree) + + // Check if the resulting tree contains all elements from both trees + var treeNodeCount = 0 + tree.iterator().forEach { _ -> treeNodeCount++ } + assert(4 == treeNodeCount) + assert("value1" == tree.getNode(1)?.value) + assert("value2" == tree.getNode(2)?.value) + assert("value3" == tree.getNode(3)?.value) + assert("value4" == tree.getNode(4)?.value) + } + + @Test + fun `Merge should throw IllegalArgumentException on attempt to merge tree that isn't bigger than this`() { + val secondTree = RBTree() + val keys = arrayOf(0, 1, -1) + val secondKeys = arrayOf(3, 4, 0) + + keys.forEach { tree.add(it, it.toString()) } + secondKeys.forEach { secondTree.add(it, it.toString()) } + + val message = + "Merge operation is defined only when attachable tree's keys is always bigger than base tree's keys" + + val exception = + assertFailsWith { + tree.merge(secondTree) + } + assert(exception.message == message) + } + } + + // Tests for setColored + @Test + fun `setColored should set color of node`() { + tree.add(1, "A") + tree.add(2, "A") + tree.add(3, "A") + + tree.setColored(true) + assert(tree.getNode(1)?.toString() == "\u001B[31m(1: A)\u001B[0m") + + tree.setColored(false) + assert(tree.getNode(1)?.toString() == "(1: A)") + } + + @Nested + inner class BalanceTreeTests { + @Test + fun `balanceRBTree should return balanced tree`() { + for (i in 1..10) { + tree.add(i, "") + } + + val expectedBalanced = arrayOf(4, 2, 6, 1, 3, 5, 8, 7, 9, 10) + var index = 0 + tree.iterateBFS().forEach { + assert(it.key == expectedBalanced[index]) + index++ + } + } + + @Test + fun `should convert red node and it's sibling node to black if added new red node on right of grandparent`() { + // scenario explanation on picture + // | ┌── (93 Red) | ┌── (93 Black) + // └── (83 Black) -----------> └── (83 Black) + // | (add 71 Red) | ┌── (71 Red ) + // └── (47 Red) └── (47 Black) + + tree.add(47, "47") + tree.add(83, "83") + tree.add(93, "93") + tree.add(71, "71") + + val root = tree.root as Node.RBNode? + val addedNode = tree.getNode(71) as Node.RBNode? + val parent = addedNode?.parent as Node.RBNode? + val uncle = root?.right as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(parent?.color == Node.RBNode.Color.BLACK) + assert(uncle?.color == Node.RBNode.Color.BLACK) + assert(addedNode?.color == Node.RBNode.Color.RED) + } + + @Test + fun `should convert red node and it's sibling node to black if added new red node on left of grandparent`() { + // scenario explanation on picture + // | ┌── (93 Red) | ┌── (93 Black) + // | | -----------> | | ┌── (90 Red) + // └── (83 Black) (add 71 Red) └── (83 Black) + // └── (47 Red) └── (47 Black) + + tree.add(47, "47") + tree.add(83, "83") + tree.add(93, "93") + tree.add(90, "90") + + val root = tree.root as Node.RBNode? + val addedNode = tree.getNode(90) as Node.RBNode? + val parent = addedNode?.parent as Node.RBNode? + val uncle = root?.right as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(parent?.color == Node.RBNode.Color.BLACK) + assert(uncle?.color == Node.RBNode.Color.BLACK) + assert(addedNode?.color == Node.RBNode.Color.RED) + } + + @Test + fun `should right rotate if added red node after red node on left of parent`() { + // scenario explanation on picture + // | ┌── (95 Red) | ┌── (95 Red) + // | ┌── (93 Black ) | ┌── (94 Black ) + // └── (83 Black ) -----------> | | └── (93 Red) + // └── (47 Black ) (add 94 Red) └── (83 Black ) + // └── (47 Black ) + + tree.add(47, "") + tree.add(83, "") + tree.add(93, "") + tree.add(95, "") + tree.add(94, "") + + val root = tree.root as Node.RBNode? + val addedNode = tree.getNode(94) as Node.RBNode? + val leftChild = addedNode?.left as Node.RBNode? + val rightChild = addedNode?.right as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(addedNode?.color == Node.RBNode.Color.BLACK) + assert(leftChild?.color == Node.RBNode.Color.RED) + assert(rightChild?.color == Node.RBNode.Color.RED) + } + + @Test + fun `should left rotate if added red node after red node on right of parent`() { + // scenario explanation on picture + // | | ┌── (93 Red) + // | ┌── (93 Black ) | ┌── (92 Black ) + // | | └── (90 Red) | | └── (90 Red) + // └── (83 Black ) -----------> └── (83 Black ) + // └── (47 Black ) (add 92 Red) └── (47 Black ) + // + + tree.add(47, "") + tree.add(83, "") + tree.add(93, "") + tree.add(90, "") + tree.add(92, "") + + val root = tree.root as Node.RBNode? + val addedNode = tree.getNode(92) as Node.RBNode? + val leftChild = addedNode?.left as Node.RBNode? + val rightChild = addedNode?.right as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(addedNode?.color == Node.RBNode.Color.BLACK) + assert(leftChild?.color == Node.RBNode.Color.RED) + assert(rightChild?.color == Node.RBNode.Color.RED) + } + } + + /** + * WARNING: + * because Red-Black tree is self-balancing, it balances the nodes automatically, that is why for the sake of + * the test we need to set the color of the nodes manually. The cases are totally possible in the real world, I + * set colors manually to make easier to understand the test cases. + */ + @Nested + inner class DeleteNodeTests { + @Test + fun `should not find node and return`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + + tree.delete(4) + + assert(tree.getNode(1) != null) + assert(tree.getNode(2) != null) + assert(tree.getNode(3) != null) + } + + @Test + fun `should delete root with no children`() { + tree.add(1, "A") + + tree.delete(1) + + assert(tree.root == null) + } + + @Test + fun `should delete right red node with no children`() { + tree.add(1, "A") + tree.add(2, "B") + + tree.delete(2) + val root = tree.root as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 1) + assert(root.left == null) + assert(root.right == null) + assert(tree.getNode(2) == null) + } + + @Test + fun `should delete left red node with no children`() { + tree.add(2, "B") + tree.add(1, "A") + + tree.delete(1) + val root = tree.root as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 2) + assert(root.left == null) + assert(root.right == null) + assert(tree.getNode(1) == null) + } + + @Test + fun `should delete root with one child`() { + tree.add(1, "A") + tree.add(2, "B") + + tree.delete(1) + val root = tree.root as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 2) + assert(root.left == null) + assert(root.right == null) + assert(tree.getNode(1) == null) + } + + @Test + fun `should delete node with 2 children and red successor`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + + tree.delete(2) + val root = tree.root as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 3) + assert(root.right == null) + assert(root.left?.key == 1) + assert(tree.getNode(2) == null) + } + + @Test + fun `should delete black node with one red child`() { + tree.add(1, "A") + tree.add(2, "B") + tree.add(3, "C") + tree.add(4, "D") + + tree.delete(3) + + val root = tree.root as Node.RBNode? + + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 2) + assert(root.left?.key == 1) + assert(root.right?.key == 4) + assert(tree.getNode(3) == null) + } + + @Test + fun `should delete black node with one black child`() { + // scenario explanation on picture + // | ┌── (6 Black) | ┌── (6 Black) + // | ┌── (5 Black) | ┌── (5 Red) + // | | └── (4 Black) -----------> | | └── (4 Black) + // └── (3 Black) (delete 2) └── (3 Black) + // └── (2 Black) └── (1 Black) + // └── (1 Black) + tree.add(3, "C") + tree.add(2, "B") + tree.add(4, "D") + tree.add(1, "A") + tree.add(5, "E") + tree.add(6, "F") + + (tree.getNode(1) as Node.RBNode?)?.color = Node.RBNode.Color.BLACK + (tree.getNode(4) as Node.RBNode?)?.color = Node.RBNode.Color.BLACK + (tree.getNode(6) as Node.RBNode?)?.color = Node.RBNode.Color.BLACK + + tree.delete(2) + + val root = tree.root as Node.RBNode? + val rootRight = root?.right as Node.RBNode? + val rootRightRight = rootRight?.right as Node.RBNode? + val rootRightLeft = rootRight?.left as Node.RBNode? + // check if the tree is balanced + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 3) + assert(root.left?.key == 1) + assert(root.right?.key == 5) + assert(rootRight?.color == Node.RBNode.Color.RED) + assert(rootRightLeft?.key == 4) + assert(rootRightLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootRightRight?.key == 6) + assert(rootRightRight!!.color == Node.RBNode.Color.BLACK) + assert(tree.getNode(2) == null) + } + + @Test + fun `should delete black node with 2 black children`() { + // scenario explanation on picture + // | ┌── (6 Red) | ┌── (6 Red) + // | ┌── (5 Black) | ┌── (5 Black) + // | | └── (4 Red) ------------> └── (4 Black) + // └── (3 Black) └── (2 Black) + // └── (2 Black) └── (1 Red) + // └── (1 Red) + + tree.add(3, "C") + tree.add(2, "B") + tree.add(4, "D") + tree.add(1, "A") + tree.add(5, "E") + tree.add(6, "F") + + tree.delete(3) + + val root = tree.root as Node.RBNode? + val rootRight = root?.right as Node.RBNode? + val rootLeft = root?.left as Node.RBNode? + val rootLeftLeft = rootLeft?.left as Node.RBNode? + val rootRightRight = rootRight?.right as Node.RBNode? + + // check if the tree is balanced + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 4) + assert(rootLeft?.key == 2) + assert(rootRight?.key == 5) + assert(rootRight!!.color == Node.RBNode.Color.BLACK) + assert(rootLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootLeftLeft?.key == 1) + assert(rootLeftLeft!!.color == Node.RBNode.Color.RED) + assert(rootRightRight?.key == 6) + assert(rootRightRight!!.color == Node.RBNode.Color.RED) + assert(tree.getNode(3) == null) + } + + @Test + fun `should delete red node with 2 children and fixDoubleBlack sibling on left`() { + // scenario explanation on picture + // | ┌── (93 Black) | ┌── (93 Black) + // | ┌── (90 Red) | ┌── (90 Red) + // | | | ┌── (81 Red) | | | ┌── (81 Red) + // | | └── (69 Black) | | └── (69 Black) + // | | └── (62 Red) | | └── (62 Red) + // └── (53 Black) -----------> └── (53 Black) + // | ┌── (43 Black) (delete 32) | ┌── (43 Black) + // └── (32 Red) └── (27 Red) + // | ┌── (27 Red) └── (17 Black) + // └── (17 Black) + val array = + arrayOf( + 32, + 81, + 17, + 90, + 93, + 43, + 27, + 53, + 69, + 62, + ) + + for (num in array) { + tree.add(num, "$num") + } + tree.delete(32) + + val root = tree.root as Node.RBNode? + val rootLeft = root?.left as Node.RBNode? + val rootLeftLeft = rootLeft?.left as Node.RBNode? + val rootLeftRight = rootLeft?.right as Node.RBNode? + + // check if the tree is balanced + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 53) + assert(rootLeft?.key == 27) + assert(rootLeft!!.color == Node.RBNode.Color.RED) + assert(rootLeftLeft?.key == 17) + assert(rootLeftLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootLeftRight?.key == 43) + assert(rootLeftRight!!.color == Node.RBNode.Color.BLACK) + assert(tree.getNode(32) == null) + } + + @Test + fun `should delete black node with no children, and fixDoubleBlack sibling on left and sibling's left child is red`() { + // scenario explanation on picture + // | ┌── (93 Black) | ┌── (90 Black) + // | ┌── (90 Red) | | └── (81 Red) + // | | | ┌── (81 Red) | ┌── (69 Red) + // | | └── (69 Black) | | └── (62 Black) + // | | └── (62 Red) -----------> └── (53 Black) + // └── (53 Black) (delete 93) | ┌── (43 Black) + // | ┌── (43 Black) └── (27 Red) + // └── (27 Red) └── (17 Black) + // └── (17 Black) + val array = + arrayOf( + 32, + 81, + 17, + 90, + 93, + 43, + 27, + 53, + 69, + 62, + ) + + for (num in array) { + tree.add(num, "$num") + } + tree.delete(32) + tree.delete(93) + + val root = tree.root as Node.RBNode? + val rootRight = root?.right as Node.RBNode? + val rootRightLeft = rootRight?.left as Node.RBNode? + val rootRightRight = rootRight?.right as Node.RBNode? + val rootRightRightLeft = rootRightRight?.left as Node.RBNode? + + // check if the tree is balanced + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 53) + assert(rootRight?.key == 69) + assert(rootRight!!.color == Node.RBNode.Color.RED) + assert(rootRightLeft?.key == 62) + assert(rootRightLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootRightRight?.key == 90) + assert(rootRightRight!!.color == Node.RBNode.Color.BLACK) + assert(rootRightRightLeft?.key == 81) + assert(rootRightRightLeft!!.color == Node.RBNode.Color.RED) + assert(tree.getNode(93) == null) + } + + @Test + fun `should delete black node with no children, fixDoubleBlack sibling on right`() { + // scenario explanation on picture + // | ┌── (93 Black) | ┌── (93 Black) + // | ┌── (84 Red) | ┌── (84 Red) + // | | └── (71 Black) | | └── (71 Black) + // | | └── (68 Red) | | └── (68 Red) + // └── (60 Black) -----------> └── (60 Black) + // | ┌── (59 Black) (delete 23) | ┌── (59 Black) + // | | └── (49 Red) └── (49 Red) + // └── (42 Red) └── (42 Black) + // └── (23 Black) + val array = + arrayOf( + 60, + 84, + 23, + 71, + 93, + 59, + 68, + 42, + 49, + ) + for (num in array) { + tree.add(num, "$num") + } + tree.delete(23) + + val root = tree.root as Node.RBNode? + val rootRight = root?.right as Node.RBNode? + val rootLeft = root?.left as Node.RBNode? + val rootRightLeft = rootRight?.left as Node.RBNode? + val rootRightRight = rootRight?.right as Node.RBNode? + val rootRightLeftLeft = rootRightLeft?.left as Node.RBNode? + val rootLeftRight = rootLeft?.right as Node.RBNode? + val rootLeftLeft = rootLeft?.left as Node.RBNode? + + // check if the tree is balanced + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 60) + assert(rootLeft?.key == 49) + assert(rootLeft!!.color == Node.RBNode.Color.RED) + assert(rootLeftLeft?.key == 42) + assert(rootLeftLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootLeftRight?.key == 59) + assert(rootLeftRight!!.color == Node.RBNode.Color.BLACK) + assert(rootRight?.key == 84) + assert(rootRight!!.color == Node.RBNode.Color.RED) + assert(rootRightLeft?.key == 71) + assert(rootRightLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootRightRight?.key == 93) + assert(rootRightRight!!.color == Node.RBNode.Color.BLACK) + assert(rootRightLeftLeft?.key == 68) + assert(rootRightLeftLeft!!.color == Node.RBNode.Color.RED) + assert(tree.getNode(23) == null) + } + + @Test + fun `should delete black node with 2 children, child's sibling - red, fixDoubleBlack sibling on right`() { + // scenario explanation on picture + // | ┌── (88 Red) + // | ┌── (78 Black) | ┌── (88 Black) + // | ┌── (69 Red) | ┌── (78 Red) + // | | └── (45 Black) | | └── (69 Black) + // └── (28 Black) -----------> └── (45 Black) + // | ┌── (21 Red) (delete 28) | ┌── (21 Red) + // └── (17 Black) └── (17 Black) + // └── (1 Red) └── (1 Red) + val array = + arrayOf( + 21, + 69, + 28, + 17, + 42, + 1, + 78, + 88, + 45, + ) + for (num in array) { + tree.add(num, "$num") + } + tree.delete(42) + tree.delete(28) + + val root = tree.root as Node.RBNode? + val rootRight = root?.right as Node.RBNode? + val rootLeft = root?.left as Node.RBNode? + val rootRightLeft = rootRight?.left as Node.RBNode? + val rootRightRight = rootRight?.right as Node.RBNode? + val rootLeftRight = rootLeft?.right as Node.RBNode? + val rootLeftLeft = rootLeft?.left as Node.RBNode? + + // check if the tree is balanced + assert(root != null && root.color == Node.RBNode.Color.BLACK) + assert(root!!.key == 45) + assert(rootLeft?.key == 17) + assert(rootLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootLeftLeft?.key == 1) + assert(rootLeftLeft!!.color == Node.RBNode.Color.RED) + assert(rootLeftRight?.key == 21) + assert(rootLeftRight!!.color == Node.RBNode.Color.RED) + assert(rootRight?.key == 78) + assert(rootRight!!.color == Node.RBNode.Color.RED) + assert(rootRightLeft?.key == 69) + assert(rootRightLeft!!.color == Node.RBNode.Color.BLACK) + assert(rootRightRight?.key == 88) + assert(rootRightRight!!.color == Node.RBNode.Color.BLACK) + assert(tree.getNode(28) == null) + assert(tree.getNode(42) == null) + } + } + + @Test + fun `toStringBeautifulHeight should work correctly on empty tree`() { + assert(tree.toStringBeautifulHeight() == "") + } + + @Test + fun `toStringBeautifulHeight should work correctly on sample`() { + // no-child, left, right and 2 child node example + // because it works similar on all this scenarios + val keys = arrayOf(0, 1, -1, 2, -2) + keys.forEach { tree.add(it, it.toString()) } + + val strTree = tree.toStringBeautifulHeight() + val expectedResult = """────────────────────────┐ + (0: 0) + ┌───────────┴───────────┐ + (-1: -1) (1: 1) + ┌─────┘ └─────┐ + (-2: -2) (2: 2) +""" + assert(strTree == expectedResult) + } +} diff --git a/trees_output.jpg b/trees_output.jpg new file mode 100644 index 0000000..d189935 Binary files /dev/null and b/trees_output.jpg differ