diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ae1749f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +labels: bug + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. macOS] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg new file mode 100644 index 0000000..2b337d7 --- /dev/null +++ b/.github/badges/jacoco.svg @@ -0,0 +1 @@ +coverage91.9% \ No newline at end of file diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..58e342a --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,18 @@ +version: 2 +mergeable: + - when: pull_request.* + name: "Description check" + validate: + - do: description + no_empty: + enabled: true + message: Description matter and should not be empty. Provide detail with **what** was changed, **why** it was changed, and **how** it was changed. + + - when: pull_request.opened + name: "Greet a contributor" + validate: [] + pass: + - do: comment + payload: + body: > + Thanks for creating a pull request! A maintainer will review your changes shortly. Please don't be discouraged if it takes a while. diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml new file mode 100644 index 0000000..969b9e3 --- /dev/null +++ b/.github/workflows/codecoverage.yml @@ -0,0 +1,63 @@ +name: Measure coverage + +on: + workflow_dispatch: + pull_request: + +permissions: write-all + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest, macos-latest ] + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: zulu + + - name: Create Coverage Report + run: | + chmod +x gradlew + ./gradlew jacocoTestReport + + - if: matrix.os == 'ubuntu-latest' + name: Generate Jacoco Badge + uses: cicirello/jacoco-badge-generator@v2.11.0 + with: + jacoco-csv-file: lib/build/jacoco/report.csv + badges-directory: .github/badges + generate-coverage-badge: true + coverage-badge-filename: jacoco.svg + + - name: Check for modified files + id: git-check + run: | + modified=$(git status --porcelain) + if [ -n "$modified" ]; then + echo "modified=true" >> $GITHUB_ENV + else + echo "modified=false" >> $GITHUB_ENV + fi + shell: bash + + - name: Commit the badges (if they changed) + if: env.git-check == 'true' + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + git add .github/badges/* + git commit -am "ci: autogenerate JaCoCo coverage badge" + git push + shell: bash + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ed46039 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: Build & Run JUnit5 tests + +on: + workflow_dispatch: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: zulu + + - name: Build & Test by Gradle + run: ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ab266e --- /dev/null +++ b/.gitignore @@ -0,0 +1,219 @@ +# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,macos,windows,linux,gradle,kotlin +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,macos,windows,linux,gradle,kotlin + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Kotlin ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/intellij+all,macos,windows,linux,gradle,kotlin diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..787043c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# 💪 Interested in contributing? + +Good boy! Please read the few sections below to understand how to implement new features. + +> 😅 Be positive! Even if your changes don't get merged in forest-group, please don't be too sad, you will always be able to run workflows directly from your fork! +> Meow + +## 🤝 Accepted contributions + +The following contributions are accepted: + + + + + + + + + + + + + + + + + + + + + + + + + +
SectionChangesAdditionsNotes
🧩 Features✔️✔️ +
    +
  • New features are allowed, but must be optional and backward compatible
  • +
+
🧪 Tests✔️✔️ +
    +
  • Everything that makes forest-group more stable is welcomed!
  • +
+
🗃️ Repository +
    +
  • Workflows, license, readmes, etc. usually don't need to be edited
  • +
+
+ +**Legend** +* ✔️: Contributions welcomed! +* ✓: Contributions are welcomed, but must be discussed first +* ❌: Only maintainers can manage these files diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5d7d8c4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) 2024 Gavrilenko Mike, Shakirov Karim, Vlasenco Daniel + + 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..3acbef0 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# forest-group + +[![Kotlin 1.9.23][kotlin_img]][kotlin_releases_url] +[![Tests passing][tests_passing_img]][tests_workflow_url] +[![Code Coverage][code_coverage_badge_img]][code_coverage_workflow_url] +[![License][license_img]][repo_license_url] + +`forest-group` is a library that lets you easily create and use [Binary search trees](https://en.wikipedia.org/wiki/Binary_search_tree), [AVL trees](https://en.wikipedia.org/wiki/AVL_tree) and [Red-black trees](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) in your applications! + +## Table of contents + +- [About the project](#about-the-project) +- [Usage](#usage) +- [Contributing](#contributing) +- [License](#license) +- [Authors](#authors) + +## About The Project + +Trees are well-known for their speed and efficiency. + +But there are lots of tree implementations that are strict to data types that can be stored. + +The solution is easy - *just use keys to store anything you want!* + +## Usage + +To create a tree, pass the key type and your stored data type to a generic. *Note that your key should implement Comparable type.* + +```kotlin +val myBSTree = BSTree() + +val myAVLTree = AVLTree() + +val myRBTree = RBTree() +``` + +You now can simply insert and replace values in your tree: + +```kotlin +val myKey = 10 + +myBSTree.insert(myKey, "Something important") + +val replacedValue = myBSTree.insert(myKey, "Something more important") +``` + +You can also search for values and delete values from tree by keys: + +```kotlin +val myValue1 = myBSTree.search(myKey) + +val myValue2 = myBSTree.delete(myKey) + +println(myValue1 == myValue2) // true +``` + +All trees are iterable by Pair(key, value), so they can be used in a for loop. + +Iterator implements inorder traversal (every next key is greater than the previous). + +```kotlin +for ((key, value) in myRBTree) { + keysList.add(key) + valuesList.add(value) +} + +myRBTree.forEach { println(it) } // prints every pair +``` + +## Contributing + +If you have found a bug, or want to propose some useful feature for our project, please firstly read our [Contribution Rules][contribute_rules_url] and +do the following: +1. Fork the Project +2. Create your Feature Branch (git checkout -b feature/my-feature) +3. Commit your Changes (git commit -m 'add some feature') +4. Push to the Branch (git push origin feature/my-feature) +5. Open a Pull Request + +## License + +Distributed under the [MIT License][repo_license_url]. + +## Authors + +- [Shakirov Karim](https://github.com/kar1mgh) +- [Vlasenco Daniel](https://github.com/spisladqo) +- [Gavrilenko Mike](https://github.com/qrutyy) + +_______________________________ + +[*Java gnomik*][java_gnomik_url] + + + +[kotlin_img]: https://img.shields.io/badge/Kotlin-%201.9.23-magenta +[tests_passing_img]: https://img.shields.io/badge/tests-Passing-green +[license_img]: https://img.shields.io/badge/license-MIT-blue +[code_coverage_badge_img]: https://raw.githubusercontent.com/spbu-coding-2023/trees-2/main/.github/badges/jacoco.svg + + + +[tests_workflow_url]: https://github.com/spbu-coding-2023/trees-2/actions/workflows/test.yml +[repo_license_url]: https://github.com/spbu-coding-2023/trees-2/blob/main/LICENSE.md +[contribute_rules_url]: https://github.com/spbu-coding-2023/trees-2/blob/main/CONTRIBUTING.md +[code_coverage_workflow_url]: https://github.com/spbu-coding-2023/trees-2/actions/workflows/codecoverage.yml + + + +[kotlin_releases_url]: https://kotlinlang.org/docs/releases.html#release-details +[java_gnomik_url]: https://ibb.co/54hJVd2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d4d7e1f --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,15 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +commons-math3 = "3.6.1" +guava = "32.1.3-jre" +junit-jupiter-engine = "5.10.0" + +[libraries] +commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter-engine" } + +[plugins] +jvm = { id = "org.jetbrains.kotlin.jvm", version = "1.9.20" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a80b22c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 0000000..0bbd9c1 --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,81 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Kotlin library project to get you started. + * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.6/userguide/building_java_projects.html in the Gradle documentation. + */ + +plugins { + // Apply the java-library plugin for API and implementation separation. + `java-library` + + kotlin("jvm") version "1.9.23" + + jacoco + + id("com.ncorti.ktfmt.gradle") version "0.17.0" +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // Use the Kotlin JUnit 5 integration. + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + + // Use the JUnit 5 integration. + testImplementation(libs.junit.jupiter.engine) + + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + // This dependency is exported to consumers, that is to say found on their compile classpath. + api(libs.commons.math3) + + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + implementation(libs.guava) + implementation(kotlin("stdlib-jdk8")) +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +tasks.named("test") { + // Use JUnit Platform for unit tests. + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) // report generates after run +} + +// Configuring JaCoCo plugin settings +jacoco { + toolVersion = "0.8.11" + reportsDirectory.set(layout.buildDirectory.dir("coverage")) // directory for reports +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) // tests are required to run before generating the report + + reports { + xml.required = false + html.required = true + csv.required = true + csv.outputLocation.set(layout.buildDirectory.file("jacoco/report.csv")) + } +} + +ktfmt { + googleStyle() + + blockIndent.set(4) + + continuationIndent.set(4) + + removeUnusedImports.set(true) +} + +tasks.register("format") { dependsOn(tasks.ktfmtFormat) } diff --git a/lib/src/main/kotlin/tsl/iterator/BinaryTreeIterator.kt b/lib/src/main/kotlin/tsl/iterator/BinaryTreeIterator.kt new file mode 100644 index 0000000..c5b83de --- /dev/null +++ b/lib/src/main/kotlin/tsl/iterator/BinaryTreeIterator.kt @@ -0,0 +1,28 @@ +package tsl.iterator + +import tsl.nodes.AbstractNode +import tsl.trees.AbstractBinaryTree + +internal class BinaryTreeIterator, V, N : AbstractNode>( + tree: AbstractBinaryTree +) : Iterator> { + + private var stack = ArrayDeque() + private var currentNode: N? = tree.root + + override fun hasNext(): Boolean { + return (!stack.isEmpty() || currentNode != null) + } + + override fun next(): Pair { + while (currentNode != null) { + currentNode?.let { stack.addLast(it) } + currentNode = currentNode?.leftChild + } + + val nextNode = stack.removeLast() + currentNode = nextNode.rightChild + + return Pair(nextNode.key, nextNode.value) + } +} diff --git a/lib/src/main/kotlin/tsl/nodes/AVLNode.kt b/lib/src/main/kotlin/tsl/nodes/AVLNode.kt new file mode 100644 index 0000000..b980b81 --- /dev/null +++ b/lib/src/main/kotlin/tsl/nodes/AVLNode.kt @@ -0,0 +1,7 @@ +package tsl.nodes + +class AVLNode, V>(key: K, value: V) : + AbstractNode>(key, value) { + + internal var height = 0 +} diff --git a/lib/src/main/kotlin/tsl/nodes/AbstractNode.kt b/lib/src/main/kotlin/tsl/nodes/AbstractNode.kt new file mode 100644 index 0000000..fadc6c5 --- /dev/null +++ b/lib/src/main/kotlin/tsl/nodes/AbstractNode.kt @@ -0,0 +1,11 @@ +package tsl.nodes + +abstract class AbstractNode, V, N : AbstractNode>( + var key: K, + var value: V +) { + internal var leftChild: N? = null + internal var rightChild: N? = null + + internal fun hasTwoChildren(): Boolean = (this.leftChild != null && this.rightChild != null) +} diff --git a/lib/src/main/kotlin/tsl/nodes/BSTNode.kt b/lib/src/main/kotlin/tsl/nodes/BSTNode.kt new file mode 100644 index 0000000..1c8529b --- /dev/null +++ b/lib/src/main/kotlin/tsl/nodes/BSTNode.kt @@ -0,0 +1,4 @@ +package tsl.nodes + +class BSTNode, V>(key: K, value: V) : + AbstractNode>(key, value) diff --git a/lib/src/main/kotlin/tsl/nodes/RBNode.kt b/lib/src/main/kotlin/tsl/nodes/RBNode.kt new file mode 100644 index 0000000..92ebc63 --- /dev/null +++ b/lib/src/main/kotlin/tsl/nodes/RBNode.kt @@ -0,0 +1,14 @@ +package tsl.nodes + +class RBNode, V>(key: K, value: V) : + AbstractNode>(key, value) { + + internal var parent: RBNode? = null + + enum class Color { + Black, + Red, + } + + internal var color = Color.Red +} diff --git a/lib/src/main/kotlin/tsl/trees/AVLTree.kt b/lib/src/main/kotlin/tsl/trees/AVLTree.kt new file mode 100644 index 0000000..a9c82bc --- /dev/null +++ b/lib/src/main/kotlin/tsl/trees/AVLTree.kt @@ -0,0 +1,147 @@ +package tsl.trees + +import kotlin.math.max +import tsl.nodes.AVLNode + +class AVLTree, V> : AbstractBinaryTree>() { + + public override fun insert(key: K, value: V): V? { + val oldValueByKey = search(key) + + root = insertNodeAndBalanceRec(root, key, value) + + return oldValueByKey + } + + private fun insertNodeAndBalanceRec( + currNode: AVLNode?, + keyToInsert: K, + valueToInsert: V + ): AVLNode { + + if (currNode == null) { + val newNode = AVLNode(keyToInsert, valueToInsert) + return newNode + } + + when { + keyToInsert < currNode.key -> + currNode.leftChild = + insertNodeAndBalanceRec(currNode.leftChild, keyToInsert, valueToInsert) + keyToInsert > currNode.key -> + currNode.rightChild = + insertNodeAndBalanceRec(currNode.rightChild, keyToInsert, valueToInsert) + else -> { + currNode.value = valueToInsert + return currNode + } + } + + var balancedNode: AVLNode = currNode + + if (getBalanceFactor(currNode) < -1) { + currNode.leftChild?.let { + if (keyToInsert > it.key) currNode.leftChild = rotateLeft(it) + + balancedNode = rotateRight(currNode) + } + } else if (getBalanceFactor(currNode) > 1) { + currNode.rightChild?.let { + if (keyToInsert < it.key) currNode.rightChild = rotateRight(it) + + balancedNode = rotateLeft(currNode) + } + } + + updateHeight(balancedNode.leftChild) + updateHeight(balancedNode.rightChild) + updateHeight(balancedNode) + + return balancedNode + } + + private fun rotateRight(oldUpperNode: AVLNode): AVLNode { + val newUpperNode = oldUpperNode.leftChild ?: return oldUpperNode + + oldUpperNode.leftChild = newUpperNode.rightChild + newUpperNode.rightChild = oldUpperNode + + return newUpperNode + } + + private fun rotateLeft(oldUpperNode: AVLNode): AVLNode { + val newUpperNode = oldUpperNode.rightChild ?: return oldUpperNode + + oldUpperNode.rightChild = newUpperNode.leftChild + newUpperNode.leftChild = oldUpperNode + + return newUpperNode + } + + private fun updateHeight(node: AVLNode?): Int { + if (node == null) return -1 + node.height = max(getHeight(node.leftChild), getHeight(node.rightChild)) + 1 + return node.height + } + + private fun getBalanceFactor(node: AVLNode): Int = + getHeight(node.rightChild) - getHeight(node.leftChild) + + private fun getHeight(node: AVLNode?): Int = node?.height ?: -1 + + public override fun delete(key: K): V? { + val deletedValue = search(key) ?: return null + + root = deleteNodeAndBalanceRec(root, key) + + return deletedValue + } + + private fun deleteNodeAndBalanceRec(currNode: AVLNode?, keyToDelete: K): AVLNode? { + + if (currNode == null) return null + + when { + keyToDelete < currNode.key -> + currNode.leftChild = deleteNodeAndBalanceRec(currNode.leftChild, keyToDelete) + keyToDelete > currNode.key -> + currNode.rightChild = deleteNodeAndBalanceRec(currNode.rightChild, keyToDelete) + keyToDelete == currNode.key -> { + if (currNode.hasTwoChildren()) { + val successor = getMinNodeRec(currNode.rightChild) + if (successor != null) { + currNode.key = successor.key + currNode.value = successor.value + + val newSubtree = deleteNodeAndBalanceRec(currNode.rightChild, successor.key) + currNode.rightChild = newSubtree + } + } else { + return currNode.leftChild ?: currNode.rightChild + } + } + } + + var balancedNode: AVLNode = currNode + + if (getBalanceFactor(currNode) < -1) { + currNode.leftChild?.let { + if (getBalanceFactor(it) > 0) currNode.leftChild = rotateLeft(it) + + balancedNode = rotateRight(currNode) + } + } else if (getBalanceFactor(currNode) > 1) { + currNode.rightChild?.let { + if (getBalanceFactor(it) < 0) currNode.rightChild = rotateRight(it) + + balancedNode = rotateLeft(currNode) + } + } + + updateHeight(balancedNode.leftChild) + updateHeight(balancedNode.rightChild) + updateHeight(balancedNode) + + return balancedNode + } +} diff --git a/lib/src/main/kotlin/tsl/trees/AbstractBinaryTree.kt b/lib/src/main/kotlin/tsl/trees/AbstractBinaryTree.kt new file mode 100644 index 0000000..a2379c7 --- /dev/null +++ b/lib/src/main/kotlin/tsl/trees/AbstractBinaryTree.kt @@ -0,0 +1,53 @@ +package tsl.trees + +import tsl.iterator.BinaryTreeIterator +import tsl.nodes.AbstractNode + +abstract class AbstractBinaryTree, V, N : AbstractNode> : + Iterable> { + + internal var root: N? = null + + public abstract fun delete(key: K): V? + + public abstract fun insert(key: K, value: V): V? + + public fun search(key: K): V? = searchNodeRec(root, key)?.value + + protected fun searchNodeRec(currNode: N?, keyToSearch: K): N? { + return when { + currNode == null -> null + keyToSearch == currNode.key -> currNode + keyToSearch < currNode.key -> searchNodeRec(currNode.leftChild, keyToSearch) + else -> searchNodeRec(currNode.rightChild, keyToSearch) + } + } + + public fun clear() { + root = null + } + + public fun isEmpty(): Boolean = root == null + + public fun getMinKey(): K? = getMinNodeRec(root)?.key + + protected fun getMinNodeRec(node: N?): N? { + return when { + node == null -> null + node.leftChild == null -> node + else -> getMinNodeRec(node.leftChild) + } + } + + public fun getMaxKey(): K? = getMaxNodeRec(root)?.key + + protected fun getMaxNodeRec(node: N?): N? { + return when { + node == null -> null + node.rightChild == null -> node + else -> getMaxNodeRec(node.rightChild) + } + } + + public override fun iterator(): Iterator> = BinaryTreeIterator(this) +} diff --git a/lib/src/main/kotlin/tsl/trees/BSTree.kt b/lib/src/main/kotlin/tsl/trees/BSTree.kt new file mode 100644 index 0000000..5834465 --- /dev/null +++ b/lib/src/main/kotlin/tsl/trees/BSTree.kt @@ -0,0 +1,77 @@ +package tsl.trees + +import tsl.nodes.BSTNode + +class BSTree, V> : AbstractBinaryTree>() { + + public override fun insert(key: K, value: V): V? { + val nodeToInsert = BSTNode(key, value) + + if (root == null) { + root = nodeToInsert + return null + } + + var currentNode = root + + while (currentNode != null) { + when { + nodeToInsert.key == currentNode.key -> { + val oldValue = currentNode.value + currentNode.value = nodeToInsert.value + return oldValue + } + nodeToInsert.key < currentNode.key -> { + if (currentNode.leftChild == null) { + currentNode.leftChild = nodeToInsert + return null + } + currentNode = currentNode.leftChild + } + nodeToInsert.key > currentNode.key -> { + if (currentNode.rightChild == null) { + currentNode.rightChild = nodeToInsert + return null + } + currentNode = currentNode.rightChild + } + } + } + return currentNode?.value + } + + public override fun delete(key: K): V? { + val deletedValue: V? = search(key) ?: return null + + deleteRecursively(root, key) + + return deletedValue + } + + private fun deleteRecursively(currentNode: BSTNode?, keyToDelete: K): BSTNode? { + if (currentNode == null) return null + + when { + keyToDelete < currentNode.key -> + currentNode.leftChild = deleteRecursively(currentNode.leftChild, keyToDelete) + keyToDelete > currentNode.key -> + currentNode.rightChild = deleteRecursively(currentNode.rightChild, keyToDelete) + keyToDelete == currentNode.key -> { + if (currentNode.hasTwoChildren()) { + val minRightNode = getMinNodeRec(currentNode.rightChild) + if (minRightNode != null) { + currentNode.key = minRightNode.key + currentNode.value = minRightNode.value + currentNode.rightChild = + deleteRecursively(currentNode.rightChild, minRightNode.key) + } + } else { + if (currentNode == root) root = null + return currentNode.leftChild ?: currentNode.rightChild + } + } + } + + return currentNode + } +} diff --git a/lib/src/main/kotlin/tsl/trees/RBTree.kt b/lib/src/main/kotlin/tsl/trees/RBTree.kt new file mode 100644 index 0000000..69ecc25 --- /dev/null +++ b/lib/src/main/kotlin/tsl/trees/RBTree.kt @@ -0,0 +1,278 @@ +package tsl.trees + +import tsl.nodes.RBNode + +class RBTree, V> : AbstractBinaryTree>() { + + // @return Null if 'insert' method haven't replaced any value. Return value if there was some + // replace + + public override fun insert(key: K, value: V): V? { + + val newNode = RBNode(key, value) + if (root == null) { + root = RBNode(key, value) + fixAfterInsertion(newNode) + return null + } + + var currentNode: RBNode? = root + val returnValue: V? = search(key) + + // traverse the tree to find the insertion point (node) + while (currentNode != null) { + if (currentNode.key > newNode.key) { + if (currentNode.leftChild == null) { + currentNode.leftChild = newNode + newNode.parent = currentNode + fixAfterInsertion(newNode) + break + } + currentNode = currentNode.leftChild + } else if (currentNode.key < newNode.key) { + if (currentNode.rightChild == null) { + currentNode.rightChild = newNode + newNode.parent = currentNode + fixAfterInsertion(newNode) + break + } + currentNode = currentNode.rightChild + } else { + currentNode.value = value + fixAfterInsertion(newNode) + break + } + } + return returnValue + } + + private fun fixAfterInsertion(node: RBNode?) { + if (node?.parent == null) root.also { it?.color = RBNode.Color.Black } + var newNode = node + var uncle: RBNode? + var newRoot = root + + while (newNode?.parent?.color == RBNode.Color.Red) { + val currentParent: RBNode? = newNode.parent + val currentGrandParent: RBNode? = currentParent?.parent + if (currentParent == currentGrandParent?.leftChild) { + uncle = currentParent?.parent?.rightChild + if (uncle?.color == RBNode.Color.Red) { + currentParent?.color = RBNode.Color.Black + uncle.color = RBNode.Color.Black + currentGrandParent?.color = RBNode.Color.Red + newNode = currentGrandParent + } else { + if ( + currentParent?.color == RBNode.Color.Red && + newNode.color == RBNode.Color.Red && + newNode == currentParent.rightChild + ) { + newRoot = rotateLeft(currentParent, newRoot) + newNode.color = RBNode.Color.Black + newNode.leftChild?.color = RBNode.Color.Red + newRoot = rotateRight(currentGrandParent, newRoot) + newNode.rightChild?.color = RBNode.Color.Red + break + } + currentParent?.color = RBNode.Color.Black + currentGrandParent?.color = RBNode.Color.Red + newRoot = rotateRight(currentGrandParent, newRoot) + } + } else if (currentParent == currentGrandParent?.rightChild) { + uncle = newNode.parent?.parent?.leftChild + if (uncle?.color == RBNode.Color.Red) { + currentParent?.color = RBNode.Color.Black + uncle.color = RBNode.Color.Black + currentGrandParent?.color = RBNode.Color.Red + newNode = currentGrandParent + } else { + if ( + currentParent?.color == RBNode.Color.Red && + newNode.color == RBNode.Color.Red && + newNode == currentParent.leftChild + ) { + currentParent.leftChild?.color = RBNode.Color.Black + newRoot = rotateRight(currentParent, newRoot) + } + currentParent?.color = RBNode.Color.Black + currentGrandParent?.color = RBNode.Color.Red + newRoot = rotateLeft(currentGrandParent, newRoot) + } + } + } + newRoot?.color = RBNode.Color.Black + root = newRoot + } + + // @return Value if delete was successful and null if it isn't. + + public override fun delete(key: K): V? { + val deletingNodeValue = search(key) + if (deletingNodeValue == null) return null // in case we do not found the deletinnode + root = deleteNode(key) + root?.color = RBNode.Color.Black // ensure the root is black after deletion + return deletingNodeValue + } + + // @return The new root node of the Red\-Black Tree after deletion\. + + private fun deleteNode( + key: K, + ): RBNode? { + var current: RBNode? = searchNodeRec(root, key) + if (current == null) return null + var newRoot = root + + if (current.hasTwoChildren()) { + val successor = getMaxNodeRec(current.leftChild) + if (successor?.parent?.leftChild == successor) successor?.parent?.leftChild = null + else successor?.parent?.rightChild = null + if (successor != null) { + current.value = successor.value + current.key = successor.key + } + current = successor + if (successor?.color == RBNode.Color.Red) { + return newRoot + } + } + + val child = if (current?.leftChild != null) current.leftChild else current?.rightChild + if (child != null) { + child.parent = current?.parent + if (current?.parent == null) return child + if (current == current.parent?.leftChild) { + current.parent?.leftChild = child + } else { + current.parent?.rightChild = child + } + + if (current.color == RBNode.Color.Black) { + newRoot = fixAfterDeletion(child) + } + } else if (current?.parent == null) { + return null + } else { + if (current.color == RBNode.Color.Black) { + newRoot = fixAfterDeletion(current) + } + if (current.parent?.leftChild == current) { + current.parent?.leftChild = null + } else { + current.parent?.rightChild = null + } + } + return newRoot + } + + // @return The root of the tree after fixing the structure to maintain the red-black properties + + private fun fixAfterDeletion(currentDeletingNode: RBNode?): RBNode? { + var fixedRoot = root + var currentNode = currentDeletingNode + // currentNode - variable that is used to perform various operations and control the loop of + // fixing the tree. + + while (currentNode != root && currentNode?.color == RBNode.Color.Black) { + if (currentNode == currentNode.parent?.leftChild) { + var currentSibling: RBNode? = currentNode.parent?.rightChild + if (currentSibling?.color == RBNode.Color.Red) { // 1st case "currentSibling is red" + currentSibling.color = RBNode.Color.Black + currentNode.parent?.color = RBNode.Color.Red + fixedRoot = rotateLeft(currentNode.parent, fixedRoot) + } else { + // 2nd case "black currentSibling, right child is black" + currentSibling?.color = currentNode.parent?.color ?: RBNode.Color.Black + currentNode.parent?.color = RBNode.Color.Black + currentSibling?.rightChild?.color = RBNode.Color.Black + fixedRoot = rotateLeft(currentNode.parent, fixedRoot) + currentNode = fixedRoot + } + } else { + var currentSibling: RBNode? = currentNode.parent?.leftChild + if (currentSibling?.color == RBNode.Color.Red) { + // 1st case "red currentSibling" + currentSibling.color = RBNode.Color.Black + currentNode.parent?.color = RBNode.Color.Red + fixedRoot = rotateRight(currentNode.parent, fixedRoot) + } else { + // 2nd case "black currentSibling, left child is red" + currentSibling?.color = currentNode.parent?.color ?: RBNode.Color.Black + currentNode.parent?.color = RBNode.Color.Black + currentSibling?.leftChild?.color = RBNode.Color.Black + fixedRoot = rotateRight(currentNode.parent, fixedRoot) + currentNode = fixedRoot + } + } + } + fixAfterInsertion(fixedRoot) + currentNode?.color = RBNode.Color.Black + return fixedRoot + } + + private fun rotateRight( + nodeToRotate: RBNode?, + currentRoot: RBNode?, + ): RBNode? { + val leftChild = nodeToRotate?.leftChild + var newRoot = currentRoot + + leftChild?.parent = nodeToRotate?.parent + nodeToRotate?.parent = leftChild + + if (leftChild?.rightChild != null) { + nodeToRotate.leftChild = leftChild.rightChild + leftChild.rightChild?.parent = nodeToRotate + leftChild.rightChild = nodeToRotate + } else { + leftChild?.rightChild = nodeToRotate + nodeToRotate?.leftChild = null + } + + if (leftChild?.parent != null) { + if (leftChild.parent?.leftChild == nodeToRotate) { + leftChild.parent?.leftChild = leftChild + } else { + leftChild.parent?.rightChild = leftChild + } + } else { + root = leftChild + newRoot = leftChild + } + return newRoot + } + + private fun rotateLeft( + nodeToRotate: RBNode?, + currentRoot: RBNode?, + ): RBNode? { + val rightChild = nodeToRotate?.rightChild + var newRoot = currentRoot + + rightChild?.parent = nodeToRotate?.parent + nodeToRotate?.parent = rightChild + + if (rightChild?.leftChild != null) { + nodeToRotate.rightChild = rightChild.leftChild + rightChild.leftChild?.parent = nodeToRotate.rightChild + rightChild.leftChild = nodeToRotate + } else { + rightChild?.leftChild = nodeToRotate + nodeToRotate?.rightChild = null + } + + // rightChild took over "nodeToRotate" place so now its parent should point to rightChild + if (rightChild?.parent != null) { + if (rightChild.parent?.rightChild == nodeToRotate) { + rightChild.parent?.rightChild = rightChild + } else { + rightChild.parent?.leftChild = rightChild + } + } else { + root = rightChild + newRoot = rightChild + } + return newRoot + } +} diff --git a/lib/src/test/kotlin/tsl/trees/AVLTreeTest.kt b/lib/src/test/kotlin/tsl/trees/AVLTreeTest.kt new file mode 100644 index 0000000..4572784 --- /dev/null +++ b/lib/src/test/kotlin/tsl/trees/AVLTreeTest.kt @@ -0,0 +1,842 @@ +package tsl.trees + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class AVLTreeTest { + private lateinit var avlTree: AVLTree + + @BeforeEach + fun setup() { + avlTree = AVLTree() + } + + @Nested + inner class InsertTests { + // Common tests + + @Test + fun `insert of new key should return null`() { + val expectedValue = null + val actualValue = avlTree.insert(1, "I am just a freak") + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `insert of existing key should replace old value with new value and return old value`() { + avlTree.insert(1, "But I love you so") + + val expectedReturnValue = "But I love you so" + val actualReturnValue = avlTree.insert(1, "Please let me go") + + val expectedNewValue = "Please let me go" + val actualNewValue = avlTree.search(1) + + assertEquals(expectedReturnValue, actualReturnValue, "Returned wrong value") + assertEquals(expectedNewValue, actualNewValue, "Value wasn't replaced with new value") + } + + // Small tree tests + + @Test + fun `insertion of left child triggering right rotation should balance small tree`() { + avlTree.insert(20, "Take") + avlTree.insert(15, "To church") + avlTree.insert(4, "Me") + + val expectedStructure = listOf(Pair(4, "Me"), Pair(15, "To church"), Pair(20, "Take")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 15) + } + + @Test + fun `insertion of right child triggering left rotation should balance small tree`() { + avlTree.insert(4, "A") + avlTree.insert(15, "B") + avlTree.insert(20, "C") + + val expectedStructure = listOf(Pair(4, "A"), Pair(15, "B"), Pair(20, "C")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 15) + } + + // Medium tree tests + + @Test + fun `insertion of right child triggering left-right rotation should balance medium tree`() { + avlTree.insert(20, "I") + avlTree.insert(4, "Swear") + avlTree.insert(26, "I'll only") + avlTree.insert(9, "Make") + avlTree.insert(3, "You") + + avlTree.insert(15, "Cry") + + val expectedStructure = + listOf( + Pair(3, "You"), + Pair(4, "Swear"), + Pair(9, "Make"), + Pair(15, "Cry"), + Pair(20, "I"), + Pair(26, "I'll only") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 9) + } + + @Test + fun `insertion of left child triggering left-right rotation should balance medium tree`() { + avlTree.insert(20, "E") + avlTree.insert(4, "B") + avlTree.insert(26, "F") + avlTree.insert(3, "A") + avlTree.insert(9, "D") + + avlTree.insert(8, "C") + + val expectedStructure = + listOf( + Pair(3, "A"), + Pair(4, "B"), + Pair(8, "C"), + Pair(9, "D"), + Pair(20, "E"), + Pair(26, "F") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 9) + } + + @Test + fun `insertion of right child triggering right-left rotation should balance medium tree`() { + avlTree.insert(20, "B") + avlTree.insert(4, "A") + avlTree.insert(26, "E") + avlTree.insert(24, "C") + avlTree.insert(30, "F") + + avlTree.insert(25, "D") + + val expectedStructure = + listOf( + Pair(4, "A"), + Pair(20, "B"), + Pair(24, "C"), + Pair(25, "D"), + Pair(26, "E"), + Pair(30, "F") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 24) + } + + @Test + fun `insertion of left child triggering right-left rotation should balance medium tree`() { + avlTree.insert(20, "B") + avlTree.insert(4, "A") + avlTree.insert(26, "E") + avlTree.insert(24, "D") + avlTree.insert(30, "F") + + avlTree.insert(21, "C") + + val expectedStructure = + listOf( + Pair(4, "A"), + Pair(20, "B"), + Pair(21, "C"), + Pair(24, "D"), + Pair(26, "E"), + Pair(30, "F") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 24) + } + + // Large tree tests + + @Test + fun `insertion of right child triggering left-right rotation should balance large tree`() { + avlTree.insert(20, "Master") + avlTree.insert(4, "Of puppets") + avlTree.insert(26, "I'm pulling") + avlTree.insert(3, "Your") + avlTree.insert(9, "Strings") + avlTree.insert(21, "Twisting") + avlTree.insert(30, "Your mind") + avlTree.insert(2, "And") + avlTree.insert(7, "Smashing") + avlTree.insert(11, "Your") + + avlTree.insert(15, "Dreams") + + val expectedStructure = + listOf( + Pair(2, "And"), + Pair(3, "Your"), + Pair(4, "Of puppets"), + Pair(7, "Smashing"), + Pair(9, "Strings"), + Pair(11, "Your"), + Pair(15, "Dreams"), + Pair(20, "Master"), + Pair(21, "Twisting"), + Pair(26, "I'm pulling"), + Pair(30, "Your mind") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 9) + } + + @Test + fun `insertion of left child triggering left-right rotation should balance large tree`() { + avlTree.insert(20, "H") + avlTree.insert(4, "C") + avlTree.insert(26, "J") + avlTree.insert(3, "B") + avlTree.insert(9, "F") + avlTree.insert(21, "I") + avlTree.insert(30, "K") + avlTree.insert(2, "A") + avlTree.insert(7, "E") + avlTree.insert(11, "G") + + avlTree.insert(6, "D") + + val expectedStructure = + listOf( + Pair(2, "A"), + Pair(3, "B"), + Pair(4, "C"), + Pair(6, "D"), + Pair(7, "E"), + Pair(9, "F"), + Pair(11, "G"), + Pair(20, "H"), + Pair(21, "I"), + Pair(26, "J"), + Pair(30, "K") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 9) + } + + @Test + fun `insertion of right child triggering right-left rotation should balance large tree`() { + avlTree.insert(20, "D") + avlTree.insert(10, "B") + avlTree.insert(30, "I") + avlTree.insert(3, "A") + avlTree.insert(15, "C") + avlTree.insert(25, "F") + avlTree.insert(35, "J") + avlTree.insert(22, "E") + avlTree.insert(27, "G") + avlTree.insert(40, "K") + + avlTree.insert(28, "H") + + val expectedStructure = + listOf( + Pair(3, "A"), + Pair(10, "B"), + Pair(15, "C"), + Pair(20, "D"), + Pair(22, "E"), + Pair(25, "F"), + Pair(27, "G"), + Pair(28, "H"), + Pair(30, "I"), + Pair(35, "J"), + Pair(40, "K") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 25) + } + + @Test + fun `insertion of left child triggering right-left rotation should balance large tree`() { + avlTree.insert(20, "D") + avlTree.insert(10, "B") + avlTree.insert(30, "I") + avlTree.insert(3, "A") + avlTree.insert(15, "C") + avlTree.insert(25, "F") + avlTree.insert(35, "J") + avlTree.insert(22, "E") + avlTree.insert(27, "H") + avlTree.insert(40, "K") + + avlTree.insert(26, "G") + + val expectedStructure = + listOf( + Pair(3, "A"), + Pair(10, "B"), + Pair(15, "C"), + Pair(20, "D"), + Pair(22, "E"), + Pair(25, "F"), + Pair(26, "G"), + Pair(27, "H"), + Pair(30, "I"), + Pair(35, "J"), + Pair(40, "K") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 25) + } + } + + @Nested + inner class DeleteTests { + // Common tests + + @Test + fun `if key doesn't exist deletion should not change tree and return null`() { + avlTree.insert(10, "A") + avlTree.insert(20, "B") + avlTree.insert(30, "C") + + val expectedReturn = null + val actualReturn = avlTree.delete(40) + + val expectedStructure = listOf(Pair(10, "A"), Pair(20, "B"), Pair(30, "C")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedReturn, actualReturn, "Returned wrong value") + assertEquals(expectedStructure, actualStructure, "The tree changed after no deletion") + assertEquals(avlTree.root?.key, 20) + } + + @Test + fun `if key exists deletion should return associated value and delete node`() { + avlTree.insert(10, "A") + avlTree.insert(20, "B") + avlTree.insert(30, "C") + + val expectedReturn = "A" + val actualReturn = avlTree.delete(10) + + val expectedStructure = listOf(Pair(20, "B"), Pair(30, "C")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedReturn, actualReturn, "Returned wrong value") + assertEquals( + expectedStructure, + actualStructure, + "The tree has incorrect structure after deletion" + ) + assertEquals(avlTree.root?.key, 20) + } + + @Test + fun `deletion of last node should make tree empty`() { + avlTree.insert(1, "A") + avlTree.delete(1) + + val expectedStructure = listOf>() + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(avlTree.isEmpty(), true) + assertEquals(expectedStructure, actualStructure) + } + + @Test + fun `deletion of node with one child should replace it with its child`() { + avlTree.insert(20, "B") + avlTree.insert(10, "A") + avlTree.insert(30, "C") + avlTree.insert(40, "D") + + avlTree.delete(30) + + val expectedStructure = listOf(Pair(10, "A"), Pair(20, "B"), Pair(40, "D")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 20) + } + + // Small tree tests + + @Test + fun `deletion of root node should replace it with its successor`() { + avlTree.insert(10, "A") + avlTree.insert(20, "B") + avlTree.insert(30, "C") + + avlTree.delete(20) + + val expectedStructure = listOf(Pair(10, "A"), Pair(30, "C")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 30) + } + + @Test + fun `deletion of right child triggering right rotation should balance small tree`() { + avlTree.insert(40, "D") + avlTree.insert(30, "C") + avlTree.insert(20, "B") + avlTree.insert(10, "A") + + avlTree.delete(40) + + val expectedStructure = listOf(Pair(10, "A"), Pair(20, "B"), Pair(30, "C")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 20) + } + + @Test + fun `deletion of left child triggering left rotation should balance small tree`() { + avlTree.insert(10, "A") + avlTree.insert(20, "B") + avlTree.insert(30, "C") + avlTree.insert(40, "D") + + avlTree.delete(10) + + val expectedStructure = listOf(Pair(20, "B"), Pair(30, "C"), Pair(40, "D")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 30) + } + + @Test + fun `deletion of right child triggering left-right rotation should balance small tree`() { + avlTree.insert(30, "C") + avlTree.insert(10, "A") + avlTree.insert(40, "D") + avlTree.insert(20, "B") + + avlTree.delete(40) + + val expectedStructure = listOf(Pair(10, "A"), Pair(20, "B"), Pair(30, "C")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 20) + } + + @Test + fun `deletion of left child triggering right-left rotation should balance small tree`() { + avlTree.insert(20, "B") + avlTree.insert(10, "A") + avlTree.insert(40, "D") + avlTree.insert(30, "C") + + avlTree.delete(10) + + val expectedStructure = listOf(Pair(20, "B"), Pair(30, "C"), Pair(40, "D")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 30) + } + + @Test + fun `deletion of node with two children should replace it with its successor in small tree`() { + avlTree.insert(20, "B") + avlTree.insert(10, "A") + avlTree.insert(30, "C") + avlTree.insert(40, "D") + + avlTree.delete(20) + + val expectedStructure = listOf(Pair(10, "A"), Pair(30, "C"), Pair(40, "D")) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 30) + } + + // Medium tree tests + + @Test + fun `deletion of right child triggering right rotation should balance medium tree`() { + avlTree.insert(50, "E") + avlTree.insert(30, "C") + avlTree.insert(60, "F") + avlTree.insert(20, "B") + avlTree.insert(40, "D") + avlTree.insert(70, "G") + avlTree.insert(10, "A") + + avlTree.delete(70) + + val expectedStructure = + listOf( + Pair(10, "A"), + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(50, "E"), + Pair(60, "F") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 30) + } + + @Test + fun `deletion of left child triggering left rotation should balance medium tree`() { + avlTree.insert(30, "C") + avlTree.insert(20, "B") + avlTree.insert(50, "E") + avlTree.insert(10, "A") + avlTree.insert(40, "D") + avlTree.insert(60, "F") + avlTree.insert(70, "G") + + avlTree.delete(10) + + val expectedStructure = + listOf( + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(50, "E"), + Pair(60, "F"), + Pair(70, "G") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 50) + } + + @Test + fun `deletion of node with two children should replace it with its successor in medium tree`() { + avlTree.insert(30, "C") + avlTree.insert(20, "B") + avlTree.insert(50, "E") + avlTree.insert(10, "A") + avlTree.insert(40, "D") + avlTree.insert(60, "F") + avlTree.insert(70, "G") + + avlTree.delete(50) + + val expectedStructure = + listOf( + Pair(10, "A"), + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(60, "F"), + Pair(70, "G") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 30) + } + + // Large tree tests + + @Test + fun `deletion of right child triggering right rotation should balance large tree`() { + avlTree.insert(80, "H") + avlTree.insert(50, "E") + avlTree.insert(120, "L") + avlTree.insert(30, "C") + avlTree.insert(60, "F") + avlTree.insert(100, "J") + avlTree.insert(130, "M") + avlTree.insert(20, "B") + avlTree.insert(40, "D") + avlTree.insert(70, "G") + avlTree.insert(90, "I") + avlTree.insert(110, "K") + avlTree.insert(10, "A") + + avlTree.delete(130) + + val expectedStructure = + listOf( + Pair(10, "A"), + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(50, "E"), + Pair(60, "F"), + Pair(70, "G"), + Pair(80, "H"), + Pair(90, "I"), + Pair(100, "J"), + Pair(110, "K"), + Pair(120, "L") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 80) + } + + @Test + fun `deletion of left child triggering left rotation should balance large tree`() { + avlTree.insert(60, "F") + avlTree.insert(20, "B") + avlTree.insert(90, "I") + avlTree.insert(10, "A") + avlTree.insert(40, "D") + avlTree.insert(80, "H") + avlTree.insert(110, "K") + avlTree.insert(30, "C") + avlTree.insert(50, "E") + avlTree.insert(70, "G") + avlTree.insert(100, "J") + avlTree.insert(120, "L") + avlTree.insert(130, "M") + + avlTree.delete(10) + + val expectedStructure = + listOf( + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(50, "E"), + Pair(60, "F"), + Pair(70, "G"), + Pair(80, "H"), + Pair(90, "I"), + Pair(100, "J"), + Pair(110, "K"), + Pair(120, "L"), + Pair(130, "M") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 60) + } + + @Test + fun `deletion of right child triggering double right rotation should balance large tree`() { + avlTree.insert(80, "H") + avlTree.insert(50, "E") + avlTree.insert(110, "K") + avlTree.insert(30, "C") + avlTree.insert(60, "F") + avlTree.insert(100, "J") + avlTree.insert(120, "L") + avlTree.insert(20, "B") + avlTree.insert(40, "D") + avlTree.insert(70, "G") + avlTree.insert(90, "I") + avlTree.insert(10, "A") + + avlTree.delete(120) + + val expectedStructure = + listOf( + Pair(10, "A"), + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(50, "E"), + Pair(60, "F"), + Pair(70, "G"), + Pair(80, "H"), + Pair(90, "I"), + Pair(100, "J"), + Pair(110, "K") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 50) + } + + @Test + fun `deletion of left child triggering double left rotation should balance large tree`() { + avlTree.insert(50, "E") + avlTree.insert(20, "B") + avlTree.insert(80, "H") + avlTree.insert(10, "A") + avlTree.insert(30, "C") + avlTree.insert(70, "G") + avlTree.insert(100, "J") + avlTree.insert(40, "D") + avlTree.insert(60, "F") + avlTree.insert(90, "I") + avlTree.insert(110, "K") + avlTree.insert(120, "L") + + avlTree.delete(10) + + val expectedStructure = + listOf( + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(50, "E"), + Pair(60, "F"), + Pair(70, "G"), + Pair(80, "H"), + Pair(90, "I"), + Pair(100, "J"), + Pair(110, "K"), + Pair(120, "L") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 80) + } + + @Test + fun `deletion of node with two children should replace it with its successor in large tree`() { + avlTree.insert(80, "H") + avlTree.insert(50, "E") + avlTree.insert(120, "L") + avlTree.insert(30, "C") + avlTree.insert(60, "F") + avlTree.insert(100, "J") + avlTree.insert(130, "M") + avlTree.insert(20, "B") + avlTree.insert(40, "D") + avlTree.insert(70, "G") + avlTree.insert(90, "I") + avlTree.insert(110, "K") + avlTree.insert(10, "A") + + avlTree.delete(80) + + val expectedStructure = + listOf( + Pair(10, "A"), + Pair(20, "B"), + Pair(30, "C"), + Pair(40, "D"), + Pair(50, "E"), + Pair(60, "F"), + Pair(70, "G"), + Pair(90, "I"), + Pair(100, "J"), + Pair(110, "K"), + Pair(120, "L"), + Pair(130, "M") + ) + + val actualStructure = mutableListOf>() + + for (pair in avlTree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + assertEquals(avlTree.root?.key, 90) + } + } +} diff --git a/lib/src/test/kotlin/tsl/trees/AbstractBinaryTreeTest.kt b/lib/src/test/kotlin/tsl/trees/AbstractBinaryTreeTest.kt new file mode 100644 index 0000000..a51af98 --- /dev/null +++ b/lib/src/test/kotlin/tsl/trees/AbstractBinaryTreeTest.kt @@ -0,0 +1,365 @@ +package tsl.trees + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +/** + * This class tests methods implemented in the AbstractTreeClass that are common for all its + * inheritors (BSTree, AVLTree, RBTree). BSTree is used in tests as an instance of the + * AbstractTreeClass. + */ +class AbstractBinaryTreeTest { + private lateinit var tree: BSTree + + @BeforeEach + fun setup() { + tree = BSTree() + tree.insert(30, "Danya") + tree.insert(10, "Misha") + tree.insert(50, "Karim") + tree.insert(20, "Moldova") + tree.insert(40, "Tatarstan") + } + + @Nested + inner class SearchTests { + + @Test + fun `search of non-existing key should return null`() { + val expectedValue = null + val actualValue = tree.search(100) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `search of existing key should return associated value`() { + val expectedValue = "Danya" + val actualValue = tree.search(30) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `search of existing key that is to the left of the root node should return the corresponding value`() { + val expectedValue = "Moldova" + val actualValue = tree.search(20) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `search of existing key that is to the right of the root node should return the corresponding value`() { + val expectedValue = "Tatarstan" + val actualValue = tree.search(40) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `empty tree should not change after search`() { + tree.clear() + tree.search(1) + + val expectedRoot = null + val actualRoot = tree.root + + assertEquals(expectedRoot, actualRoot) + } + + @Test + fun `non-empty tree should not change after search`() { + tree.search(40) + + val expectedStructure = + listOf( + Pair(10, "Misha"), + Pair(20, "Moldova"), + Pair(30, "Danya"), + Pair(40, "Tatarstan"), + Pair(50, "Karim") + ) + + val actualStructure = mutableListOf>() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + } + + @Nested + inner class ClearTests { + + @Test + fun `empty tree should be empty after clear`() { + val emptyTree = BSTree() + emptyTree.clear() + + val expectedRoot = null + val actualRoot = emptyTree.root + + assertEquals(expectedRoot, actualRoot) + } + + @Test + fun `not empty tree should be empty after clear`() { + tree.clear() + + val expectedRoot = null + val actualRoot = tree.root + + assertEquals(expectedRoot, actualRoot) + } + } + + @Nested + inner class IsEmptyTests { + + @Test + fun `isEmpty should return true if the tree is empty`() { + tree.clear() + + val expectedValue = true + val actualValue = tree.isEmpty() + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `isEmpty should return false if the tree is not empty`() { + val expectedValue = false + val actualValue = tree.isEmpty() + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `isEmpty should not change an empty tree`() { + tree.clear() + tree.isEmpty() + + val expectedRoot = null + val actualRoot = tree.root + + assertEquals(expectedRoot, actualRoot) + } + + @Test + fun `isEmpty should not change a non-empty tree`() { + tree.isEmpty() + + val expectedStructure = + listOf( + Pair(10, "Misha"), + Pair(20, "Moldova"), + Pair(30, "Danya"), + Pair(40, "Tatarstan"), + Pair(50, "Karim") + ) + + val actualStructure = mutableListOf>() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + } + + @Nested + inner class GetMinKeyTests { + + @Test + fun `getMinKey should return null if the tree is empty`() { + tree.clear() + + val expectedKey = null + val actualKey = tree.getMinKey() + + assertEquals(expectedKey, actualKey) + } + + @Test + fun `getMinKey should return min key if the tree is not empty`() { + val expectedKey = 10 + val actualKey = tree.getMinKey() + + assertEquals(expectedKey, actualKey) + } + + @Test + fun `getMinKey should not change an empty tree`() { + tree.clear() + tree.getMinKey() + + val expectedRoot = null + val actualRoot = tree.root + + assertEquals(expectedRoot, actualRoot) + } + + @Test + fun `getMinKey should not change a non-empty tree`() { + tree.getMinKey() + + val expectedStructure = + listOf( + Pair(10, "Misha"), + Pair(20, "Moldova"), + Pair(30, "Danya"), + Pair(40, "Tatarstan"), + Pair(50, "Karim") + ) + + val actualStructure = mutableListOf>() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + } + + @Nested + inner class GetMaxKeyTests { + + @Test + fun `getMaxKey should return null if the tree is empty`() { + tree.clear() + + val expectedKey = null + val actualKey = tree.getMaxKey() + + assertEquals(expectedKey, actualKey) + } + + @Test + fun `getMaxKey should return max key if the tree is not empty`() { + val expectedKey = 50 + val actualKey = tree.getMaxKey() + + assertEquals(expectedKey, actualKey) + } + + @Test + fun `getMaxKey should not change an empty tree`() { + tree.clear() + tree.getMaxKey() + + val expectedRoot = null + val actualRoot = tree.root + + assertEquals(expectedRoot, actualRoot) + } + + @Test + fun `getMaxKey should not change a non-empty tree`() { + tree.getMaxKey() + + val expectedStructure = + listOf( + Pair(10, "Misha"), + Pair(20, "Moldova"), + Pair(30, "Danya"), + Pair(40, "Tatarstan"), + Pair(50, "Karim") + ) + + val actualStructure = mutableListOf>() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + } + + @Nested + inner class IteratorTests { + + @Test + fun `hasNext should return false right after it traversed all nodes in a non-empty tree`() { + val treeIterator = tree.iterator() + + var count = 0 + while (treeIterator.hasNext()) { + treeIterator.next() + count++ + } + + val expectedQuantity = 5 + val actualQuantity = count + + assertEquals(expectedQuantity, actualQuantity) + } + + @Test + fun `hasNext should always return false in an empty tree`() { + tree.clear() + val treeIterator = tree.iterator() + + var count = 0 + while (treeIterator.hasNext()) { + treeIterator.next() + count++ + } + + val expectedQuantity = 0 + val actualQuantity = count + + assertEquals(expectedQuantity, actualQuantity) + } + + @Test + fun `next method should return pairs with bigger keys than it has returned before`() { + val treeIterator = tree.iterator() + + var everyNextKeyIsBigger = true + + var currentKey = treeIterator.next().first + var previousKey = currentKey + var iteration = 0 + while (treeIterator.hasNext()) { + iteration++ + currentKey = treeIterator.next().first + if (currentKey <= previousKey) { + everyNextKeyIsBigger = false + break + } + previousKey = currentKey + } + + assertEquals(true, everyNextKeyIsBigger) + } + + @Test + fun `iterator should not change an empty tree`() { + tree.clear() + val treeIterator = tree.iterator() + + while (treeIterator.hasNext()) treeIterator.next() + + val expectedRoot = null + val actualRoot = tree.root + + assertEquals(expectedRoot, actualRoot) + } + + @Test + fun `iterator should not change a not empty tree`() { + val treeIterator = tree.iterator() + + while (treeIterator.hasNext()) treeIterator.next() + + val expectedStructure = + listOf( + Pair(10, "Misha"), + Pair(20, "Moldova"), + Pair(30, "Danya"), + Pair(40, "Tatarstan"), + Pair(50, "Karim") + ) + + val actualStructure = mutableListOf>() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + } +} diff --git a/lib/src/test/kotlin/tsl/trees/BSTreeTest.kt b/lib/src/test/kotlin/tsl/trees/BSTreeTest.kt new file mode 100644 index 0000000..c3838bb --- /dev/null +++ b/lib/src/test/kotlin/tsl/trees/BSTreeTest.kt @@ -0,0 +1,192 @@ +package tsl.trees + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class BSTreeTest { + private lateinit var tree: BSTree + + @BeforeEach + fun setup() { + tree = BSTree() + tree.insert(30, "java") + tree.insert(40, "gnomik") + tree.insert(20, "kotlin") + tree.insert(10, "kotik") + } + + @Nested + inner class InsertTests { + + @Test + fun `insert of the key that isn't in the tree should return null`() { + val expectedValue = null + val actualValue = tree.insert(50, "pesik") + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `insert of existing key should replace old value and return it`() { + val expectedReturnValue = "kotik" + val actualReturnValue = tree.insert(10, "pesik") + + val expectedNewValue = "pesik" + val actualNewValue = tree.search(10) + + assertEquals(expectedReturnValue, actualReturnValue, "wrong value returned") + assertEquals(expectedNewValue, actualNewValue, "value didn't change") + } + + @Test + fun `node with the smallest key should be inserted at the most left position`() { + tree.insert(5, "pesik") + + val expectedStructure = + listOf( + Pair(5, "pesik"), + Pair(10, "kotik"), + Pair(20, "kotlin"), + Pair(30, "java"), + Pair(40, "gnomik") + ) + + val actualStructure: MutableList> = mutableListOf() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + + @Test + fun `node with the biggest key should be inserted at the most right position`() { + tree.insert(50, "pesik") + + val expectedStructure = + listOf( + Pair(10, "kotik"), + Pair(20, "kotlin"), + Pair(30, "java"), + Pair(40, "gnomik"), + Pair(50, "pesik") + ) + + val actualStructure: MutableList> = mutableListOf() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + + @Test + fun `node with random key should be inserted at correct position`() { + tree.insert(25, "pesik") + + val expectedStructure = + listOf( + Pair(10, "kotik"), + Pair(20, "kotlin"), + Pair(25, "pesik"), + Pair(30, "java"), + Pair(40, "gnomik") + ) + + val actualStructure: MutableList> = mutableListOf() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + } + + @Nested + inner class DeleteTests { + + @Test + fun `delete of the non-existing key should return null`() { + val expectedValue = null + val actualValue = tree.delete(42069) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `delete of existing key should return the corresponding deleted value`() { + val expectedValue = "gnomik" + val actualValue = tree.delete(40) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `a key should be deleted after deletion`() { + tree.delete(30) + + val expectedValue = null + val actualValue = tree.search(30) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `delete of a node without children shouldn't change tree structure`() { + tree.delete(10) + + val expectedStructure = listOf(Pair(20, "kotlin"), Pair(30, "java"), Pair(40, "gnomik")) + val actualStructure: MutableList> = mutableListOf() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + + @Test + fun `delete of a node with a left child should replace it with it's child`() { + tree.delete(20) + + val expectedStructure = listOf(Pair(10, "kotik"), Pair(30, "java"), Pair(40, "gnomik")) + val actualStructure: MutableList> = mutableListOf() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + + @Test + fun `delete of a node with a right child should replace it with it's child`() { + tree.insert(50, "pesik") + tree.delete(40) + + val expectedStructure = + listOf(Pair(10, "kotik"), Pair(20, "kotlin"), Pair(30, "java"), Pair(50, "pesik")) + + val actualStructure: MutableList> = mutableListOf() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + + @Test + fun `delete of a node with two children should replace it with smallest node in the right subtree`() { + tree.insert(35, "pesik") + tree.delete(30) + + val expectedStructure = + listOf(Pair(10, "kotik"), Pair(20, "kotlin"), Pair(35, "pesik"), Pair(40, "gnomik")) + + val actualStructure: MutableList> = mutableListOf() + for (pair in tree) actualStructure.add(pair) + + assertEquals(expectedStructure, actualStructure) + } + + @Test + fun `deletion of last node should make tree empty`() { + tree.clear() + tree.insert(1, "A") + tree.delete(1) + + val expectedRoot = null + val actualRoot = tree.root + + assertEquals(expectedRoot, actualRoot) + } + } +} diff --git a/lib/src/test/kotlin/tsl/trees/RBTreeTest.kt b/lib/src/test/kotlin/tsl/trees/RBTreeTest.kt new file mode 100644 index 0000000..6f40fec --- /dev/null +++ b/lib/src/test/kotlin/tsl/trees/RBTreeTest.kt @@ -0,0 +1,583 @@ +package tsl.trees + +import kotlin.random.Random +import kotlin.test.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import tsl.nodes.RBNode + +/* + * Some standarts and "variables" that were used for decreasing the amount of words) + * S - sibling of deleting node + * V - deleting node + * U - node that will take place of deleting one + */ + +class RBTreeTest { + private lateinit var rbTree: RBTree + private lateinit var inorderedTraversalTree: MutableList> + + @BeforeEach + fun setup() { + rbTree = RBTree() + inorderedTraversalTree = mutableListOf() + } + + @Nested + inner class InsertTests { + + @Test + fun `insert if the node with this key doesn't exist`() { + rbTree.insert(15, "i") + rbTree.insert(10, "am") + rbTree.insert(5, "really") + val returnValue = rbTree.insert(7, "wordless") + assertEquals(null, returnValue) + } + + @Test + fun `insert if the node with this key exists`() { + rbTree.insert(15, "i") + rbTree.insert(10, "am") + rbTree.insert(5, "really") + val returnValue = rbTree.insert(5, "wordless") + assertEquals("really", returnValue) + } + + @Test + fun `insert left child`() { + rbTree.insert(17, "legend") + rbTree.insert(27, "club") + rbTree.insert(49, "poliklinika") + val returnValue = rbTree.insert(16, "18+") + assertEquals(null, returnValue) + assertEquals(16, rbTree.root?.leftChild?.leftChild?.key) + } + + @Test + fun `insert right child`() { + rbTree.insert(17, "legend") + rbTree.insert(27, "club") + rbTree.insert(49, "poliklinika") + val returnValue = rbTree.insert(20, "wordless") + assertEquals(null, returnValue) + assertEquals(20, rbTree.root?.leftChild?.rightChild?.key) + } + + @Test + fun `balance negative numbers`() { + val rbTree = RBTree() + rbTree.insert(-1, "a") + rbTree.insert(1, "a") + rbTree.insert(0, "a") + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals(listOf(Pair(-1, "a"), Pair(0, "a"), Pair(1, "a")), inorderedTraversalTree) + } + } + + @Nested + inner class DeleteTests { + + @Test + fun `successful delete (return value check)`() { + rbTree.insert(30, "pink") + rbTree.insert(20, "white") + rbTree.insert(40, "red") + + val returnValue = rbTree.delete(40) + assertEquals("red", returnValue) + } + + @Test + fun `unsuccessful delete (return value check)`() { + rbTree.insert(30, "pink") + rbTree.insert(20, "white") + rbTree.insert(40, "red") + + val returnValue = rbTree.delete(50) + assertEquals(null, returnValue) + } + + @Test + fun `delete if sibling and its left-child are black`() { + rbTree.insert(30, "pink") + rbTree.insert(20, "white") + rbTree.insert(40, "red") + rbTree.insert(50, "yellow") + + val returnValue = rbTree.delete(20) + assertEquals("white", returnValue) + } + + @Test + fun `delete right-right case`() { + // s is right child of its parent and both children of s are red + rbTree.insert(30, "pink") + rbTree.insert(18, "white") + rbTree.insert(40, "red") + rbTree.insert(45, "yellow") + + val returnValue = rbTree.delete(40) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals("red", returnValue) + assertEquals( + listOf(Pair(18, "white"), Pair(30, "pink"), Pair(45, "yellow")), + inorderedTraversalTree + ) + } + + @Test + fun `delete left-left case`() { + // s is left child of its parent and both children of s are red + rbTree.insert(30, "pink") + rbTree.insert(40, "white") + rbTree.insert(18, "red") + rbTree.insert(10, "yellow") + + val returnValue = rbTree.delete(18) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(10, "yellow"), Pair(30, "pink"), Pair(40, "white")), + inorderedTraversalTree + ) + assertEquals("red", returnValue) + } + + @Test + fun `delete right-left case`() { + // s is right child of its parent and r is left child of s + rbTree.insert(30, "pink") + rbTree.insert(40, "white") + rbTree.insert(18, "red") + rbTree.insert(35, "yellow") + + val returnValue = rbTree.delete(40) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(18, "red"), Pair(30, "pink"), Pair(35, "yellow")), + inorderedTraversalTree + ) + assertEquals("white", returnValue) + } + + @Test + fun `delete left-right case`() { + // s is left child of its parent and r is right child + rbTree.insert(30, "pink") + rbTree.insert(40, "white") + rbTree.insert(18, "red") + rbTree.insert(20, "yellow") + + val returnValue = rbTree.delete(18) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(20, "yellow"), Pair(30, "pink"), Pair(40, "white")), + inorderedTraversalTree + ) + assertEquals("red", returnValue) + } + + @Test + fun `delete left-red sibling`() { + rbTree.insert(20, "pink") + rbTree.insert(30, "white") + rbTree.insert(10, "red") + rbTree.insert(25, "brown") + rbTree.insert(35, "gold") + + val returnValue = rbTree.delete(10) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(20, "pink"), Pair(25, "brown"), Pair(30, "white"), Pair(35, "gold")), + inorderedTraversalTree + ) + assertEquals("red", returnValue) + } + + @Test + fun `delete root - sole node in tree`() { + rbTree.insert(10, "hihi haha") + val returnValue = rbTree.delete(10) + assertEquals("hihi haha", returnValue) + } + + @Test + fun `delete node with two children`() { + rbTree.insert(20, "pink") + rbTree.insert(30, "white") + rbTree.insert(10, "red") + rbTree.insert(25, "brown") + rbTree.insert(35, "gold") + + val returnValue = rbTree.delete(30) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(10, "red"), Pair(20, "pink"), Pair(25, "brown"), Pair(35, "gold")), + inorderedTraversalTree + ) + assertEquals("white", returnValue) + } + + @Test + fun `delete lonely black node with red sibling`() { + rbTree.insert(20, "hi") + rbTree.insert(10, "hihi") + rbTree.insert(40, "what") + rbTree.insert(30, "omg") + rbTree.insert(45, "damn") + rbTree.insert(33, "!") + + val returnValue = rbTree.delete(10) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf( + Pair(20, "hi"), + Pair(30, "omg"), + Pair(33, "!"), + Pair(40, "what"), + Pair(45, "damn") + ), + inorderedTraversalTree + ) + assertEquals("hihi", returnValue) + } + + @Test + fun `delete left-black node with red-right child`() { + rbTree.insert(100, "huh") + rbTree.insert(80, "?") + rbTree.insert(200, "you") + rbTree.insert(60, "meow") + rbTree.insert(90, ".") + rbTree.insert(88, "cat") + + val returnValue = rbTree.delete(60) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf( + Pair(80, "?"), + Pair(88, "cat"), + Pair(90, "."), + Pair(100, "huh"), + Pair(200, "you") + ), + inorderedTraversalTree + ) + assertEquals("meow", returnValue) + } + + @Test + fun `delete black node with zero children and black sibling`() { + rbTree.insert(110, "walking") + rbTree.insert(238, "in") + rbTree.insert(88, "of") + rbTree.insert(233, "phone") + + val returnValue = rbTree.delete(233) + rbTree.delete(88) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals(listOf(Pair(110, "walking"), Pair(238, "in")), inorderedTraversalTree) + assertEquals("phone", returnValue) + } + + @Test + fun `delete left-red node with no children`() { + val rbTree = RBTree() + rbTree.insert(6, "monkey") + rbTree.insert(4, "you") + rbTree.insert(7, "dog") + rbTree.insert(2, "cat") + rbTree.insert(5, "dog") + rbTree.insert(1, "cat") + + val returnValue = rbTree.delete(7) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf( + Pair(1, "cat"), + Pair(2, "cat"), + Pair(4, "you"), + Pair(5, "dog"), + Pair(6, "monkey") + ), + inorderedTraversalTree + ) + assertEquals("dog", returnValue) + } + + @Test + fun `delete black node with two children (left subtree & right leaf)`() { + val rbTree = RBTree() + rbTree.insert(200, "Im") + rbTree.insert(160, "gonna make") + rbTree.insert(400, "him an") + rbTree.insert(120, "offer") + rbTree.insert(180, "he") + rbTree.insert(130, "cant refuse") + + val returnValue = rbTree.delete(200) + val inorderedTraversalTree: MutableList> = mutableListOf() + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf( + Pair(120, "offer"), + Pair(130, "cant refuse"), + Pair(160, "gonna make"), + Pair(180, "he"), + Pair(400, "him an") + ), + inorderedTraversalTree + ) + assertEquals("Im", returnValue) + } + + @Test + fun `fix after deletion case 3 right child red`() { + rbTree.insert(15, "C") + rbTree.insert(10, "A") + rbTree.insert(20, "E") + rbTree.insert(22, "F") + + // for case 3 - insert sibling and ensure its child is red + rbTree.insert(23, "K") + + // deletion to trigger fixAfterDeletion with Case 3 conditions + rbTree.delete(10) + + val currentSibling = rbTree.root?.rightChild + assertEquals(null, currentSibling?.rightChild?.color) + assertEquals(RBNode.Color.Black, currentSibling?.color) + } + + @Test + fun `fix after deletion case 3 left child red`() { + rbTree.insert(15, "C") + rbTree.insert(10, "A") + rbTree.insert(20, "E") + rbTree.insert(18, "D") + + // for case 3 - insert sibling and ensure its child is red + rbTree.insert(17, "Z") + // deletion to trigger fixAfterDeletion with Case 3 conditions + rbTree.delete(10) + + val currentSibling = rbTree.root?.rightChild + assertEquals(RBNode.Color.Black, currentSibling?.color) + } + + @Test + fun `fix after deletion fourth case`() { + rbTree.insert(15, "c") + rbTree.insert(10, "a") + rbTree.insert(20, "e") + rbTree.insert(5, "b") + rbTree.insert(4, "f") + + val returnValue = rbTree.delete(4) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(5, "b"), Pair(10, "a"), Pair(15, "c"), Pair(20, "e")), + inorderedTraversalTree + ) + assertEquals("f", returnValue) + } + + @Test + fun `delete right-red sibling`() { + rbTree.insert(20, "pink") + rbTree.insert(30, "white") + rbTree.insert(15, "red") + rbTree.insert(10, "brown") + rbTree.insert(5, "gold") + + val returnValue = rbTree.delete(30) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(5, "gold"), Pair(10, "brown"), Pair(15, "red"), Pair(20, "pink")), + inorderedTraversalTree + ) + assertEquals("white", returnValue) + } + + @Test + fun `root left rotate`() { + rbTree.insert(20, "pink") + rbTree.insert(30, "white") + rbTree.insert(15, "red") + rbTree.insert(10, "brown") + rbTree.insert(5, "gold") + + val returnValue = rbTree.delete(20) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(5, "gold"), Pair(10, "brown"), Pair(15, "red"), Pair(30, "white")), + inorderedTraversalTree + ) + assertEquals("pink", returnValue) + } + + @Test + fun `root right rotate`() { + rbTree.insert(20, "pink") + rbTree.insert(30, "white") + rbTree.insert(15, "red") + rbTree.insert(10, "brown") + rbTree.insert(5, "gold") + + val returnValue = rbTree.delete(30) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals( + listOf(Pair(5, "gold"), Pair(10, "brown"), Pair(15, "red"), Pair(20, "pink")), + inorderedTraversalTree + ) + assertEquals("white", returnValue) + } + + @Test + fun `zero root deletion`() { + rbTree.insert(0, "a") + + val returnValue = rbTree.delete(0) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals(listOf(), inorderedTraversalTree) + assertEquals("a", returnValue) + } + + @Test + fun `duplicates test`() { + rbTree.insert(-1, "a") + rbTree.insert(1, "a") + rbTree.insert(0, "a") + rbTree.insert(0, "b") + + val returnValue = rbTree.delete(0) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals(listOf(Pair(-1, "a"), Pair(1, "a")), inorderedTraversalTree) + assertEquals("b", returnValue) + } + + @Test + fun `zero delete case`() { + rbTree.insert(-1, "a") + rbTree.insert(1, "a") + + val returnValue = rbTree.delete(0) + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals(listOf(Pair(-1, "a"), Pair(1, "a")), inorderedTraversalTree) + assertEquals(null, returnValue) + } + + @Test + fun `random tests`() { + val generator = Random(12) + + val randomKeys = mutableListOf() + for (i in 1..70) { + val randomValue = generator.nextInt() + randomKeys.add(randomValue) + rbTree.insert(randomValue, "") // this sets up key and default value + } + + val len = randomKeys.size + for (index in 1..len) { + val value: Int = randomKeys.removeLast() + rbTree.delete(value) + } + + for ((key, value) in rbTree) { + inorderedTraversalTree += Pair(key, value) + } + assertEquals(listOf(), inorderedTraversalTree) + } + } + + @Nested + inner class AuxiliaryTests { + @Test + fun `insert and balance, color check 1`() { + rbTree.insert(10, "A") + rbTree.insert(5, "B") + rbTree.insert(15, "C") + rbTree.insert(3, "D") + rbTree.insert(7, "E") + + assertEquals(RBNode.Color.Black, rbTree.root?.color) + assertEquals(RBNode.Color.Black, rbTree.root?.leftChild?.color) + assertEquals(RBNode.Color.Black, rbTree.root?.rightChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.leftChild?.leftChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.leftChild?.rightChild?.color) + } + + @Test + fun `insert and balance, color check 2`() { + rbTree.insert(10, "A") + rbTree.insert(5, "B") + rbTree.insert(15, "C") + rbTree.insert(3, "D") + rbTree.insert(7, "E") + rbTree.insert(13, "F") + rbTree.insert(17, "G") + rbTree.insert(2, "H") + rbTree.insert(4, "I") + rbTree.insert(6, "J") + rbTree.insert(8, "K") + rbTree.insert(14, "L") + rbTree.insert(16, "M") + rbTree.insert(18, "N") + + assertEquals(RBNode.Color.Black, rbTree.root?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.leftChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.rightChild?.color) + assertEquals(RBNode.Color.Black, rbTree.root?.leftChild?.leftChild?.color) + assertEquals(RBNode.Color.Black, rbTree.root?.leftChild?.rightChild?.color) + assertEquals(RBNode.Color.Black, rbTree.root?.rightChild?.leftChild?.color) + assertEquals(RBNode.Color.Black, rbTree.root?.rightChild?.rightChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.leftChild?.leftChild?.leftChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.leftChild?.leftChild?.rightChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.leftChild?.rightChild?.leftChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.leftChild?.rightChild?.rightChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.rightChild?.leftChild?.rightChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.rightChild?.rightChild?.leftChild?.color) + assertEquals(RBNode.Color.Red, rbTree.root?.rightChild?.rightChild?.rightChild?.color) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..fa97dad --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + plugins { + kotlin("jvm") version "1.9.23" + } +} +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.6/userguide/multi_project_builds.html in the Gradle documentation. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" +} + +rootProject.name = "TSL" +include("lib")