From 8772eb9ce59b95e49bd4aac706ca3d5a41d62854 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Thu, 23 Mar 2023 06:43:46 +0000 Subject: [PATCH 01/87] Setting up GitHub Classroom Feedback From 41eff228d10b646b76a1d5897ff726c4a301d43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9A=9C=EF=B8=8F=20GryaznovAS?= <113209592+GryaznovAS@users.noreply.github.com> Date: Wed, 29 Mar 2023 23:42:39 +0300 Subject: [PATCH 02/87] docs: Added license file. --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0532723 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 spbu-coding-2022 + +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. From c0550ad39d4c428fd2ee69912f5dda8869a6af8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9A=9C=EF=B8=8F=20GryaznovAS?= <113209592+GryaznovAS@users.noreply.github.com> Date: Thu, 30 Mar 2023 11:32:59 +0300 Subject: [PATCH 03/87] docs: Changed copyright contributors. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 0532723..513212c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 spbu-coding-2022 +Copyright (c) Arsene Baitenov, Artem Gryaznov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 3c3a3942e6bd988457d5b74bcd3581610644219b Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Mon, 3 Apr 2023 22:09:20 +0300 Subject: [PATCH 04/87] docs: Added README file. --- README.md | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..748cb72 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ + +
+
+ + Logo + + +

Search Trees Project

+ +

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

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

(Back to top)

+ + + +### Used Technologies + +Technologies used to develop the project: + +* [![gradle](https://img.shields.io/badge/gradle-FFFFFF?style=for-the-badge&logo=gradle&logoColor=black&)](https://gradle.org/) +* [![gradle](https://img.shields.io/badge/kotlin-FFFFFF?style=for-the-badge&logo=kotlin&logoColor=black&)](https://kotlinlang.org/) +* [![gradle](https://img.shields.io/badge/junit-FFFFFF?style=for-the-badge&logo=junit&logoColor=black&)](https://junit.org/) + +

(Back to top)

+ + + + +## Getting Started + +To start working with our development, you need to follow these steps: + +### Prerequisites + +Install JUnit - programmer-friendly testing framework for Java and the JVM. + +* npm + + ```sh + npm install junit + ``` + +### Clone Repository + +Clone repository to start local development. + +* git + + ```sh + git clone https://github.com/spbu-coding-2022/trees-12.git + ``` + + +## Roadmap + +- [x] Add LICENSE +- [x] Add README +- [ ] Add Gradle BM +- [ ] Develop Trees Interfaces +- [ ] Develop Trees Realisation +- [ ] Develop User Interface +- [ ] Develop CI + +

(Back to top)

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

(Back to top)

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

(Back to top)

+ + + + +## Acknowledgments + +The resources that we used to get information about binary search trees, their features and implementation possibilities: + +* [MIT License](https://mit-license.org) +* [Binary Search Tree](https://en.wikipedia.org/wiki/Search_tree) +* [AVL Search Tree](https://en.wikipedia.org/wiki/AVL_tree) +* [Red-Black Search Tree](https://en.wikipedia.org/wiki/Red–black_tree) +* [Gradle Documentation](https://docs.gradle.org/current/userguide/userguide.html) +* [JUnit Documentation](https://junit.org/junit5/docs/current/user-guide/) +* [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) + +

(Back to top)

From f0d0ee2d2e306dc4be172df8ecec3110e102f089 Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Tue, 4 Apr 2023 00:01:26 +0300 Subject: [PATCH 05/87] docs: Added CONTRIBUTING file. --- CONTRIBUTING.md | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7ec2dde --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,154 @@ + +
+
+ + Logo + + +

Search Trees Project

+ +

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

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

(Back to top)

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

(Back to top)

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

(Back to top)

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

(Back to top)

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

(Back to top)

From 5865654c622477a67a68ebc44d0eef1fa73b2b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9A=9C=EF=B8=8F=20GryaznovAS?= <113209592+GryaznovAS@users.noreply.github.com> Date: Wed, 5 Apr 2023 11:36:24 +0300 Subject: [PATCH 06/87] docs: Deleted JUnit-Installation block in README. --- README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/README.md b/README.md index 748cb72..d323773 100644 --- a/README.md +++ b/README.md @@ -79,21 +79,7 @@ Technologies used to develop the project: ## Getting Started -To start working with our development, you need to follow these steps: - -### Prerequisites - -Install JUnit - programmer-friendly testing framework for Java and the JVM. - -* npm - - ```sh - npm install junit - ``` - -### Clone Repository - -Clone repository to start local development. +To start working with our development, you need to clone repository: * git From 395f5be09ba05c224e90b290374b61d4927ed08b Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 4 Apr 2023 21:06:24 +0300 Subject: [PATCH 07/87] struct: Added .gitignore for filtering files. --- .gitignore | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ac1b4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Out Package # +/out/ + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.idea \ No newline at end of file From 993bd8cb540d32df168c5c8cf1e8c22705651392 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 4 Apr 2023 21:30:22 +0300 Subject: [PATCH 08/87] feat: Inited build tool Gradle. --- .gitattributes | 9 + BinarySearchTrees/build.gradle.kts | 37 ++++ app/build.gradle.kts | 43 ++++ app/src/main/kotlin/app/App.kt | 5 + gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 244 +++++++++++++++++++++++ gradlew.bat | 92 +++++++++ settings.gradle.kts | 2 + 8 files changed, 438 insertions(+) create mode 100644 .gitattributes create mode 100644 BinarySearchTrees/build.gradle.kts create mode 100644 app/build.gradle.kts create mode 100644 app/src/main/kotlin/app/App.kt create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts 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/BinarySearchTrees/build.gradle.kts b/BinarySearchTrees/build.gradle.kts new file mode 100644 index 0000000..11270ce --- /dev/null +++ b/BinarySearchTrees/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.10" + jacoco +} + +repositories { + mavenCentral() +} + +dependencies { + // Use the Kotlin JDK 8 standard library. + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testImplementation(platform("org.junit:junit-bom:5.9.2")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + finalizedBy("jacocoTestReport") + useJUnitPlatform() + maxHeapSize = "2G" + testLogging { + events("passed", "skipped", "failed") + } + reports.html.outputLocation.set(file("${buildDir}/reports/test")) +} + +tasks.named("jacocoTestReport") { + dependsOn(tasks.test) + reports { + xml.required.set(false) + html.required.set(true) + html.outputLocation.set(file("${buildDir}/reports/jacoco")) + csv.required.set(true) + csv.outputLocation.set(file("${buildDir}/jacoco/report.csv")) + } +} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..fc4f6de --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.10" + jacoco + application +} + +repositories { + mavenCentral() +} + +dependencies { + // Use the Kotlin JDK 8 standard library. + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + testImplementation(platform("org.junit:junit-bom:5.9.2")) + testImplementation("org.junit.jupiter:junit-jupiter") + implementation(project(":BinarySearchTrees")) +} + +tasks.test { + finalizedBy("jacocoTestReport") + useJUnitPlatform() + maxHeapSize = "2G" + testLogging { + events("passed", "skipped", "failed") + } + reports.html.outputLocation.set(file("${buildDir}/reports/test")) +} + +tasks.named("jacocoTestReport") { + dependsOn(tasks.test) + reports { + xml.required.set(false) + html.required.set(true) + html.outputLocation.set(file("${buildDir}/reports/jacoco")) + csv.required.set(true) + csv.outputLocation.set(file("${buildDir}/jacoco/report.csv")) + } +} + +application { + mainClass.set("app.AppKt") +} diff --git a/app/src/main/kotlin/app/App.kt b/app/src/main/kotlin/app/App.kt new file mode 100644 index 0000000..50d217a --- /dev/null +++ b/app/src/main/kotlin/app/App.kt @@ -0,0 +1,5 @@ +package app + +fun main() { + println("Hello World!") +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bdc9a83 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..79a61d4 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3428b26 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "trees-12" +include("app", "BinarySearchTrees") From a87b195b5677bde2cdac77406ac9c6a4d8e55ce3 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 4 Apr 2023 21:35:30 +0300 Subject: [PATCH 09/87] ci: Added script for GitHub Actions. --- .github/workflows/test.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8b4e9bd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Test with coverage +on: + push: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout project sources + uses: actions/checkout@v2 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-version: current + gradle-home-cache-cleanup: true + - name: Run test + run: | + ./gradlew clean + ./gradlew test + - name: JaCoCo Coverage Report + env: + report_path: BinarySearchTrees/build/jacoco/report.csv + run: | + awk -F"," '{ instructions += $4 + $5; covered += $5; branches += $6 + $7; branches_covered +=$7 } END { print "Instructions covered:", covered"/"instructions, "--", 100*covered/instructions"%"; print "Branches covered:", branches_covered"/"branches, "--", 100*branches_covered/branches"%" }' $report_path + - uses: actions/upload-artifact@v3 + with: + name: binarysearchtree-test-and-coverage-reports + path: | + BinarySearchTrees/build/reports \ No newline at end of file From 484844a3787c953a1549d202843877d00d399cd9 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 4 Apr 2023 21:37:56 +0300 Subject: [PATCH 10/87] feat: Added forgotten gradle-wrapper.jar --- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 61608 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..ccebba7710deaf9f98673a68957ea02138b60d0a GIT binary patch literal 61608 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 literal 0 HcmV?d00001 From 6f3764bf05804a0f58e44b06f13e6017240aa0ef Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 11 Apr 2023 20:55:55 +0300 Subject: [PATCH 11/87] feat: Added interface describing bst functions. --- .../binarysearchtrees/BinarySearchTree.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt new file mode 100644 index 0000000..1b0fae0 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt @@ -0,0 +1,34 @@ +package binarysearchtrees + +interface BinarySearchTree, V> { + val size: Int + + fun isEmpty(): Boolean + + fun clear() + + fun getRoot(): MutableVertex? + + operator fun get(key: K): V? + + fun put(key: K, value: V): V? + + fun remove(key: K): V? + + fun remove(key: K, value: V): Boolean + + //for extension functions like all, any, ... + interface Vertex { + val key: K + val value: V + val left: Vertex? + val right: Vertex? + } + + interface MutableVertex : Vertex { + override val left: MutableVertex? + override val right: MutableVertex? + + fun setValue(newValue: V): V + } +} \ No newline at end of file From 7ec522c8007dde06d20120ba450723c27f12101d Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 11 Apr 2023 20:56:43 +0300 Subject: [PATCH 12/87] feat: Added class that implements bst interface. --- .../AbstractBinarySearchTree.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt new file mode 100644 index 0000000..780fd25 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt @@ -0,0 +1,24 @@ +package binarysearchtrees + +abstract class AbstractBinarySearchTree, V> : BinarySearchTree { + override var size: Int = 0 + protected var root: Vertex? = null + + override fun isEmpty(): Boolean = (root == null) + + override fun clear() { + size = 0 + root = null + } + + override fun getRoot(): BinarySearchTree.MutableVertex? = root + + protected class Vertex( + override val key: K, + override var value: V, + override var left: Vertex? = null, + override var right: Vertex? = null + ) : BinarySearchTree.MutableVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } +} \ No newline at end of file From 3d96795da99477ae64e029a8d82e714529f9f449 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 11 Apr 2023 21:00:05 +0300 Subject: [PATCH 13/87] feat: Added search operation. --- .../binarysearchtrees/AbstractBinarySearchTree.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt index 780fd25..f797556 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt @@ -13,6 +13,18 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree override fun getRoot(): BinarySearchTree.MutableVertex? = root + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + protected class Vertex( override val key: K, override var value: V, From 47218439dcac61d8067fe633729eaec9a26dbf65 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 11 Apr 2023 21:03:05 +0300 Subject: [PATCH 14/87] feat: Added delete and add operations. --- .../AbstractBinarySearchTree.kt | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt index f797556..5d6511e 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt @@ -25,6 +25,102 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree return vertex?.value } + override fun put(key: K, value: V): V? { + var f = false + var vertex: Vertex = root ?: Vertex(key, value).also { + root = it + f = true + } + while (vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left ?: Vertex(key, value).also { + vertex.left = it + f = true + } + } else { + vertex = vertex.right ?: Vertex(key, value).also { + vertex.right = it + f = true + } + } + } + return if (f) null else vertex.setValue(value) + } + + override fun remove(key: K): V? { + var parent: Vertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + val oldValue = vertex?.value + if (parent == null) { + root = vertex?.let { removeVertex(it) } + } else { + if (parent.left == vertex) { + parent.left = vertex?.let { removeVertex(it) } + } else { + parent.right = vertex?.let { removeVertex(it) } + } + } + return oldValue + } + + override fun remove(key: K, value: V): Boolean { + var parent: Vertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return if (vertex?.value == value) { + if (parent == null) { + root = vertex?.let { removeVertex(it) } + } else { + if (parent.left == vertex) { + parent.left = vertex?.let { removeVertex(it) } + } else { + parent.right = vertex?.let { removeVertex(it) } + } + } + true + } else false + } + + private fun removeVertex(vertex: Vertex): Vertex? { + return vertex.left?.let { left -> + vertex.right?.let { right -> + //search of parent of Vertex with next key + var nextParent = vertex + var next = right // vertex.right + while (next.left != null) { + next = next.left?.let { + nextParent = next + it + } ?: next + } + if (nextParent == vertex) { + next.left = left // vertex.left + } else { + nextParent.left = null + next.left = left // vertex.left + next.right = right // vertex.right + } + + next + } ?: left + } ?: vertex.right + } + protected class Vertex( override val key: K, override var value: V, From 079c25f5c900b44b278325fa65d5074b8b39bdad Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 11 Apr 2023 23:13:16 +0300 Subject: [PATCH 15/87] feat: Added initializing and extension functions for BST. --- .../main/kotlin/binarysearchtrees/Trees.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt new file mode 100644 index 0000000..304561f --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt @@ -0,0 +1,38 @@ +package binarysearchtrees + +// initializing functions + +fun , V> binarySearchTreeOf(): BinarySearchTree { + return object : AbstractBinarySearchTree() {} +} + +fun , V> binarySearchTreeOf( + vararg args: Pair +): BinarySearchTree { + val tree = object : AbstractBinarySearchTree() {} + for (it in args) { + tree.put(it.first, it.second) + } + return tree +} + +// extension functions + +fun , V> BinarySearchTree.forEach(action: (BinarySearchTree.MutableVertex) -> Unit) { + fun traversalInOrder(vertex: BinarySearchTree.MutableVertex?) { + if (vertex == null) { + return + } else { + traversalInOrder(vertex.left) + action(vertex) + traversalInOrder(vertex.right) + } + } + traversalInOrder(this.getRoot()) +} + +fun , V> BinarySearchTree.toList(): List> { + val list = mutableListOf>() + this.forEach { list.add(Pair(it.key, it.value)) } + return list +} \ No newline at end of file From f97bb39272561ed7c5e880f44739b0e1f2e7531e Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 11 Apr 2023 23:16:33 +0300 Subject: [PATCH 16/87] test: Added tests for put function. --- .../AbstractBinarySearchTreeTest.kt | 33 +++++++++++++++++++ .../binarysearchtrees/InvariantCheckers.kt | 9 +++++ 2 files changed, 42 insertions(+) create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt new file mode 100644 index 0000000..520386d --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt @@ -0,0 +1,33 @@ +package binarysearchtrees + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.random.Random + +class AbstractBinarySearchTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: AbstractBinarySearchTree + + @BeforeEach + fun init() { + tree = object : AbstractBinarySearchTree() {} + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isBinarySearchTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, it.second) } + assertEquals(values.reversed().distinctBy { it.first }.sortedBy { it.first }, tree.toList()) + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt new file mode 100644 index 0000000..4bd0f8f --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt @@ -0,0 +1,9 @@ +package binarysearchtrees + +fun , V> isBinarySearchTree(tree: BinarySearchTree): Boolean { + fun checkBSTInvariant(vertex: BinarySearchTree.Vertex): Boolean { + return vertex.left?.let { vertex.key > it.key && checkBSTInvariant(it) } ?: true + && vertex.right?.let { vertex.key < it.key && checkBSTInvariant(it) } ?: true + } + return tree.getRoot()?.let { checkBSTInvariant(it) } ?: true +} \ No newline at end of file From 6a931798b9ad5d2595b98992378c72ff7c61ae2b Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sun, 16 Apr 2023 03:32:28 +0300 Subject: [PATCH 17/87] feat: Added iterator and set operation. --- .../AbstractBinarySearchTree.kt | 51 +++++++++++++++++++ .../binarysearchtrees/BinarySearchTree.kt | 4 ++ 2 files changed, 55 insertions(+) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt index 5d6511e..041cd39 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt @@ -3,16 +3,22 @@ package binarysearchtrees abstract class AbstractBinarySearchTree, V> : BinarySearchTree { override var size: Int = 0 protected var root: Vertex? = null + protected var modCount: Int = 0 override fun isEmpty(): Boolean = (root == null) override fun clear() { size = 0 root = null + ++modCount } override fun getRoot(): BinarySearchTree.MutableVertex? = root + override fun iterator(): Iterator> { + return BinarySearchTreeIterator(getRoot()) { modCount } + } + override fun get(key: K): V? { var vertex = root while (vertex != null && vertex.key != key) { @@ -44,9 +50,14 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree } } } + ++modCount return if (f) null else vertex.setValue(value) } + override fun set(key: K, value: V) { + put(key, value) + } + override fun remove(key: K): V? { var parent: Vertex? = null var vertex = root @@ -68,6 +79,7 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree parent.right = vertex?.let { removeVertex(it) } } } + ++modCount return oldValue } @@ -82,6 +94,7 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree vertex = vertex.right } } + ++modCount return if (vertex?.value == value) { if (parent == null) { root = vertex?.let { removeVertex(it) } @@ -129,4 +142,42 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree ) : BinarySearchTree.MutableVertex { override fun setValue(newValue: V): V = value.also { value = newValue } } + + protected class BinarySearchTreeIterator( + root: BinarySearchTree.MutableVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left + } + } + + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() + } + } + + override fun next(): BinarySearchTree.MutableVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex + } + } + } } \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt index 1b0fae0..da48761 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt @@ -13,10 +13,14 @@ interface BinarySearchTree, V> { fun put(key: K, value: V): V? + operator fun set(key: K, value: V) + fun remove(key: K): V? fun remove(key: K, value: V): Boolean + operator fun iterator(): Iterator> + //for extension functions like all, any, ... interface Vertex { val key: K From 2a06776c24f6e9c1acea5c028c4fdff7c7b09dfb Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sun, 16 Apr 2023 03:35:12 +0300 Subject: [PATCH 18/87] fix: Deleted extension functions for BST. --- .../main/kotlin/binarysearchtrees/Trees.kt | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt index 304561f..bbe755e 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt @@ -14,25 +14,4 @@ fun , V> binarySearchTreeOf( tree.put(it.first, it.second) } return tree -} - -// extension functions - -fun , V> BinarySearchTree.forEach(action: (BinarySearchTree.MutableVertex) -> Unit) { - fun traversalInOrder(vertex: BinarySearchTree.MutableVertex?) { - if (vertex == null) { - return - } else { - traversalInOrder(vertex.left) - action(vertex) - traversalInOrder(vertex.right) - } - } - traversalInOrder(this.getRoot()) -} - -fun , V> BinarySearchTree.toList(): List> { - val list = mutableListOf>() - this.forEach { list.add(Pair(it.key, it.value)) } - return list } \ No newline at end of file From 4572a678e205765f58070bd70fe9c0d099bff098 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sun, 16 Apr 2023 03:42:51 +0300 Subject: [PATCH 19/87] test: Changed test for put function. --- .../binarysearchtrees/AbstractBinarySearchTreeTest.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt index 520386d..b07a67e 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt @@ -28,6 +28,13 @@ class AbstractBinarySearchTreeTest { @Test fun `Function put adds all elements with unique keys`() { values.forEach { tree.put(it.first, it.second) } - assertEquals(values.reversed().distinctBy { it.first }.sortedBy { it.first }, tree.toList()) + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) } } \ No newline at end of file From ee8c6d7012a234614fb6986790d789c4933d2dff Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sun, 16 Apr 2023 03:47:59 +0300 Subject: [PATCH 20/87] test: Added test for iterator exceptions. --- .../AbstractBinarySearchTreeTest.kt | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt index b07a67e..f636bcc 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt @@ -1,9 +1,10 @@ package binarysearchtrees -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import kotlin.random.Random class AbstractBinarySearchTreeTest { @@ -37,4 +38,33 @@ class AbstractBinarySearchTreeTest { listOfPairKeyValue ) } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } } \ No newline at end of file From 914e6ceb0aa3d7402c135f05b124ef85b99aadf7 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sun, 16 Apr 2023 03:49:50 +0300 Subject: [PATCH 21/87] test: Added tests for get, remove and clear functions. --- .../AbstractBinarySearchTreeTest.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt index f636bcc..ed86ebe 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt @@ -67,4 +67,65 @@ class AbstractBinarySearchTreeTest { iterator = tree.iterator() assertThrows(NoSuchElementException::class.java) { iterator.next() } } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertTrue(isBinarySearchTree(tree)) + + value = key * 95 + tree[key] = value + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertTrue(isBinarySearchTree(tree)) + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertTrue(isBinarySearchTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertTrue(isBinarySearchTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertEquals(0, tree.size) + } } \ No newline at end of file From 6cc95a473b68261784f98e7d8128e7a98a7d44a0 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 23:32:29 +0300 Subject: [PATCH 22/87] fix: Added increment/decrement of size in put/remove functions, changed places of modCount increment. --- .../AbstractBinarySearchTree.kt | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt index 041cd39..aa7e749 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt @@ -50,8 +50,11 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree } } } - ++modCount - return if (f) null else vertex.setValue(value) + return if (f) { + ++size + ++modCount + null + } else vertex.setValue(value) } override fun set(key: K, value: V) { @@ -70,16 +73,19 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree } } val oldValue = vertex?.value - if (parent == null) { - root = vertex?.let { removeVertex(it) } - } else { - if (parent.left == vertex) { - parent.left = vertex?.let { removeVertex(it) } + if (vertex != null) { + if (parent == null) { + root = vertex.let { removeVertex(it) } } else { - parent.right = vertex?.let { removeVertex(it) } + if (parent.left == vertex) { + parent.left = vertex.let { removeVertex(it) } + } else { + parent.right = vertex.let { removeVertex(it) } + } } + --size + ++modCount } - ++modCount return oldValue } @@ -94,7 +100,6 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree vertex = vertex.right } } - ++modCount return if (vertex?.value == value) { if (parent == null) { root = vertex?.let { removeVertex(it) } @@ -105,6 +110,8 @@ abstract class AbstractBinarySearchTree, V> : BinarySearchTree parent.right = vertex?.let { removeVertex(it) } } } + --size + ++modCount true } else false } From af174f672d8986135df8e218414ce0e84d776638 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 23:33:48 +0300 Subject: [PATCH 23/87] test: Added size checks. --- .../AbstractBinarySearchTreeTest.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt index ed86ebe..dba1e4a 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt @@ -37,12 +37,14 @@ class AbstractBinarySearchTreeTest { values.reversed().distinctBy { it.first }.sortedBy { it.first }, listOfPairKeyValue ) + assertEquals(values.distinctBy { it.first }.size, tree.size) } @ParameterizedTest @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) fun `Functions of iterator throws exceptions after change tree`(key: Int) { values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) var iterator = tree.iterator() tree.remove(key) @@ -76,25 +78,33 @@ class AbstractBinarySearchTreeTest { val expected = key * 198 tree[key] = expected assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) } @ParameterizedTest @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) - fun `Function remove deletes the element correctly`(key: Int) { + fun `Function remove deletes the some element correctly`(key: Int) { values.forEach { tree.put(it.first, it.second) } var value = key * 198 tree[key] = value + var size = tree.size - 1 assertEquals(value, tree.remove(key)) assertEquals(null, tree.remove(key)) assertEquals(null, tree[key]) + assertEquals(size, tree.size) assertTrue(isBinarySearchTree(tree)) value = key * 95 tree[key] = value + size = tree.size - 1 assertFalse(tree.remove(key, value + 10)) assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) assertEquals(null, tree[key]) + assertEquals(size, tree.size) assertTrue(isBinarySearchTree(tree)) } @@ -107,16 +117,20 @@ class AbstractBinarySearchTreeTest { it.setValue(value) it.key } ?: -25 + var size = tree.size - 1 assertEquals(value, tree.remove(oldKey)) assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) assertTrue(isBinarySearchTree(tree)) oldKey = tree.getRoot()?.let { it.setValue(value) it.key } ?: -25 + size = tree.size - 1 assertTrue(tree.remove(oldKey, value)) assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) assertTrue(isBinarySearchTree(tree)) } @@ -126,6 +140,7 @@ class AbstractBinarySearchTreeTest { tree.clear() assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) assertEquals(0, tree.size) } } \ No newline at end of file From ce87a5447bda22f14b88438126e13a6edae1c06d Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 23:36:55 +0300 Subject: [PATCH 24/87] test: Added another test for remove operation. Test checks removal every twentieth element. --- .../AbstractBinarySearchTreeTest.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt index dba1e4a..349ba80 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt @@ -108,6 +108,24 @@ class AbstractBinarySearchTreeTest { assertTrue(isBinarySearchTree(tree)) } + @Test + fun `Function remove deletes the existing element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val elements = mutableListOf>() + values.reversed().distinctBy { it.first }.forEach { elements.add(it) } + elements.shuffle() + for (i in 0 until elements.size step 20) { + val key = elements[i].first + val value = elements[i].second + val size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + } + @Test fun `Function remove deletes the root element correctly`() { values.forEach { tree.put(it.first, it.second) } From 6f793571ea8176e5ff67b5eff71ce5b0fea61abe Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 18:39:15 +0300 Subject: [PATCH 25/87] feat: Added interface describing rbt functions. --- .../kotlin/binarysearchtrees/RedBlackTree.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt new file mode 100644 index 0000000..efce916 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt @@ -0,0 +1,21 @@ +package binarysearchtrees + +interface RedBlackTree, V> : BinarySearchTree { + override fun getRoot(): MutableVertex? + + override operator fun iterator(): Iterator> + + //for extension functions like all, any, ... + interface Vertex : BinarySearchTree.Vertex { + val color: Color + override val left: Vertex? + override val right: Vertex? + + enum class Color { RED, BLACK } + } + + interface MutableVertex : BinarySearchTree.MutableVertex, Vertex { + override val left: MutableVertex? + override val right: MutableVertex? + } +} \ No newline at end of file From c0c9cbbff046a0cefa0458d51f8e180a03de1443 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 18:49:03 +0300 Subject: [PATCH 26/87] feat: Added abstract class that implements bst interface and iterator. --- .../binarysearchtrees/AbstractRedBlackTree.kt | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt new file mode 100644 index 0000000..5d55fec --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt @@ -0,0 +1,69 @@ +package binarysearchtrees + +abstract class AbstractRedBlackTree, V> : RedBlackTree { + override var size: Int = 0 + protected var root: Vertex? = null + protected var modCount: Int = 0 + + override fun isEmpty(): Boolean = (root == null) + + override fun clear() { + size = 0 + root = null + ++modCount + } + + override fun getRoot(): RedBlackTree.MutableVertex? = root + + override fun iterator(): Iterator> { + return RedBlackTreeIterator(getRoot()) { modCount } + } + + protected class Vertex( + override val key: K, + override var value: V, + override var color: RedBlackTree.Vertex.Color = RedBlackTree.Vertex.Color.RED, + override var left: Vertex? = null, + override var right: Vertex? = null + ) : RedBlackTree.MutableVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } + + protected class RedBlackTreeIterator( + root: RedBlackTree.MutableVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left + } + } + + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() + } + } + + override fun next(): RedBlackTree.MutableVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex + } + } + } +} \ No newline at end of file From 717a4dbc67b04dda194baab8b0d5fbb8ed15b9fe Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 18:59:28 +0300 Subject: [PATCH 27/87] feat: Added search(get), delete(remove) and add(set, put) operations. --- .../binarysearchtrees/AbstractRedBlackTree.kt | 505 ++++++++++++++++++ 1 file changed, 505 insertions(+) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt index 5d55fec..91987ea 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt @@ -19,6 +19,511 @@ abstract class AbstractRedBlackTree, V> : RedBlackTree { return RedBlackTreeIterator(getRoot()) { modCount } } + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + + override fun put(key: K, value: V): V? { + var new = false + val stack = mutableListOf>() + var vertex = root ?: Vertex(key, value, RedBlackTree.Vertex.Color.BLACK).also { + root = it + new = true + } + while (vertex.key != key) { + stack.add(vertex) + if (vertex.key > key) { + vertex = vertex.left ?: Vertex(key, value).also { + vertex.left = it + new = true + } + } else { + vertex = vertex.right ?: Vertex(key, value).also { + vertex.right = it + new = true + } + } + } + + // balance and return + return if (new) { + stack.add(vertex) + balanceAfterInsert(stack) + stack.clear() + ++size + ++modCount + null + } else { + stack.clear() + vertex.setValue(value) + } + } + + override fun set(key: K, value: V) { + put(key, value) + } + + override fun remove(key: K): V? { + val stack = mutableListOf>() + var parentIndex = -1 + var vertex = root + while (vertex != null && vertex.key != key) { + stack.add(vertex) + ++parentIndex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + val oldValue = vertex?.value + if (vertex != null) { + val goner = getGoner(vertex, parentIndex, stack) + + // leaf removal + stack.add(goner) + removeLeaf(stack) + --size + ++modCount + } + stack.clear() + return oldValue + } + + override fun remove(key: K, value: V): Boolean { + val stack = mutableListOf>() + var parentIndex = -1 + var vertex = root + while (vertex != null && vertex.key != key) { + stack.add(vertex) + ++parentIndex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return if (vertex != null && vertex.value == value) { + val goner = getGoner(vertex, parentIndex, stack) + + // leaf removal + stack.add(goner) + removeLeaf(stack) + --size + ++modCount + stack.clear() + true + } else { + stack.clear() + false + } + } + + private fun rotateLeft(parent: Vertex, child: Vertex): Vertex { + parent.color = child.color.also { child.color = parent.color } + parent.right = child.left + child.left = parent + return child + } + + private fun rotateRight(parent: Vertex, child: Vertex): Vertex { + parent.color = child.color.also { child.color = parent.color } + parent.left = child.right + child.right = parent + return child + } + + private fun balanceAfterInsert(stack: MutableList>) { + val black = RedBlackTree.Vertex.Color.BLACK + val red = RedBlackTree.Vertex.Color.RED + var notEnd = true + + var vertex = stack.removeLast() + while (notEnd && stack.lastOrNull()?.color == red) { + var parent = stack.removeLast() + val grandparent = stack.removeLast() + if (grandparent.left == parent) { + if (grandparent.right?.color == red) { + grandparent.color = red + grandparent.right?.color = black + parent.color = black + vertex = grandparent + } else { + notEnd = false + if (parent.right == vertex) { + parent = rotateLeft(parent, vertex) + grandparent.left = parent + } + if (root == grandparent) { + root = rotateRight(grandparent, parent) + } else { + val ggparent = stack.last() + if (ggparent.left == grandparent) { + ggparent.left = rotateRight(grandparent, parent) + } else { + ggparent.right = rotateRight(grandparent, parent) + } + } + } + } else { + if (grandparent.left?.color == red) { + grandparent.color = red + grandparent.left?.color = black + parent.color = black + vertex = grandparent + } else { + notEnd = false + if (parent.left == vertex) { + parent = rotateRight(parent, vertex) + grandparent.right = parent + } + if (root == grandparent) { + root = rotateLeft(grandparent, parent) + } else { + val ggparent = stack.last() + if (ggparent.left == grandparent) { + ggparent.left = rotateLeft(grandparent, parent) + } else { + ggparent.right = rotateLeft(grandparent, parent) + } + } + } + } + } + + root?.color = black + } + + private fun getGoner(vertex: Vertex, parentIndex: Int, stack: MutableList>): Vertex { + return vertex.left?.let { left -> + vertex.right?.let { right -> + // swap with the vertex that is next by key + var nextParent: Vertex = vertex + stack.add(nextParent) + var next = right + while (next.left != null) { + next = next.left?.let { + nextParent = next + stack.add(nextParent) + it + } ?: next + } + vertex.color = next.color.also { next.color = vertex.color } + if (root == vertex) { + root = next + } else { + val parent = stack[parentIndex] + if (parent.left == vertex) { + parent.left = next + } else { + parent.right = next + } + } + stack[parentIndex + 1] = next + next.left = left + vertex.left = null + vertex.right = next.right + if (nextParent == vertex) { + next.right = vertex + } else { + next.right = right + nextParent.left = vertex + } + vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + next.right = right + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + } + vertex + } ?: left.let { left -> + // swap with the single red leaf + vertex.color = left.color.also { left.color = vertex.color } + if (root == vertex) { + root = left + } else { + val parent = stack.last() + if (parent.left == vertex) { + parent.left = left + } else { + parent.right = left + } + } + left.right = vertex + vertex.left = null + vertex.right = null + stack.add(left) + vertex + } + } ?: vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + if (root == vertex) { + root = right + } else { + val parent = stack.last() + if (parent.left == vertex) { + parent.left = right + } else { + parent.right = right + } + } + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + vertex + } ?: vertex // vertex is already a leaf + } + + private fun removeLeaf(stack: MutableList>) { + val black = RedBlackTree.Vertex.Color.BLACK + val red = RedBlackTree.Vertex.Color.RED + var notEnd = true + + fun balanceLeft(parent: Vertex): Vertex { + fun balanceBlackRightBrother(parent: Vertex): Vertex { + val brother = parent.right + if (brother != null) { + return brother.right?.let { rightCousin -> + if (rightCousin.color == red) { + notEnd = false + rightCousin.color = black + rotateLeft(parent, brother) + } else { + brother.left?.let { leftCousin -> + if (leftCousin.color == red) { + notEnd = false + parent.right = rotateRight(brother, leftCousin) + brother.color = black + rotateLeft(parent, leftCousin) + } else { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + parent + } + } ?: parent.let { + brother.color = red + if (it.color == red) { + notEnd = false + parent.color = black + } + it + } + } + } ?: brother.left?.let { leftCousin -> + if (leftCousin.color == red) { + notEnd = false + parent.right = rotateRight(brother, leftCousin) + brother.color = black + rotateLeft(parent, leftCousin) + } else { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + parent + } + } ?: parent.let { + brother.color = red + if (it.color == red) { + notEnd = false + parent.color = black + } + it + } + } else { + throw Exception("Deletion error: height of right subtree must be at least 1") + } + } + + return parent.right?.let { right -> + if (right.color == red) { + val newParent = rotateLeft(parent, right) + newParent.left = balanceBlackRightBrother(parent) + if (notEnd) { + balanceBlackRightBrother(newParent) + } else { + newParent + } + } else { + balanceBlackRightBrother(parent) + } + } ?: balanceBlackRightBrother(parent) + } + + fun balanceRight(parent: Vertex): Vertex { + fun balanceBlackLeftBrother(parent: Vertex): Vertex { + val brother = parent.left + if (brother != null) { + return brother.left?.let { leftCousin -> + if (leftCousin.color == red) { + notEnd = false + leftCousin.color = black + rotateLeft(parent, brother) + } else { + brother.right?.let { rightCousin -> + if (rightCousin.color == red) { + notEnd = false + parent.left = rotateLeft(brother, rightCousin) + brother.color = black + rotateRight(parent, rightCousin) + } else { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + parent + } + } ?: parent.let { + brother.color = red + if (it.color == red) { + notEnd = false + parent.color = black + } + it + } + } + } ?: brother.right?.let { rightCousin -> + if (rightCousin.color == red) { + notEnd = false + parent.left = rotateLeft(brother, rightCousin) + brother.color = black + rotateRight(parent, rightCousin) + } else { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + parent + } + } ?: parent.let { + brother.color = red + if (it.color == red) { + notEnd = false + parent.color = black + } + it + } + } else { + throw Exception("Deletion error: height of left subtree must be at least 1") + } + } + + return parent.left?.let { left -> + if (left.color == red) { + val newParent = rotateRight(parent, left) + newParent.right = balanceBlackLeftBrother(parent) + if (notEnd) { + balanceBlackLeftBrother(newParent) + } else { + newParent + } + } else { + balanceBlackLeftBrother(parent) + } + } ?: balanceBlackLeftBrother(parent) + } + + val goner = stack.removeLast() + if (goner.color == red) { + val parent = stack.last() + if (parent.left == goner) { + parent.left = null + } else { + parent.right = null + } + } else { + // removal black leaf + if (root == goner) { + root = null + } else { + var parent = stack.removeLast() + if (root == parent) { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + root = parent + } else { + val grandparent = stack.last() + if (grandparent.left == parent) { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + grandparent.left = parent + } else { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + grandparent.right = parent + } + } + stack.add(parent) + + while (notEnd && stack.size >= 2) { + val vertex = stack.removeLast() + var parent = stack.removeLast() + if (root == parent) { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + root = parent + } else { + val grandparent = stack.last() + if (grandparent.left == parent) { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + grandparent.left = parent + } else { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + grandparent.right = parent + } + } + stack.add(parent) + } + root?.color = black + } + } + } + protected class Vertex( override val key: K, override var value: V, From 09bf28d5adf5d1dd39dde1565e7890921b6794e2 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 19:01:57 +0300 Subject: [PATCH 28/87] feat: Added initializing functions for RBT. --- .../src/main/kotlin/binarysearchtrees/Trees.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt index bbe755e..15ad523 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt @@ -14,4 +14,18 @@ fun , V> binarySearchTreeOf( tree.put(it.first, it.second) } return tree +} + +fun , V> redBlackTreeOf(): RedBlackTree { + return object : AbstractRedBlackTree() {} +} + +fun , V> redBlackTreeOf( + vararg args: Pair +): RedBlackTree { + val tree = object : AbstractRedBlackTree() {} + for (it in args) { + tree.put(it.first, it.second) + } + return tree } \ No newline at end of file From 847918f7d0bed9555b737bab2f72dd1a14acdc35 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 21:25:30 +0300 Subject: [PATCH 29/87] fix: Changed receiving of leaf for deletion. --- .../binarysearchtrees/AbstractRedBlackTree.kt | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt index 91987ea..cb226a8 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt @@ -233,18 +233,27 @@ abstract class AbstractRedBlackTree, V> : RedBlackTree { vertex.right = next.right if (nextParent == vertex) { next.right = vertex + vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + next.right = right + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + } } else { next.right = right nextParent.left = vertex - } - vertex.right?.let { right -> - // swap with the single red leaf - vertex.color = right.color.also { right.color = vertex.color } - next.right = right - right.left = vertex - vertex.left = null - vertex.right = null - stack.add(right) + vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + nextParent.left = right + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + } } vertex } ?: left.let { left -> From fbeee001b4a035abc2c29a42b7b4767c5d84678a Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 21:28:08 +0300 Subject: [PATCH 30/87] test: Added tests for RBT. Added: - checker of RBT invariant; - added test for search(get) operation; - added test for insert(put, set) operation; - added test for remove operation; - added test for iterator exceptions. --- .../AbstractRedBlackTreeTest.kt | 147 ++++++++++++++++++ .../binarysearchtrees/InvariantCheckers.kt | 29 ++++ 2 files changed, 176 insertions(+) create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt new file mode 100644 index 0000000..e7da308 --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt @@ -0,0 +1,147 @@ +package binarysearchtrees + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random + +class AbstractRedBlackTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: AbstractRedBlackTree + + @BeforeEach + fun init() { + tree = object : AbstractRedBlackTree() {} + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isRedBlackTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, 245) } + values.forEach { tree.put(it.first, it.second) } + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) + assertEquals(values.distinctBy { it.first }.size, tree.size) + } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the some element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + var size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + + value = key * 95 + tree[key] = value + size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + var size = tree.size - 1 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) + assertEquals(0, tree.size) + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt index 4bd0f8f..8087961 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt @@ -6,4 +6,33 @@ fun , V> isBinarySearchTree(tree: BinarySearchTree): Boo && vertex.right?.let { vertex.key < it.key && checkBSTInvariant(it) } ?: true } return tree.getRoot()?.let { checkBSTInvariant(it) } ?: true +} + +fun , V> isRedBlackTree(tree: RedBlackTree): Boolean { + var f = true + + fun checkRBTInvariant(vertex: RedBlackTree.Vertex): Int { + if (vertex.color == RedBlackTree.Vertex.Color.RED) { + f = f && (vertex.left?.color != RedBlackTree.Vertex.Color.RED) + f = f && (vertex.right?.color != RedBlackTree.Vertex.Color.RED) + } + val leftBlackHeight = vertex.left?.let { + f = f && (vertex.key > it.key) + checkRBTInvariant(it) + } ?: 1 + val rightBlackHeight = vertex.right?.let { + f = f && (vertex.key < it.key) + checkRBTInvariant(it) + } ?: 1 + f = f && (leftBlackHeight == rightBlackHeight) + return if (vertex.color == RedBlackTree.Vertex.Color.BLACK) + leftBlackHeight + 1 + else leftBlackHeight + } + + return tree.getRoot()?.let { + f = (it.color == RedBlackTree.Vertex.Color.BLACK) + checkRBTInvariant(it) + f + } ?: true } \ No newline at end of file From c87f5469d32c498dd6e5fcb658918644cc324976 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 22:57:08 +0300 Subject: [PATCH 31/87] fix: Fixed orientation bug in leaf removal function. --- .../src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt index cb226a8..9073572 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt @@ -384,7 +384,7 @@ abstract class AbstractRedBlackTree, V> : RedBlackTree { if (leftCousin.color == red) { notEnd = false leftCousin.color = black - rotateLeft(parent, brother) + rotateRight(parent, brother) } else { brother.right?.let { rightCousin -> if (rightCousin.color == red) { From a0f69fa0b29ff2bf1eb416048198df57060b121e Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 22:58:59 +0300 Subject: [PATCH 32/87] test: Added another test for remove operation. Test checks removal every twentieth element. --- .../AbstractRedBlackTreeTest.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt index e7da308..792bc56 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt @@ -109,6 +109,24 @@ class AbstractRedBlackTreeTest { assertTrue(isRedBlackTree(tree)) } + @Test + fun `Function remove deletes the existing element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val elements = mutableListOf>() + values.reversed().distinctBy { it.first }.forEach { elements.add(it) } + elements.shuffle() + for (i in 0 until elements.size step 20) { + val key = elements[i].first + val value = elements[i].second + val size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + } + @Test fun `Function remove deletes the root element correctly`() { values.forEach { tree.put(it.first, it.second) } From c9fbb63f589b56b05d079b10660561551cf3c9f2 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 18 Apr 2023 23:40:59 +0300 Subject: [PATCH 33/87] test: Changed invariant checker in root removal test. --- .../test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt index 792bc56..ce60364 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt @@ -140,7 +140,7 @@ class AbstractRedBlackTreeTest { assertEquals(value, tree.remove(oldKey)) assertNotEquals(oldKey, tree.getRoot()?.key) assertEquals(size, tree.size) - assertTrue(isBinarySearchTree(tree)) + assertTrue(isRedBlackTree(tree)) oldKey = tree.getRoot()?.let { it.setValue(value) @@ -150,7 +150,7 @@ class AbstractRedBlackTreeTest { assertTrue(tree.remove(oldKey, value)) assertNotEquals(oldKey, tree.getRoot()?.key) assertEquals(size, tree.size) - assertTrue(isBinarySearchTree(tree)) + assertTrue(isRedBlackTree(tree)) } @Test From 6ad0691ececfcce4ff96a937c8922efead7733eb Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 24 Apr 2023 21:11:47 +0300 Subject: [PATCH 34/87] struct: Files removed due to architecture change. --- .../AbstractBinarySearchTree.kt | 190 ------ .../binarysearchtrees/AbstractRedBlackTree.kt | 583 ------------------ .../kotlin/binarysearchtrees/RedBlackTree.kt | 21 - .../main/kotlin/binarysearchtrees/Trees.kt | 31 - .../AbstractBinarySearchTreeTest.kt | 164 ----- .../AbstractRedBlackTreeTest.kt | 165 ----- .../binarysearchtrees/InvariantCheckers.kt | 38 -- 7 files changed, 1192 deletions(-) delete mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt delete mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt delete mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt delete mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt delete mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt delete mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt delete mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt deleted file mode 100644 index aa7e749..0000000 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractBinarySearchTree.kt +++ /dev/null @@ -1,190 +0,0 @@ -package binarysearchtrees - -abstract class AbstractBinarySearchTree, V> : BinarySearchTree { - override var size: Int = 0 - protected var root: Vertex? = null - protected var modCount: Int = 0 - - override fun isEmpty(): Boolean = (root == null) - - override fun clear() { - size = 0 - root = null - ++modCount - } - - override fun getRoot(): BinarySearchTree.MutableVertex? = root - - override fun iterator(): Iterator> { - return BinarySearchTreeIterator(getRoot()) { modCount } - } - - override fun get(key: K): V? { - var vertex = root - while (vertex != null && vertex.key != key) { - if (vertex.key > key) { - vertex = vertex.left - } else { - vertex = vertex.right - } - } - return vertex?.value - } - - override fun put(key: K, value: V): V? { - var f = false - var vertex: Vertex = root ?: Vertex(key, value).also { - root = it - f = true - } - while (vertex.key != key) { - if (vertex.key > key) { - vertex = vertex.left ?: Vertex(key, value).also { - vertex.left = it - f = true - } - } else { - vertex = vertex.right ?: Vertex(key, value).also { - vertex.right = it - f = true - } - } - } - return if (f) { - ++size - ++modCount - null - } else vertex.setValue(value) - } - - override fun set(key: K, value: V) { - put(key, value) - } - - override fun remove(key: K): V? { - var parent: Vertex? = null - var vertex = root - while (vertex != null && vertex.key != key) { - parent = vertex - if (vertex.key > key) { - vertex = vertex.left - } else { - vertex = vertex.right - } - } - val oldValue = vertex?.value - if (vertex != null) { - if (parent == null) { - root = vertex.let { removeVertex(it) } - } else { - if (parent.left == vertex) { - parent.left = vertex.let { removeVertex(it) } - } else { - parent.right = vertex.let { removeVertex(it) } - } - } - --size - ++modCount - } - return oldValue - } - - override fun remove(key: K, value: V): Boolean { - var parent: Vertex? = null - var vertex = root - while (vertex != null && vertex.key != key) { - parent = vertex - if (vertex.key > key) { - vertex = vertex.left - } else { - vertex = vertex.right - } - } - return if (vertex?.value == value) { - if (parent == null) { - root = vertex?.let { removeVertex(it) } - } else { - if (parent.left == vertex) { - parent.left = vertex?.let { removeVertex(it) } - } else { - parent.right = vertex?.let { removeVertex(it) } - } - } - --size - ++modCount - true - } else false - } - - private fun removeVertex(vertex: Vertex): Vertex? { - return vertex.left?.let { left -> - vertex.right?.let { right -> - //search of parent of Vertex with next key - var nextParent = vertex - var next = right // vertex.right - while (next.left != null) { - next = next.left?.let { - nextParent = next - it - } ?: next - } - if (nextParent == vertex) { - next.left = left // vertex.left - } else { - nextParent.left = null - next.left = left // vertex.left - next.right = right // vertex.right - } - - next - } ?: left - } ?: vertex.right - } - - protected class Vertex( - override val key: K, - override var value: V, - override var left: Vertex? = null, - override var right: Vertex? = null - ) : BinarySearchTree.MutableVertex { - override fun setValue(newValue: V): V = value.also { value = newValue } - } - - protected class BinarySearchTreeIterator( - root: BinarySearchTree.MutableVertex?, - private val getModCount: () -> Int - ) : Iterator> { - private val stack: MutableList> = mutableListOf() - private val expectedModCount: Int = getModCount() - - init { - var vertex = root - while (vertex != null) { - stack.add(vertex) - vertex = vertex.left - } - } - - override fun hasNext(): Boolean { - if (expectedModCount != getModCount()) { - throw ConcurrentModificationException() - } else { - return stack.isNotEmpty() - } - } - - override fun next(): BinarySearchTree.MutableVertex { - if (expectedModCount != getModCount()) { - throw ConcurrentModificationException() - } else { - val vertex = stack.removeLast() - var nextVertex = vertex.right - while (nextVertex != null) { - stack.add(nextVertex) - nextVertex = nextVertex.left - } - return vertex - } - } - } -} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt deleted file mode 100644 index 9073572..0000000 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/AbstractRedBlackTree.kt +++ /dev/null @@ -1,583 +0,0 @@ -package binarysearchtrees - -abstract class AbstractRedBlackTree, V> : RedBlackTree { - override var size: Int = 0 - protected var root: Vertex? = null - protected var modCount: Int = 0 - - override fun isEmpty(): Boolean = (root == null) - - override fun clear() { - size = 0 - root = null - ++modCount - } - - override fun getRoot(): RedBlackTree.MutableVertex? = root - - override fun iterator(): Iterator> { - return RedBlackTreeIterator(getRoot()) { modCount } - } - - override fun get(key: K): V? { - var vertex = root - while (vertex != null && vertex.key != key) { - if (vertex.key > key) { - vertex = vertex.left - } else { - vertex = vertex.right - } - } - return vertex?.value - } - - override fun put(key: K, value: V): V? { - var new = false - val stack = mutableListOf>() - var vertex = root ?: Vertex(key, value, RedBlackTree.Vertex.Color.BLACK).also { - root = it - new = true - } - while (vertex.key != key) { - stack.add(vertex) - if (vertex.key > key) { - vertex = vertex.left ?: Vertex(key, value).also { - vertex.left = it - new = true - } - } else { - vertex = vertex.right ?: Vertex(key, value).also { - vertex.right = it - new = true - } - } - } - - // balance and return - return if (new) { - stack.add(vertex) - balanceAfterInsert(stack) - stack.clear() - ++size - ++modCount - null - } else { - stack.clear() - vertex.setValue(value) - } - } - - override fun set(key: K, value: V) { - put(key, value) - } - - override fun remove(key: K): V? { - val stack = mutableListOf>() - var parentIndex = -1 - var vertex = root - while (vertex != null && vertex.key != key) { - stack.add(vertex) - ++parentIndex - if (vertex.key > key) { - vertex = vertex.left - } else { - vertex = vertex.right - } - } - val oldValue = vertex?.value - if (vertex != null) { - val goner = getGoner(vertex, parentIndex, stack) - - // leaf removal - stack.add(goner) - removeLeaf(stack) - --size - ++modCount - } - stack.clear() - return oldValue - } - - override fun remove(key: K, value: V): Boolean { - val stack = mutableListOf>() - var parentIndex = -1 - var vertex = root - while (vertex != null && vertex.key != key) { - stack.add(vertex) - ++parentIndex - if (vertex.key > key) { - vertex = vertex.left - } else { - vertex = vertex.right - } - } - return if (vertex != null && vertex.value == value) { - val goner = getGoner(vertex, parentIndex, stack) - - // leaf removal - stack.add(goner) - removeLeaf(stack) - --size - ++modCount - stack.clear() - true - } else { - stack.clear() - false - } - } - - private fun rotateLeft(parent: Vertex, child: Vertex): Vertex { - parent.color = child.color.also { child.color = parent.color } - parent.right = child.left - child.left = parent - return child - } - - private fun rotateRight(parent: Vertex, child: Vertex): Vertex { - parent.color = child.color.also { child.color = parent.color } - parent.left = child.right - child.right = parent - return child - } - - private fun balanceAfterInsert(stack: MutableList>) { - val black = RedBlackTree.Vertex.Color.BLACK - val red = RedBlackTree.Vertex.Color.RED - var notEnd = true - - var vertex = stack.removeLast() - while (notEnd && stack.lastOrNull()?.color == red) { - var parent = stack.removeLast() - val grandparent = stack.removeLast() - if (grandparent.left == parent) { - if (grandparent.right?.color == red) { - grandparent.color = red - grandparent.right?.color = black - parent.color = black - vertex = grandparent - } else { - notEnd = false - if (parent.right == vertex) { - parent = rotateLeft(parent, vertex) - grandparent.left = parent - } - if (root == grandparent) { - root = rotateRight(grandparent, parent) - } else { - val ggparent = stack.last() - if (ggparent.left == grandparent) { - ggparent.left = rotateRight(grandparent, parent) - } else { - ggparent.right = rotateRight(grandparent, parent) - } - } - } - } else { - if (grandparent.left?.color == red) { - grandparent.color = red - grandparent.left?.color = black - parent.color = black - vertex = grandparent - } else { - notEnd = false - if (parent.left == vertex) { - parent = rotateRight(parent, vertex) - grandparent.right = parent - } - if (root == grandparent) { - root = rotateLeft(grandparent, parent) - } else { - val ggparent = stack.last() - if (ggparent.left == grandparent) { - ggparent.left = rotateLeft(grandparent, parent) - } else { - ggparent.right = rotateLeft(grandparent, parent) - } - } - } - } - } - - root?.color = black - } - - private fun getGoner(vertex: Vertex, parentIndex: Int, stack: MutableList>): Vertex { - return vertex.left?.let { left -> - vertex.right?.let { right -> - // swap with the vertex that is next by key - var nextParent: Vertex = vertex - stack.add(nextParent) - var next = right - while (next.left != null) { - next = next.left?.let { - nextParent = next - stack.add(nextParent) - it - } ?: next - } - vertex.color = next.color.also { next.color = vertex.color } - if (root == vertex) { - root = next - } else { - val parent = stack[parentIndex] - if (parent.left == vertex) { - parent.left = next - } else { - parent.right = next - } - } - stack[parentIndex + 1] = next - next.left = left - vertex.left = null - vertex.right = next.right - if (nextParent == vertex) { - next.right = vertex - vertex.right?.let { right -> - // swap with the single red leaf - vertex.color = right.color.also { right.color = vertex.color } - next.right = right - right.left = vertex - vertex.left = null - vertex.right = null - stack.add(right) - } - } else { - next.right = right - nextParent.left = vertex - vertex.right?.let { right -> - // swap with the single red leaf - vertex.color = right.color.also { right.color = vertex.color } - nextParent.left = right - right.left = vertex - vertex.left = null - vertex.right = null - stack.add(right) - } - } - vertex - } ?: left.let { left -> - // swap with the single red leaf - vertex.color = left.color.also { left.color = vertex.color } - if (root == vertex) { - root = left - } else { - val parent = stack.last() - if (parent.left == vertex) { - parent.left = left - } else { - parent.right = left - } - } - left.right = vertex - vertex.left = null - vertex.right = null - stack.add(left) - vertex - } - } ?: vertex.right?.let { right -> - // swap with the single red leaf - vertex.color = right.color.also { right.color = vertex.color } - if (root == vertex) { - root = right - } else { - val parent = stack.last() - if (parent.left == vertex) { - parent.left = right - } else { - parent.right = right - } - } - right.left = vertex - vertex.left = null - vertex.right = null - stack.add(right) - vertex - } ?: vertex // vertex is already a leaf - } - - private fun removeLeaf(stack: MutableList>) { - val black = RedBlackTree.Vertex.Color.BLACK - val red = RedBlackTree.Vertex.Color.RED - var notEnd = true - - fun balanceLeft(parent: Vertex): Vertex { - fun balanceBlackRightBrother(parent: Vertex): Vertex { - val brother = parent.right - if (brother != null) { - return brother.right?.let { rightCousin -> - if (rightCousin.color == red) { - notEnd = false - rightCousin.color = black - rotateLeft(parent, brother) - } else { - brother.left?.let { leftCousin -> - if (leftCousin.color == red) { - notEnd = false - parent.right = rotateRight(brother, leftCousin) - brother.color = black - rotateLeft(parent, leftCousin) - } else { - brother.color = red - if (parent.color == red) { - notEnd = false - parent.color = black - } - parent - } - } ?: parent.let { - brother.color = red - if (it.color == red) { - notEnd = false - parent.color = black - } - it - } - } - } ?: brother.left?.let { leftCousin -> - if (leftCousin.color == red) { - notEnd = false - parent.right = rotateRight(brother, leftCousin) - brother.color = black - rotateLeft(parent, leftCousin) - } else { - brother.color = red - if (parent.color == red) { - notEnd = false - parent.color = black - } - parent - } - } ?: parent.let { - brother.color = red - if (it.color == red) { - notEnd = false - parent.color = black - } - it - } - } else { - throw Exception("Deletion error: height of right subtree must be at least 1") - } - } - - return parent.right?.let { right -> - if (right.color == red) { - val newParent = rotateLeft(parent, right) - newParent.left = balanceBlackRightBrother(parent) - if (notEnd) { - balanceBlackRightBrother(newParent) - } else { - newParent - } - } else { - balanceBlackRightBrother(parent) - } - } ?: balanceBlackRightBrother(parent) - } - - fun balanceRight(parent: Vertex): Vertex { - fun balanceBlackLeftBrother(parent: Vertex): Vertex { - val brother = parent.left - if (brother != null) { - return brother.left?.let { leftCousin -> - if (leftCousin.color == red) { - notEnd = false - leftCousin.color = black - rotateRight(parent, brother) - } else { - brother.right?.let { rightCousin -> - if (rightCousin.color == red) { - notEnd = false - parent.left = rotateLeft(brother, rightCousin) - brother.color = black - rotateRight(parent, rightCousin) - } else { - brother.color = red - if (parent.color == red) { - notEnd = false - parent.color = black - } - parent - } - } ?: parent.let { - brother.color = red - if (it.color == red) { - notEnd = false - parent.color = black - } - it - } - } - } ?: brother.right?.let { rightCousin -> - if (rightCousin.color == red) { - notEnd = false - parent.left = rotateLeft(brother, rightCousin) - brother.color = black - rotateRight(parent, rightCousin) - } else { - brother.color = red - if (parent.color == red) { - notEnd = false - parent.color = black - } - parent - } - } ?: parent.let { - brother.color = red - if (it.color == red) { - notEnd = false - parent.color = black - } - it - } - } else { - throw Exception("Deletion error: height of left subtree must be at least 1") - } - } - - return parent.left?.let { left -> - if (left.color == red) { - val newParent = rotateRight(parent, left) - newParent.right = balanceBlackLeftBrother(parent) - if (notEnd) { - balanceBlackLeftBrother(newParent) - } else { - newParent - } - } else { - balanceBlackLeftBrother(parent) - } - } ?: balanceBlackLeftBrother(parent) - } - - val goner = stack.removeLast() - if (goner.color == red) { - val parent = stack.last() - if (parent.left == goner) { - parent.left = null - } else { - parent.right = null - } - } else { - // removal black leaf - if (root == goner) { - root = null - } else { - var parent = stack.removeLast() - if (root == parent) { - if (parent.left == goner) { - parent.left = null - parent = balanceLeft(parent) - } else { - parent.right = null - parent = balanceRight(parent) - } - root = parent - } else { - val grandparent = stack.last() - if (grandparent.left == parent) { - if (parent.left == goner) { - parent.left = null - parent = balanceLeft(parent) - } else { - parent.right = null - parent = balanceRight(parent) - } - grandparent.left = parent - } else { - if (parent.left == goner) { - parent.left = null - parent = balanceLeft(parent) - } else { - parent.right = null - parent = balanceRight(parent) - } - grandparent.right = parent - } - } - stack.add(parent) - - while (notEnd && stack.size >= 2) { - val vertex = stack.removeLast() - var parent = stack.removeLast() - if (root == parent) { - if (parent.left == vertex) { - parent = balanceLeft(parent) - } else { - parent = balanceRight(parent) - } - root = parent - } else { - val grandparent = stack.last() - if (grandparent.left == parent) { - if (parent.left == vertex) { - parent = balanceLeft(parent) - } else { - parent = balanceRight(parent) - } - grandparent.left = parent - } else { - if (parent.left == vertex) { - parent = balanceLeft(parent) - } else { - parent = balanceRight(parent) - } - grandparent.right = parent - } - } - stack.add(parent) - } - root?.color = black - } - } - } - - protected class Vertex( - override val key: K, - override var value: V, - override var color: RedBlackTree.Vertex.Color = RedBlackTree.Vertex.Color.RED, - override var left: Vertex? = null, - override var right: Vertex? = null - ) : RedBlackTree.MutableVertex { - override fun setValue(newValue: V): V = value.also { value = newValue } - } - - protected class RedBlackTreeIterator( - root: RedBlackTree.MutableVertex?, - private val getModCount: () -> Int - ) : Iterator> { - private val stack: MutableList> = mutableListOf() - private val expectedModCount: Int = getModCount() - - init { - var vertex = root - while (vertex != null) { - stack.add(vertex) - vertex = vertex.left - } - } - - override fun hasNext(): Boolean { - if (expectedModCount != getModCount()) { - throw ConcurrentModificationException() - } else { - return stack.isNotEmpty() - } - } - - override fun next(): RedBlackTree.MutableVertex { - if (expectedModCount != getModCount()) { - throw ConcurrentModificationException() - } else { - val vertex = stack.removeLast() - var nextVertex = vertex.right - while (nextVertex != null) { - stack.add(nextVertex) - nextVertex = nextVertex.left - } - return vertex - } - } - } -} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt deleted file mode 100644 index efce916..0000000 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/RedBlackTree.kt +++ /dev/null @@ -1,21 +0,0 @@ -package binarysearchtrees - -interface RedBlackTree, V> : BinarySearchTree { - override fun getRoot(): MutableVertex? - - override operator fun iterator(): Iterator> - - //for extension functions like all, any, ... - interface Vertex : BinarySearchTree.Vertex { - val color: Color - override val left: Vertex? - override val right: Vertex? - - enum class Color { RED, BLACK } - } - - interface MutableVertex : BinarySearchTree.MutableVertex, Vertex { - override val left: MutableVertex? - override val right: MutableVertex? - } -} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt deleted file mode 100644 index 15ad523..0000000 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt +++ /dev/null @@ -1,31 +0,0 @@ -package binarysearchtrees - -// initializing functions - -fun , V> binarySearchTreeOf(): BinarySearchTree { - return object : AbstractBinarySearchTree() {} -} - -fun , V> binarySearchTreeOf( - vararg args: Pair -): BinarySearchTree { - val tree = object : AbstractBinarySearchTree() {} - for (it in args) { - tree.put(it.first, it.second) - } - return tree -} - -fun , V> redBlackTreeOf(): RedBlackTree { - return object : AbstractRedBlackTree() {} -} - -fun , V> redBlackTreeOf( - vararg args: Pair -): RedBlackTree { - val tree = object : AbstractRedBlackTree() {} - for (it in args) { - tree.put(it.first, it.second) - } - return tree -} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt deleted file mode 100644 index 349ba80..0000000 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractBinarySearchTreeTest.kt +++ /dev/null @@ -1,164 +0,0 @@ -package binarysearchtrees - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import kotlin.random.Random - -class AbstractBinarySearchTreeTest { - private val randomizer = Random(100) - private val elementsCount = 1000 - private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } - private lateinit var tree: AbstractBinarySearchTree - - @BeforeEach - fun init() { - tree = object : AbstractBinarySearchTree() {} - } - - @Test - fun `Function put doesn't violate the invariant`() { - values.forEach { - tree.put(it.first, it.second) - assertTrue(isBinarySearchTree(tree)) - } - } - - @Test - fun `Function put adds all elements with unique keys`() { - values.forEach { tree.put(it.first, it.second) } - val listOfPairKeyValue = mutableListOf>() - for (it in tree) { - listOfPairKeyValue.add(Pair(it.key, it.value)) - } - assertEquals( - values.reversed().distinctBy { it.first }.sortedBy { it.first }, - listOfPairKeyValue - ) - assertEquals(values.distinctBy { it.first }.size, tree.size) - } - - @ParameterizedTest - @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) - fun `Functions of iterator throws exceptions after change tree`(key: Int) { - values.forEach { tree.put(it.first, it.second) } - tree.put(key, 69) - - var iterator = tree.iterator() - tree.remove(key) - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - tree.put(key, key * 100) - assertThrows(ConcurrentModificationException::class.java) { iterator.next() } - - iterator = tree.iterator() - tree.remove(key, key * 100) - assertThrows(ConcurrentModificationException::class.java) { iterator.next() } - - iterator = tree.iterator() - tree[key] = key * 100 - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - tree.clear() - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - assertThrows(NoSuchElementException::class.java) { iterator.next() } - } - - @ParameterizedTest(name = "Function get returns correct value for key {0}") - @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) - fun `Function get returns correct value`(key: Int) { - values.forEach { tree.put(it.first, it.second) } - - val expected = key * 198 - tree[key] = expected - assertEquals(expected, tree.get(key)) - - tree.remove(key) - assertEquals(null, tree.get(key)) - } - - @ParameterizedTest - @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) - fun `Function remove deletes the some element correctly`(key: Int) { - values.forEach { tree.put(it.first, it.second) } - - var value = key * 198 - tree[key] = value - var size = tree.size - 1 - assertEquals(value, tree.remove(key)) - assertEquals(null, tree.remove(key)) - assertEquals(null, tree[key]) - assertEquals(size, tree.size) - assertTrue(isBinarySearchTree(tree)) - - value = key * 95 - tree[key] = value - size = tree.size - 1 - assertFalse(tree.remove(key, value + 10)) - assertTrue(tree.remove(key, value)) - assertFalse(tree.remove(key, value)) - assertEquals(null, tree[key]) - assertEquals(size, tree.size) - assertTrue(isBinarySearchTree(tree)) - } - - @Test - fun `Function remove deletes the existing element correctly`() { - values.forEach { tree.put(it.first, it.second) } - - val elements = mutableListOf>() - values.reversed().distinctBy { it.first }.forEach { elements.add(it) } - elements.shuffle() - for (i in 0 until elements.size step 20) { - val key = elements[i].first - val value = elements[i].second - val size = tree.size - 1 - assertEquals(value, tree.remove(key)) - assertEquals(null, tree[key]) - assertEquals(size, tree.size) - assertTrue(isBinarySearchTree(tree)) - } - } - - @Test - fun `Function remove deletes the root element correctly`() { - values.forEach { tree.put(it.first, it.second) } - - val value = 45 - var oldKey = tree.getRoot()?.let { - it.setValue(value) - it.key - } ?: -25 - var size = tree.size - 1 - assertEquals(value, tree.remove(oldKey)) - assertNotEquals(oldKey, tree.getRoot()?.key) - assertEquals(size, tree.size) - assertTrue(isBinarySearchTree(tree)) - - oldKey = tree.getRoot()?.let { - it.setValue(value) - it.key - } ?: -25 - size = tree.size - 1 - assertTrue(tree.remove(oldKey, value)) - assertNotEquals(oldKey, tree.getRoot()?.key) - assertEquals(size, tree.size) - assertTrue(isBinarySearchTree(tree)) - } - - @Test - fun `Function clear makes tree empty`() { - values.forEach { tree.put(it.first, it.second) } - - tree.clear() - assertTrue(tree.isEmpty()) - assertNull(tree.getRoot()) - assertEquals(0, tree.size) - } -} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt deleted file mode 100644 index ce60364..0000000 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/AbstractRedBlackTreeTest.kt +++ /dev/null @@ -1,165 +0,0 @@ -package binarysearchtrees - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import kotlin.random.Random - -class AbstractRedBlackTreeTest { - private val randomizer = Random(100) - private val elementsCount = 1000 - private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } - private lateinit var tree: AbstractRedBlackTree - - @BeforeEach - fun init() { - tree = object : AbstractRedBlackTree() {} - } - - @Test - fun `Function put doesn't violate the invariant`() { - values.forEach { - tree.put(it.first, it.second) - assertTrue(isRedBlackTree(tree)) - } - } - - @Test - fun `Function put adds all elements with unique keys`() { - values.forEach { tree.put(it.first, 245) } - values.forEach { tree.put(it.first, it.second) } - val listOfPairKeyValue = mutableListOf>() - for (it in tree) { - listOfPairKeyValue.add(Pair(it.key, it.value)) - } - assertEquals( - values.reversed().distinctBy { it.first }.sortedBy { it.first }, - listOfPairKeyValue - ) - assertEquals(values.distinctBy { it.first }.size, tree.size) - } - - @ParameterizedTest - @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) - fun `Functions of iterator throws exceptions after change tree`(key: Int) { - values.forEach { tree.put(it.first, it.second) } - tree.put(key, 69) - - var iterator = tree.iterator() - tree.remove(key) - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - tree.put(key, key * 100) - assertThrows(ConcurrentModificationException::class.java) { iterator.next() } - - iterator = tree.iterator() - tree.remove(key, key * 100) - assertThrows(ConcurrentModificationException::class.java) { iterator.next() } - - iterator = tree.iterator() - tree[key] = key * 100 - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - tree.clear() - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - assertThrows(NoSuchElementException::class.java) { iterator.next() } - } - - @ParameterizedTest(name = "Function get returns correct value for key {0}") - @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) - fun `Function get returns correct value`(key: Int) { - values.forEach { tree.put(it.first, it.second) } - - val expected = key * 198 - tree[key] = expected - assertEquals(expected, tree.get(key)) - - tree.remove(key) - assertEquals(null, tree.get(key)) - } - - @ParameterizedTest - @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) - fun `Function remove deletes the some element correctly`(key: Int) { - values.forEach { tree.put(it.first, it.second) } - - var value = key * 198 - tree[key] = value - var size = tree.size - 1 - assertEquals(value, tree.remove(key)) - assertEquals(null, tree.remove(key)) - assertEquals(null, tree[key]) - assertEquals(size, tree.size) - assertTrue(isRedBlackTree(tree)) - - value = key * 95 - tree[key] = value - size = tree.size - 1 - assertFalse(tree.remove(key, value + 10)) - assertTrue(tree.remove(key, value)) - assertFalse(tree.remove(key, value)) - assertEquals(null, tree[key]) - assertEquals(size, tree.size) - assertTrue(isRedBlackTree(tree)) - } - - @Test - fun `Function remove deletes the existing element correctly`() { - values.forEach { tree.put(it.first, it.second) } - - val elements = mutableListOf>() - values.reversed().distinctBy { it.first }.forEach { elements.add(it) } - elements.shuffle() - for (i in 0 until elements.size step 20) { - val key = elements[i].first - val value = elements[i].second - val size = tree.size - 1 - assertEquals(value, tree.remove(key)) - assertEquals(null, tree[key]) - assertEquals(size, tree.size) - assertTrue(isRedBlackTree(tree)) - } - } - - @Test - fun `Function remove deletes the root element correctly`() { - values.forEach { tree.put(it.first, it.second) } - - val value = 45 - var oldKey = tree.getRoot()?.let { - it.setValue(value) - it.key - } ?: -25 - var size = tree.size - 1 - assertEquals(value, tree.remove(oldKey)) - assertNotEquals(oldKey, tree.getRoot()?.key) - assertEquals(size, tree.size) - assertTrue(isRedBlackTree(tree)) - - oldKey = tree.getRoot()?.let { - it.setValue(value) - it.key - } ?: -25 - size = tree.size - 1 - assertTrue(tree.remove(oldKey, value)) - assertNotEquals(oldKey, tree.getRoot()?.key) - assertEquals(size, tree.size) - assertTrue(isRedBlackTree(tree)) - } - - @Test - fun `Function clear makes tree empty`() { - values.forEach { tree.put(it.first, it.second) } - - tree.clear() - assertTrue(tree.isEmpty()) - assertNull(tree.getRoot()) - assertEquals(0, tree.size) - } -} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt deleted file mode 100644 index 8087961..0000000 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/InvariantCheckers.kt +++ /dev/null @@ -1,38 +0,0 @@ -package binarysearchtrees - -fun , V> isBinarySearchTree(tree: BinarySearchTree): Boolean { - fun checkBSTInvariant(vertex: BinarySearchTree.Vertex): Boolean { - return vertex.left?.let { vertex.key > it.key && checkBSTInvariant(it) } ?: true - && vertex.right?.let { vertex.key < it.key && checkBSTInvariant(it) } ?: true - } - return tree.getRoot()?.let { checkBSTInvariant(it) } ?: true -} - -fun , V> isRedBlackTree(tree: RedBlackTree): Boolean { - var f = true - - fun checkRBTInvariant(vertex: RedBlackTree.Vertex): Int { - if (vertex.color == RedBlackTree.Vertex.Color.RED) { - f = f && (vertex.left?.color != RedBlackTree.Vertex.Color.RED) - f = f && (vertex.right?.color != RedBlackTree.Vertex.Color.RED) - } - val leftBlackHeight = vertex.left?.let { - f = f && (vertex.key > it.key) - checkRBTInvariant(it) - } ?: 1 - val rightBlackHeight = vertex.right?.let { - f = f && (vertex.key < it.key) - checkRBTInvariant(it) - } ?: 1 - f = f && (leftBlackHeight == rightBlackHeight) - return if (vertex.color == RedBlackTree.Vertex.Color.BLACK) - leftBlackHeight + 1 - else leftBlackHeight - } - - return tree.getRoot()?.let { - f = (it.color == RedBlackTree.Vertex.Color.BLACK) - checkRBTInvariant(it) - f - } ?: true -} \ No newline at end of file From d1e71ad5f1f26a5657da07b96492a13dc1a32ac1 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 24 Apr 2023 21:21:00 +0300 Subject: [PATCH 35/87] fix: Vertex interfaces moved to a separate file. --- .../kotlin/binarysearchtrees/BinarySearchTree.kt | 15 --------------- .../main/kotlin/binarysearchtrees/Vertices.kt | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/Vertices.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt index da48761..89d327e 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/BinarySearchTree.kt @@ -20,19 +20,4 @@ interface BinarySearchTree, V> { fun remove(key: K, value: V): Boolean operator fun iterator(): Iterator> - - //for extension functions like all, any, ... - interface Vertex { - val key: K - val value: V - val left: Vertex? - val right: Vertex? - } - - interface MutableVertex : Vertex { - override val left: MutableVertex? - override val right: MutableVertex? - - fun setValue(newValue: V): V - } } \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Vertices.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Vertices.kt new file mode 100644 index 0000000..569c1d8 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Vertices.kt @@ -0,0 +1,16 @@ +package binarysearchtrees + +//for extension functions like all, any, ... +interface Vertex { + val key: K + val value: V + val left: Vertex? + val right: Vertex? +} + +interface MutableVertex : Vertex { + override val left: MutableVertex? + override val right: MutableVertex? + + fun setValue(newValue: V): V +} \ No newline at end of file From 06f1e41131e3507285ecef9d87968f80314c762c Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 24 Apr 2023 21:24:08 +0300 Subject: [PATCH 36/87] feat: Added simple BST implementation. --- .../SimpleBinarySearchTree.kt | 194 ++++++++++++++++++ .../binarysearchtree/Vertex.kt | 8 + 2 files changed, 202 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/Vertex.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt new file mode 100644 index 0000000..a10fca2 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt @@ -0,0 +1,194 @@ +package binarysearchtrees.binarysearchtree + +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.binarysearchtree.Vertex as PublicVertex + +open class SimpleBinarySearchTree, V> : BinarySearchTree { + final override var size: Int = 0 + private set + protected var root: Vertex? = null + protected var modCount: Int = 0 + + override fun isEmpty(): Boolean = (root == null) + + override fun clear() { + size = 0 + root = null + ++modCount + } + + override fun getRoot(): PublicVertex? = root + + override fun iterator(): Iterator> { + return BinarySearchTreeIterator(getRoot()) { modCount } + } + + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + + override fun put(key: K, value: V): V? { + var new = false + var vertex: Vertex = root ?: Vertex(key, value).also { + root = it + new = true + } + while (vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left ?: Vertex(key, value).also { + vertex.left = it + new = true + } + } else { + vertex = vertex.right ?: Vertex(key, value).also { + vertex.right = it + new = true + } + } + } + return if (new) { + ++size + ++modCount + null + } else vertex.setValue(value) + } + + override operator fun set(key: K, value: V) { + put(key, value) + } + + override fun remove(key: K): V? { + var parent: Vertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + val oldValue = vertex?.value + if (vertex != null) { + if (parent == null) { + root = removeVertex(vertex) + } else { + if (parent.left == vertex) { + parent.left = removeVertex(vertex) + } else { + parent.right = removeVertex(vertex) + } + } + --size + ++modCount + } + return oldValue + } + + override fun remove(key: K, value: V): Boolean { + var parent: Vertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return if (vertex?.value == value) { + if (parent == null) { + root = vertex?.let { removeVertex(it) } + } else { + if (parent.left == vertex) { + parent.left = vertex?.let { removeVertex(it) } + } else { + parent.right = vertex?.let { removeVertex(it) } + } + } + --size + ++modCount + true + } else false + } + + private fun removeVertex(vertex: Vertex): Vertex? { + return vertex.left?.let { left -> + vertex.right?.let { right -> + //search of parent of Vertex with next key + var nextParent = vertex + var next = right // vertex.right + var nextLeft = next.left // left son of next vertex + while (nextLeft != null) { + nextParent = next + next = nextLeft + nextLeft = next.left + } + if (nextParent == vertex) { + next.left = left // vertex.left + } else { + nextParent.left = next.right + next.left = left // vertex.left + next.right = right // vertex.right + } + + next + } ?: left + } ?: vertex.right + } + + protected class Vertex( + override val key: K, + override var value: V, + override var left: Vertex? = null, + override var right: Vertex? = null + ) : PublicVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } + + protected class BinarySearchTreeIterator( + root: PublicVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left + } + } + + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() + } + } + + override fun next(): PublicVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex + } + } + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/Vertex.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/Vertex.kt new file mode 100644 index 0000000..7a04e31 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/Vertex.kt @@ -0,0 +1,8 @@ +package binarysearchtrees.binarysearchtree + +import binarysearchtrees.MutableVertex + +interface Vertex : MutableVertex { + override val left: Vertex? + override val right: Vertex? +} \ No newline at end of file From 4c9845bedb12315b9de6e8d67b0fe78afa539eb9 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 24 Apr 2023 21:26:28 +0300 Subject: [PATCH 37/87] test: Added tests for simple BST. --- .../binarysearchtree/BSTInvarinatChecker.kt | 9 + .../SimpleBinarySearchTreeTest.kt | 164 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvarinatChecker.kt create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTreeTest.kt diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvarinatChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvarinatChecker.kt new file mode 100644 index 0000000..1d6be1e --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvarinatChecker.kt @@ -0,0 +1,9 @@ +package binarysearchtrees.binarysearchtree + +fun , V> isBinarySearchTree(tree: SimpleBinarySearchTree): Boolean { + fun checkBSTInvariant(vertex: Vertex): Boolean { + return vertex.left?.let { vertex.key > it.key && checkBSTInvariant(it) } ?: true + && vertex.right?.let { vertex.key < it.key && checkBSTInvariant(it) } ?: true + } + return tree.getRoot()?.let { checkBSTInvariant(it) } ?: true +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTreeTest.kt new file mode 100644 index 0000000..ef171e4 --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTreeTest.kt @@ -0,0 +1,164 @@ +package binarysearchtrees.binarysearchtree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random + +class SimpleBinarySearchTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: SimpleBinarySearchTree + + @BeforeEach + fun init() { + tree = SimpleBinarySearchTree() + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isBinarySearchTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, it.second) } + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) + assertEquals(values.distinctBy { it.first }.size, tree.size) + } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the some element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + var size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + + value = key * 95 + tree[key] = value + size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + + @Test + fun `Function remove deletes the existing element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val elements = mutableListOf>() + values.reversed().distinctBy { it.first }.forEach { elements.add(it) } + elements.shuffle() + for (i in 0 until elements.size step 20) { + val key = elements[i].first + val value = elements[i].second + val size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + var size = tree.size - 1 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isBinarySearchTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) + assertEquals(0, tree.size) + } +} \ No newline at end of file From fc0b8040a877aaf6156538c2eba4eed1810278ff Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 24 Apr 2023 21:30:53 +0300 Subject: [PATCH 38/87] feat: Added RBT implementation. --- .../redblacktree/RedBlackTree.kt | 528 ++++++++++++++++++ .../binarysearchtrees/redblacktree/Vertex.kt | 11 + 2 files changed, 539 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/Vertex.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt new file mode 100644 index 0000000..6609f97 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt @@ -0,0 +1,528 @@ +package binarysearchtrees.redblacktree + +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.redblacktree.Vertex.Color +import binarysearchtrees.redblacktree.Vertex as PublicVertex + +open class RedBlackTree, V> : BinarySearchTree { + final override var size: Int = 0 + private set + protected var root: Vertex? = null + protected var modCount: Int = 0 + + override fun isEmpty(): Boolean = (root == null) + + override fun clear() { + size = 0 + root = null + ++modCount + } + + override fun getRoot(): PublicVertex? = root + + override fun iterator(): Iterator> { + return RedBlackTreeIterator(getRoot()) { modCount } + } + + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + + override fun put(key: K, value: V): V? { + var new = false + val stack = mutableListOf>() + var vertex = root ?: Vertex(key, value, Color.BLACK).also { + root = it + new = true + } + while (vertex.key != key) { + stack.add(vertex) + if (vertex.key > key) { + vertex = vertex.left ?: Vertex(key, value).also { + vertex.left = it + new = true + } + } else { + vertex = vertex.right ?: Vertex(key, value).also { + vertex.right = it + new = true + } + } + } + + // balance and return + return if (new) { + stack.add(vertex) + balanceAfterInsert(stack) + stack.clear() + ++size + ++modCount + null + } else { + stack.clear() + vertex.setValue(value) + } + } + + override operator fun set(key: K, value: V) { + put(key, value) + } + + override fun remove(key: K): V? { + val stack = mutableListOf>() + var parentIndex = -1 + var vertex = root + while (vertex != null && vertex.key != key) { + stack.add(vertex) + ++parentIndex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + val oldValue = vertex?.value + if (vertex != null) { + val goner = getGoner(vertex, parentIndex, stack) + + // leaf removal + stack.add(goner) + removeLeaf(stack) + --size + ++modCount + } + stack.clear() + return oldValue + } + + override fun remove(key: K, value: V): Boolean { + val stack = mutableListOf>() + var parentIndex = -1 + var vertex = root + while (vertex != null && vertex.key != key) { + stack.add(vertex) + ++parentIndex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return if (vertex != null && vertex.value == value) { + val goner = getGoner(vertex, parentIndex, stack) + + // leaf removal + stack.add(goner) + removeLeaf(stack) + --size + ++modCount + stack.clear() + true + } else { + stack.clear() + false + } + } + + private fun rotateLeft(parent: Vertex, child: Vertex): Vertex { + parent.color = child.color.also { child.color = parent.color } + parent.right = child.left + child.left = parent + return child + } + + private fun rotateRight(parent: Vertex, child: Vertex): Vertex { + parent.color = child.color.also { child.color = parent.color } + parent.left = child.right + child.right = parent + return child + } + + private fun balanceAfterInsert(stack: MutableList>) { + val black = Color.BLACK + val red = Color.RED + var notEnd = true + + var vertex = stack.removeLast() + while (notEnd && stack.lastOrNull()?.color == red) { + var parent = stack.removeLast() + val grandparent = stack.removeLast() + if (grandparent.left == parent) { + if (grandparent.right?.color == red) { + grandparent.color = red + grandparent.right?.color = black + parent.color = black + vertex = grandparent + } else { + notEnd = false + if (parent.right == vertex) { + parent = rotateLeft(parent, vertex) + grandparent.left = parent + } + if (root == grandparent) { + root = rotateRight(grandparent, parent) + } else { + val ggparent = stack.last() + if (ggparent.left == grandparent) { + ggparent.left = rotateRight(grandparent, parent) + } else { + ggparent.right = rotateRight(grandparent, parent) + } + } + } + } else { + if (grandparent.left?.color == red) { + grandparent.color = red + grandparent.left?.color = black + parent.color = black + vertex = grandparent + } else { + notEnd = false + if (parent.left == vertex) { + parent = rotateRight(parent, vertex) + grandparent.right = parent + } + if (root == grandparent) { + root = rotateLeft(grandparent, parent) + } else { + val ggparent = stack.last() + if (ggparent.left == grandparent) { + ggparent.left = rotateLeft(grandparent, parent) + } else { + ggparent.right = rotateLeft(grandparent, parent) + } + } + } + } + } + + root?.color = black + } + + private fun getGoner(vertex: Vertex, parentIndex: Int, stack: MutableList>): Vertex { + return vertex.left?.let { left -> + vertex.right?.let { right -> + // swap with the vertex that is next by key + var nextParent: Vertex = vertex + stack.add(nextParent) + var next = right + var nextLeft = next.left + while (nextLeft != null) { + nextParent = next + stack.add(nextParent) + next = nextLeft + nextLeft = next.left + } + vertex.color = next.color.also { next.color = vertex.color } + if (root == vertex) { + root = next + } else { + val parent = stack[parentIndex] + if (parent.left == vertex) { + parent.left = next + } else { + parent.right = next + } + } + stack[parentIndex + 1] = next + next.left = left + vertex.left = null + vertex.right = next.right + if (nextParent == vertex) { + next.right = vertex + vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + next.right = right + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + } + } else { + next.right = right + nextParent.left = vertex + vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + nextParent.left = right + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + } + } + vertex + } ?: left.let { left -> + // swap with the single red leaf + vertex.color = left.color.also { left.color = vertex.color } + if (root == vertex) { + root = left + } else { + val parent = stack.last() + if (parent.left == vertex) { + parent.left = left + } else { + parent.right = left + } + } + left.right = vertex + vertex.left = null + vertex.right = null + stack.add(left) + vertex + } + } ?: vertex.right?.let { right -> + // swap with the single red leaf + vertex.color = right.color.also { right.color = vertex.color } + if (root == vertex) { + root = right + } else { + val parent = stack.last() + if (parent.left == vertex) { + parent.left = right + } else { + parent.right = right + } + } + right.left = vertex + vertex.left = null + vertex.right = null + stack.add(right) + vertex + } ?: vertex // vertex is already a leaf + } + + private fun removeLeaf(stack: MutableList>) { + val black = Color.BLACK + val red = Color.RED + var notEnd = true + + fun balanceLeft(parent: Vertex): Vertex { + fun balanceBlackRightBrother(parent: Vertex): Vertex { + var brother = parent.right + if (brother != null) { + var rightCousin = brother.right + val leftCousin = brother.left + + if ((rightCousin?.color ?: black) == black && (leftCousin?.color ?: black) == black) { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + return parent + } + + if ((rightCousin?.color ?: black) == black && leftCousin != null) { + rightCousin = brother + brother = rotateRight(brother, leftCousin) + parent.right = brother + } + + notEnd = false + rightCousin?.color = black + return rotateLeft(parent, brother) + } else { + throw Exception("Deletion error: height of right subtree must be at least 1") + } + } + + return parent.right?.let { right -> + if (right.color == red) { + val newParent = rotateLeft(parent, right) + newParent.left = balanceBlackRightBrother(parent) + if (notEnd) { + balanceBlackRightBrother(newParent) + } else { + newParent + } + } else { + balanceBlackRightBrother(parent) + } + } ?: balanceBlackRightBrother(parent) + } + + fun balanceRight(parent: Vertex): Vertex { + fun balanceBlackLeftBrother(parent: Vertex): Vertex { + parent.left?.let { it -> + var brother = it + var leftCousin = brother.left + val rightCousin = brother.right + + if ((leftCousin?.color ?: black) == black && (rightCousin?.color ?: black) == black) { + brother.color = red + if (parent.color == red) { + notEnd = false + parent.color = black + } + return parent + } + + if ((leftCousin?.color ?: black) == black && rightCousin != null) { + leftCousin = brother + brother = rotateLeft(brother, rightCousin) + parent.left = brother + } + + notEnd = false + leftCousin?.color = black + return rotateRight(parent, brother) + } ?: throw Exception("Deletion error: height of left subtree must be at least 1") + } + + return parent.left?.let { left -> + if (left.color == red) { + val newParent = rotateRight(parent, left) + newParent.right = balanceBlackLeftBrother(parent) + if (notEnd) { + balanceBlackLeftBrother(newParent) + } else { + newParent + } + } else { + balanceBlackLeftBrother(parent) + } + } ?: balanceBlackLeftBrother(parent) + } + + val goner = stack.removeLast() + if (goner.color == red) { + val parent = stack.last() + if (parent.left == goner) { + parent.left = null + } else { + parent.right = null + } + return + } + + // removal black leaf + if (root == goner) { + root = null + return + } + + var parent = stack.removeLast() + if (root == parent) { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + root = parent + } else { + val grandparent = stack.last() + if (grandparent.left == parent) { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + grandparent.left = parent + } else { + if (parent.left == goner) { + parent.left = null + parent = balanceLeft(parent) + } else { + parent.right = null + parent = balanceRight(parent) + } + grandparent.right = parent + } + } + stack.add(parent) + + while (notEnd && stack.size >= 2) { + val vertex = stack.removeLast() + parent = stack.removeLast() + if (root == parent) { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + root = parent + } else { + val grandparent = stack.last() + if (grandparent.left == parent) { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + grandparent.left = parent + } else { + if (parent.left == vertex) { + parent = balanceLeft(parent) + } else { + parent = balanceRight(parent) + } + grandparent.right = parent + } + } + stack.add(parent) + } + root?.color = black + } + + protected class Vertex( + override val key: K, + override var value: V, + override var color: Color = Color.RED, + override var left: Vertex? = null, + override var right: Vertex? = null + ) : PublicVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } + + protected class RedBlackTreeIterator( + root: PublicVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left + } + } + + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() + } + } + + override fun next(): PublicVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex + } + } + } +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/Vertex.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/Vertex.kt new file mode 100644 index 0000000..505378b --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/Vertex.kt @@ -0,0 +1,11 @@ +package binarysearchtrees.redblacktree + +import binarysearchtrees.MutableVertex + +interface Vertex : MutableVertex { + val color: Color + override val left: Vertex? + override val right: Vertex? + + enum class Color { RED, BLACK } +} \ No newline at end of file From e426e79ecfed902f31d51b4b74605c9ff38323d2 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 24 Apr 2023 21:32:57 +0300 Subject: [PATCH 39/87] test: Added tests for RBT. --- .../redblacktree/RBTInvarinatChecker.kt | 30 ++++ .../redblacktree/RedBlackTreeTest.kt | 165 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvarinatChecker.kt create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RedBlackTreeTest.kt diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvarinatChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvarinatChecker.kt new file mode 100644 index 0000000..c0c4459 --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvarinatChecker.kt @@ -0,0 +1,30 @@ +package binarysearchtrees.redblacktree + +fun , V> isRedBlackTree(tree: RedBlackTree): Boolean { + var f = true + + fun checkRBTInvariant(vertex: Vertex): Int { + if (vertex.color == Vertex.Color.RED) { + f = f && (vertex.left?.color != Vertex.Color.RED) + f = f && (vertex.right?.color != Vertex.Color.RED) + } + val leftBlackHeight = vertex.left?.let { + f = f && (vertex.key > it.key) + checkRBTInvariant(it) + } ?: 1 + val rightBlackHeight = vertex.right?.let { + f = f && (vertex.key < it.key) + checkRBTInvariant(it) + } ?: 1 + f = f && (leftBlackHeight == rightBlackHeight) + return if (vertex.color == Vertex.Color.BLACK) + leftBlackHeight + 1 + else leftBlackHeight + } + + return tree.getRoot()?.let { + f = (it.color == Vertex.Color.BLACK) + checkRBTInvariant(it) + f + } ?: true +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RedBlackTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RedBlackTreeTest.kt new file mode 100644 index 0000000..36d791b --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RedBlackTreeTest.kt @@ -0,0 +1,165 @@ +package binarysearchtrees.redblacktree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random + +class RedBlackTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: RedBlackTree + + @BeforeEach + fun init() { + tree = RedBlackTree() + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isRedBlackTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, 245) } + values.forEach { tree.put(it.first, it.second) } + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) + assertEquals(values.distinctBy { it.first }.size, tree.size) + } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the some element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + var size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + + value = key * 95 + tree[key] = value + size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + + @Test + fun `Function remove deletes the existing element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val elements = mutableListOf>() + values.reversed().distinctBy { it.first }.forEach { elements.add(it) } + elements.shuffle() + for (i in 0 until elements.size step 20) { + val key = elements[i].first + val value = elements[i].second + val size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + var size = tree.size - 1 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isRedBlackTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) + assertEquals(0, tree.size) + } +} \ No newline at end of file From 780e9d419b7305fa2c3f685bb8d9feb6f776b86b Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 24 Apr 2023 21:38:20 +0300 Subject: [PATCH 40/87] feat: Added initializing functions for BST interface. --- .../main/kotlin/binarysearchtrees/Trees.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt new file mode 100644 index 0000000..8f9fa43 --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/Trees.kt @@ -0,0 +1,19 @@ +package binarysearchtrees + +import binarysearchtrees.redblacktree.RedBlackTree + +// initializing functions + +fun , V> binarySearchTreeOf(): BinarySearchTree { + return RedBlackTree() +} + +fun , V> binarySearchTreeOf( + vararg args: Pair +): BinarySearchTree { + val tree = RedBlackTree() + for (it in args) { + tree.put(it.first, it.second) + } + return tree +} \ No newline at end of file From 1a9855c6b3875b81e183f62244d5b138bb8917c2 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 02:11:36 +0300 Subject: [PATCH 41/87] fix: Changed RBT size getter visibility modifier to protected. --- .../main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt index 6609f97..90437d9 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/redblacktree/RedBlackTree.kt @@ -6,7 +6,7 @@ import binarysearchtrees.redblacktree.Vertex as PublicVertex open class RedBlackTree, V> : BinarySearchTree { final override var size: Int = 0 - private set + protected set protected var root: Vertex? = null protected var modCount: Int = 0 From 5872711e774ef869763abd0c96cd2fbca46ffa43 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 02:13:32 +0300 Subject: [PATCH 42/87] feat: Added a dependencies to use org.neo4j.ogm. --- app/build.gradle.kts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fc4f6de..440a606 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("org.jetbrains.kotlin.jvm") version "1.8.10" + id("org.jetbrains.kotlin.plugin.noarg") version "1.8.20" jacoco application } @@ -14,9 +15,18 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.9.2")) testImplementation("org.junit.jupiter:junit-jupiter") + + implementation("org.neo4j:neo4j-ogm-core:4.0.5") + implementation("org.neo4j:neo4j-ogm-bolt-driver:4.0.5") + implementation(project(":BinarySearchTrees")) } +noArg { + annotation("org.neo4j.ogm.annotation.NodeEntity") + annotation("org.neo4j.ogm.annotation.RelationshipEntity") +} + tasks.test { finalizedBy("jacocoTestReport") useJUnitPlatform() From 919c1b16d9480fedc3a49d050975f072cf39a4fa Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 02:37:30 +0300 Subject: [PATCH 43/87] feat: Added a dependency for SLF4J providers. --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 440a606..4a58060 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation("org.neo4j:neo4j-ogm-core:4.0.5") implementation("org.neo4j:neo4j-ogm-bolt-driver:4.0.5") + implementation("org.slf4j:slf4j-simple:2.0.0") implementation(project(":BinarySearchTrees")) } From e7ce16f051effe35cc980d0f684be6d3d2f49a21 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 02:44:34 +0300 Subject: [PATCH 44/87] feat: Added Compose file with neo4j for Docker Compose. --- docker-compose.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..78833bb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.9" + +services: + neo4j: + image: neo4j:4.0.5 + container_name: neo4j + ports: + - "7474:7474" + - "7687:7687" + environment: + - NEO4J_AUTH=neo4j/qwerty \ No newline at end of file From 04cdd65441caa21cf73dc21a1bde11790a7b2d88 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 02:47:05 +0300 Subject: [PATCH 45/87] feat: Added RBT repository to save RBT to neo4j database. --- .../main/kotlin/app/model/repos/RBTRepo.kt | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 app/src/main/kotlin/app/model/repos/RBTRepo.kt diff --git a/app/src/main/kotlin/app/model/repos/RBTRepo.kt b/app/src/main/kotlin/app/model/repos/RBTRepo.kt new file mode 100644 index 0000000..edd4317 --- /dev/null +++ b/app/src/main/kotlin/app/model/repos/RBTRepo.kt @@ -0,0 +1,116 @@ +package app.model.repos + +import binarysearchtrees.redblacktree.RedBlackTree +import binarysearchtrees.redblacktree.Vertex +import binarysearchtrees.redblacktree.Vertex.Color +import org.neo4j.ogm.annotation.* +import org.neo4j.ogm.config.Configuration +import org.neo4j.ogm.session.SessionFactory + +class RBTRepo( + configuration: Configuration, + private val serializeValue: (ValueType) -> String, + private val deserializeValue: (String) -> ValueType +) { + private val sessionFactory = SessionFactory(configuration, "app.model.repos") + private val session = sessionFactory.openSession() + + fun getNames(): List { + return session.loadAll( + TreeEntity::class.java, + 0 + ).map { it.name } + } + + fun get(name: String): Pair, String> { + val treeEntity = session.load( + TreeEntity::class.java, + name, + -1 + ) + return RBT().apply { + treeEntity.root?.let { buildTree(it, deserializeValue) } + } to treeEntity.settingsData + } + + fun set(name: String, tree: RedBlackTree, settingsData: String) { + remove(name) + session.save( + TreeEntity( + name, + settingsData, + tree.getRoot()?.toVertexEntity(serializeValue) + ) + ) + } + + fun remove(name: String): Boolean { + return session.query( + "MATCH r = (t:Tree{name : \$NAME})-[*]->() DETACH DELETE r", + mapOf("NAME" to name) + ).queryStatistics().containsUpdates() + } + + private fun Vertex.toVertexEntity(serializeValue: (ValueType) -> String): VertexEntity { + return VertexEntity( + key, + serializeValue(value), + color.toString()[0], + left?.toVertexEntity(serializeValue), + right?.toVertexEntity(serializeValue) + ) + } +} + +@NodeEntity("Tree") +data class TreeEntity( + @Id + val name: String, + + @Property + val settingsData: String, + + @Relationship(type = "ROOT") + val root: VertexEntity? +) + +@NodeEntity("Vertex") +data class VertexEntity( + @Property + val key: String, + + @Property + val value: String, + + @Property + val color: Char, + + @Relationship(type = "LEFT") + val left: VertexEntity?, + + @Relationship(type = "RIGHT") + val right: VertexEntity?, +) { + @Id + @GeneratedValue + var id: Long? = null +} + +private class RBT : RedBlackTree() { + fun buildTree(vertexEntity: VertexEntity, deserializeValue: (String) -> ValueType) { + root = vertexEntity.toVertex(deserializeValue) + } + + private fun VertexEntity.toVertex(deserializeValue: (String) -> ValueType): Vertex { + val vertex = Vertex( + key, + deserializeValue(value), + if (color == 'R') Color.RED else Color.BLACK + ) + ++size + ++modCount + vertex.left = left?.toVertex(deserializeValue) + vertex.right = right?.toVertex(deserializeValue) + return vertex + } +} \ No newline at end of file From 2bb8908d5efe1ab43457e650993889ccb9973742 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 03:30:52 +0300 Subject: [PATCH 46/87] refactor: Renamed RBT repository class. --- .../kotlin/app/model/repos/{RBTRepo.kt => RBTRepository.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/main/kotlin/app/model/repos/{RBTRepo.kt => RBTRepository.kt} (98%) diff --git a/app/src/main/kotlin/app/model/repos/RBTRepo.kt b/app/src/main/kotlin/app/model/repos/RBTRepository.kt similarity index 98% rename from app/src/main/kotlin/app/model/repos/RBTRepo.kt rename to app/src/main/kotlin/app/model/repos/RBTRepository.kt index edd4317..9846bd6 100644 --- a/app/src/main/kotlin/app/model/repos/RBTRepo.kt +++ b/app/src/main/kotlin/app/model/repos/RBTRepository.kt @@ -7,7 +7,7 @@ import org.neo4j.ogm.annotation.* import org.neo4j.ogm.config.Configuration import org.neo4j.ogm.session.SessionFactory -class RBTRepo( +class RBTRepository( configuration: Configuration, private val serializeValue: (ValueType) -> String, private val deserializeValue: (String) -> ValueType From 3c03266a809c22f38c51ad6d8e555814a2f87f18 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 01:54:26 +0300 Subject: [PATCH 47/87] fix: Changed BST size getter visibility modifier to protected. --- .../binarysearchtree/SimpleBinarySearchTree.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt index a10fca2..c46b159 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/binarysearchtree/SimpleBinarySearchTree.kt @@ -5,7 +5,7 @@ import binarysearchtrees.binarysearchtree.Vertex as PublicVertex open class SimpleBinarySearchTree, V> : BinarySearchTree { final override var size: Int = 0 - private set + protected set protected var root: Vertex? = null protected var modCount: Int = 0 From 066be9ac788ab15ac4740d1457c1cb85035f509e Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 01:55:45 +0300 Subject: [PATCH 48/87] feat: Added a dependency to use kotlinx.serialization. --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fc4f6de..6d0f0f6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("org.jetbrains.kotlin.jvm") version "1.8.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.8.20" jacoco application } @@ -14,6 +15,8 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.9.2")) testImplementation("org.junit.jupiter:junit-jupiter") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") + implementation(project(":BinarySearchTrees")) } From 34af52d53cc5aca110e749c808ac6d1c17f6c63f Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sat, 29 Apr 2023 01:57:42 +0300 Subject: [PATCH 49/87] feat: Added BST repository to save BST to flat files. --- .../main/kotlin/app/model/repos/BSTRepo.kt | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 app/src/main/kotlin/app/model/repos/BSTRepo.kt diff --git a/app/src/main/kotlin/app/model/repos/BSTRepo.kt b/app/src/main/kotlin/app/model/repos/BSTRepo.kt new file mode 100644 index 0000000..6580177 --- /dev/null +++ b/app/src/main/kotlin/app/model/repos/BSTRepo.kt @@ -0,0 +1,78 @@ +package app.model.repos + +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.binarysearchtree.Vertex +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File + +class BSTRepository( + private val dirPath: String, + private val serializeValue: (ValueType) -> String, + private val deserializeValue: (String) -> ValueType +) { + init { + File(dirPath).mkdirs() + } + + fun getNames(): List { + return File(dirPath).listFiles { it -> + it.name.endsWith(".json") + }?.map { it.name.dropLast(5) } ?: listOf() + } + + fun get(name: String): Pair, String> { + val jsonTree = Json.decodeFromString(File(dirPath, "$name.json").readText()) + return BST().apply { + jsonTree.root?.let { buildTree(it, deserializeValue) } + } to jsonTree.settingsData + } + + fun set(name: String, tree: SimpleBinarySearchTree, settingsData: String) { + val file = File(dirPath, "$name.json") + file.createNewFile() + file.writeText(Json.encodeToString(JsonTree(settingsData, tree.getRoot()?.toJsonNode(serializeValue)))) + } + + fun remove(name: String): Boolean = File(dirPath, "$name.json").delete() + + private fun Vertex.toJsonNode(serializeValue: (ValueType) -> String): JsonVertex { + return JsonVertex( + key, + serializeValue(value), + left?.toJsonNode(serializeValue), + right?.toJsonNode(serializeValue) + ) + } +} + +@Serializable +data class JsonTree( + val settingsData: String, + val root: JsonVertex? +) + +@Serializable +data class JsonVertex( + val key: String, + val value: String, + val left: JsonVertex?, + val right: JsonVertex?, +) + +private class BST : SimpleBinarySearchTree() { + fun buildTree(jsonVertex: JsonVertex, deserializeValue: (String) -> ValueType) { + root = jsonVertex.toVertex(deserializeValue) + } + + private fun JsonVertex.toVertex(deserializeValue: (String) -> ValueType): Vertex { + val vertex = Vertex(key, deserializeValue(value)) + ++size + ++modCount + vertex.left = left?.toVertex(deserializeValue) + vertex.right = right?.toVertex(deserializeValue) + return vertex + } +} \ No newline at end of file From e9f17505096572a13e0a4a455ed8c23ededab79b Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sun, 30 Apr 2023 20:49:41 +0300 Subject: [PATCH 50/87] refactor: Renamed RBT repository implementation file. --- .../main/kotlin/app/model/repos/{RBTRepository.kt => RBTRepo.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/kotlin/app/model/repos/{RBTRepository.kt => RBTRepo.kt} (100%) diff --git a/app/src/main/kotlin/app/model/repos/RBTRepository.kt b/app/src/main/kotlin/app/model/repos/RBTRepo.kt similarity index 100% rename from app/src/main/kotlin/app/model/repos/RBTRepository.kt rename to app/src/main/kotlin/app/model/repos/RBTRepo.kt From 78096b14db4a1c23cf4158960ce44400e0c24ce2 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Sun, 30 Apr 2023 20:53:19 +0300 Subject: [PATCH 51/87] refactor: Renamed RBT and BST checkers implementation files. --- .../{BSTInvarinatChecker.kt => BSTInvariantChecker.kt} | 0 .../{RBTInvarinatChecker.kt => RBTInvariantChecker.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/{BSTInvarinatChecker.kt => BSTInvariantChecker.kt} (100%) rename BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/{RBTInvarinatChecker.kt => RBTInvariantChecker.kt} (100%) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvarinatChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvariantChecker.kt similarity index 100% rename from BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvarinatChecker.kt rename to BinarySearchTrees/src/test/kotlin/binarysearchtrees/binarysearchtree/BSTInvariantChecker.kt diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvarinatChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvariantChecker.kt similarity index 100% rename from BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvarinatChecker.kt rename to BinarySearchTrees/src/test/kotlin/binarysearchtrees/redblacktree/RBTInvariantChecker.kt From 6fa3828f80fed4abc125db85f9d47af92316978b Mon Sep 17 00:00:00 2001 From: Arsene Baitenov <91395485+Arsene-Baitenov@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:50:39 +0300 Subject: [PATCH 52/87] test: Added exclusion from Jacoco Report --- BinarySearchTrees/build.gradle.kts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BinarySearchTrees/build.gradle.kts b/BinarySearchTrees/build.gradle.kts index 11270ce..dde70d0 100644 --- a/BinarySearchTrees/build.gradle.kts +++ b/BinarySearchTrees/build.gradle.kts @@ -27,6 +27,13 @@ tasks.test { tasks.named("jacocoTestReport") { dependsOn(tasks.test) + + classDirectories.setFrom(files(classDirectories.files.map { + fileTree(it) { + exclude("**/binarysearchtrees/TreesKt.*") + } + })) + reports { xml.required.set(false) html.required.set(true) @@ -34,4 +41,4 @@ tasks.named("jacocoTestReport") { csv.required.set(true) csv.outputLocation.set(file("${buildDir}/jacoco/report.csv")) } -} \ No newline at end of file +} From 6394415039f9280e2ad688cd0b59eaad9577693a Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 30 Apr 2023 23:25:31 +0300 Subject: [PATCH 53/87] ci: Added mergeable.yml file for pull requests. --- .github/mergeable.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/mergeable.yml diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..8f61760 --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,25 @@ +version: 2 +mergeable: + - when: pull_request.*, pull_request_review.* + validate: + - do: title + no_empty: + enabled: true + message: "The title should not be empty." + begins_with: + match: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] + message: "The name must begin with a capital letter." + ends_with: + match: '.' + message: "The name must end with a dot." + + - do: description + no_empty: + enabled: true + message: "The description should not be empty." + + - do: approvals + min: + count: 1 + required: + assignees: true \ No newline at end of file From 169a585df7aa1b71a7459c1cb12056dab844c746 Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 30 Apr 2023 23:27:27 +0300 Subject: [PATCH 54/87] fix: Deleted invalid rule for title. --- .github/mergeable.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/mergeable.yml b/.github/mergeable.yml index 8f61760..3a1438a 100644 --- a/.github/mergeable.yml +++ b/.github/mergeable.yml @@ -9,9 +9,6 @@ mergeable: begins_with: match: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] message: "The name must begin with a capital letter." - ends_with: - match: '.' - message: "The name must end with a dot." - do: description no_empty: From 7f511957cee26db021969c247b0a962f330597b7 Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 30 Apr 2023 23:58:09 +0300 Subject: [PATCH 55/87] fix: Added ignore rule for feedback. --- .github/mergeable.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/mergeable.yml b/.github/mergeable.yml index 3a1438a..21fcfb0 100644 --- a/.github/mergeable.yml +++ b/.github/mergeable.yml @@ -1,6 +1,13 @@ version: 2 mergeable: - when: pull_request.*, pull_request_review.* + filter: + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none validate: - do: title no_empty: From 21b226a59dbda51af4f581c6bce2920e1dd1b069 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 1 May 2023 20:34:47 +0300 Subject: [PATCH 56/87] Updated logo in README.md file. --- README.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d323773..a03eb5d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
- Logo + Logo

Search Trees Project

@@ -38,7 +38,7 @@
  • Clone Repository
  • -
  • Roadmap
  • +
  • App Usage
  • License
  • Contact
  • Acknowledgments
  • @@ -87,16 +87,10 @@ To start working with our development, you need to clone repository: git clone https://github.com/spbu-coding-2022/trees-12.git ``` - -## Roadmap + +## App Usage -- [x] Add LICENSE -- [x] Add README -- [ ] Add Gradle BM -- [ ] Develop Trees Interfaces -- [ ] Develop Trees Realisation -- [ ] Develop User Interface -- [ ] Develop CI +- Some tips about app usage.

    (Back to top)

    @@ -135,5 +129,7 @@ The resources that we used to get information about binary search trees, their f * [Gradle Documentation](https://docs.gradle.org/current/userguide/userguide.html) * [JUnit Documentation](https://junit.org/junit5/docs/current/user-guide/) * [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +* [Mergeable Documentation](https://mergeable.readthedocs.io/en/latest/configuration.html#basics) +* [Compose Documentation](https://developer.android.com/jetpack/compose/documentation) -

    (Back to top)

    +

    (Back to top)

    \ No newline at end of file From aa97ab0211aba1bb83e64a02c48b78ac874f03c7 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 2 May 2023 11:48:07 +0300 Subject: [PATCH 57/87] docs: Updated -Getting Started- block. --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a03eb5d..cf42847 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ AVL, Red-Black and Binary Search Trees models.

    - View Demo - · Report Bug · Request Feature @@ -71,6 +69,10 @@ Technologies used to develop the project: * [![gradle](https://img.shields.io/badge/gradle-FFFFFF?style=for-the-badge&logo=gradle&logoColor=black&)](https://gradle.org/) * [![gradle](https://img.shields.io/badge/kotlin-FFFFFF?style=for-the-badge&logo=kotlin&logoColor=black&)](https://kotlinlang.org/) * [![gradle](https://img.shields.io/badge/junit-FFFFFF?style=for-the-badge&logo=junit&logoColor=black&)](https://junit.org/) +* [![gradle](https://img.shields.io/badge/neo4j-FFFFFF?style=for-the-badge&logo=neo4j&logoColor=black&)](https://neo4j.com) +* [![gradle](https://img.shields.io/badge/sqlite-FFFFFF?style=for-the-badge&logo=sqlite&logoColor=black&)](https://www.sqlite.org/index.html) +* [![gradle](https://img.shields.io/badge/docker-FFFFFF?style=for-the-badge&logo=docker&logoColor=black&)](https://www.docker.com) +* [![gradle](https://img.shields.io/badge/compose-FFFFFF?style=for-the-badge&logo=compose&logoColor=black&)](https://www.jetbrains.com/ru-ru/lp/compose-multiplatform/)

    (Back to top)

    @@ -87,6 +89,52 @@ To start working with our development, you need to clone repository: git clone https://github.com/spbu-coding-2022/trees-12.git ``` +To initialize the library and start working with it, you need to know the following lines: + +* Initializing BinarySearchTree (default RedBlackTree): + + ```kotlin + val tree = binarySearchTreeOf() + ``` + +* Initializing simple BinarySearchTree: + + ```kotlin + val tree = SimpleBinarySearchTree() + ``` + +* Initializing RedBlackTree: + + ```kotlin + val tree = RedBlackTree() + ``` + +* Initializing AVLTree: + + ```kotlin + val tree = AVLTree() + ``` + +To work with trees, you also need to know the management commands: + +* Inserting a value by key: + + ```kotlin + tree[key] = value + ``` + +* Getting a value by key: + + ```kotlin + val value = tree[key] + ``` + +* Deleting a value by key: + + ```kotlin + tree.remove(key) + ``` + ## App Usage @@ -131,5 +179,6 @@ The resources that we used to get information about binary search trees, their f * [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) * [Mergeable Documentation](https://mergeable.readthedocs.io/en/latest/configuration.html#basics) * [Compose Documentation](https://developer.android.com/jetpack/compose/documentation) +* [Docker Desktop](https://docs.docker.com/desktop/)

    (Back to top)

    \ No newline at end of file From b904f4850be74b137f6b7bafb6a976e81e5a6516 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 2 May 2023 11:55:25 +0300 Subject: [PATCH 58/87] docs: Updated -App Usage- block. --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf42847..d5bd65f 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,21 @@ To work with trees, you also need to know the management commands: ## App Usage -- Some tips about app usage. +Before launching the application, you need to run "neo4j" via "docker": + +* This is done by the command in project repository: + +```sh +docker compose up -d +``` + +You also need to build the application and run it: + +* This is done by the command: + +```sh +./gradlew run +```

    (Back to top)

    From c4deb8262cd6dc83df1538ae6f7685c36b38b821 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 2 May 2023 12:03:06 +0300 Subject: [PATCH 59/87] docs: Added start window to -App Usage- block. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d5bd65f..482f307 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,12 @@ You also need to build the application and run it: ./gradlew run ``` +A little bit about the user interface: + +* After launching the application, a window will appear in which you can select the desired type of tree, as well as previously saved models of this type. + +Start +

    (Back to top)

    From a1947e5a6d103df7bdbd64ceea96beb640987bd9 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 2 May 2023 12:04:07 +0300 Subject: [PATCH 60/87] docs: Changed start window size in -App Usage- block. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 482f307..3185387 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ A little bit about the user interface: * After launching the application, a window will appear in which you can select the desired type of tree, as well as previously saved models of this type. -Start +Start

    (Back to top)

    From af1c202e660678cfaef3623d663dbfd1963b00a6 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 2 May 2023 12:10:58 +0300 Subject: [PATCH 61/87] docs: Added main draw window to -App Usage- block. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3185387..bb1cbff 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,11 @@ A little bit about the user interface: * After launching the application, a window will appear in which you can select the desired type of tree, as well as previously saved models of this type. -Start +Start + +* After you select the tree, a window will appear with functional buttons for adding, deleting and searching for values by key. Also, above the buttons you can see the type of the selected tree, and below them there will be buttons for saving and deleting the current model. + +Main

    (Back to top)

    From 3e5996c6249338f4c379f4442ac464700fe77913 Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Tue, 2 May 2023 12:30:32 +0300 Subject: [PATCH 62/87] feat: Added AVL-Vertex class. --- .../main/kotlin/binarysearchtrees/avltree/Vertex.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt new file mode 100644 index 0000000..ac1564b --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt @@ -0,0 +1,10 @@ +package binarysearchtrees.avltree + +import binarysearchtrees.MutableVertex + +interface Vertex : MutableVertex { + var balance: Int + + override val left: Vertex? + override val right: Vertex? +} \ No newline at end of file From df26d304a1b0c0f0e69c8b733e54ab67d3476b39 Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Tue, 2 May 2023 12:31:47 +0300 Subject: [PATCH 63/87] feat: Developed tests for AVL-Tree. --- .../binarysearchtrees/avltree/AVLTreeTest.kt | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt new file mode 100644 index 0000000..e562147 --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt @@ -0,0 +1,165 @@ +package binarysearchtrees.avltree + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random + +class AVLTreeTest { + private val randomizer = Random(100) + private val elementsCount = 1000 + private val values = Array(elementsCount) { Pair(randomizer.nextInt(), randomizer.nextInt()) } + private lateinit var tree: AVLTree + + @BeforeEach + fun init() { + tree = AVLTree() + } + + @Test + fun `Function put doesn't violate the invariant`() { + values.forEach { + tree.put(it.first, it.second) + assertTrue(isAVLTree(tree)) + } + } + + @Test + fun `Function put adds all elements with unique keys`() { + values.forEach { tree.put(it.first, 245) } + values.forEach { tree.put(it.first, it.second) } + val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } + assertEquals( + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue + ) + assertEquals(values.distinctBy { it.first }.size, tree.size) + } + + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "Function get returns correct value for key {0}") + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function get returns correct value`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + val expected = key * 198 + tree[key] = expected + assertEquals(expected, tree.get(key)) + + tree.remove(key) + assertEquals(null, tree.get(key)) + } + + @ParameterizedTest + @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) + fun `Function remove deletes the some element correctly`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + + var value = key * 198 + tree[key] = value + var size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + + value = key * 95 + tree[key] = value + size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + } + + @Test + fun `Function remove deletes the existing element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val elements = mutableListOf>() + values.reversed().distinctBy { it.first }.forEach { elements.add(it) } + elements.shuffle() + for (i in 0 until elements.size step 20) { + val key = elements[i].first + val value = elements[i].second + val size = tree.size - 1 + assertEquals(value, tree.remove(key)) + assertEquals(null, tree[key]) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + } + } + + @Test + fun `Function remove deletes the root element correctly`() { + values.forEach { tree.put(it.first, it.second) } + + val value = 45 + var oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + var size = tree.size - 1 + assertEquals(value, tree.remove(oldKey)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + + oldKey = tree.getRoot()?.let { + it.setValue(value) + it.key + } ?: -25 + size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) + assertNotEquals(oldKey, tree.getRoot()?.key) + assertEquals(size, tree.size) + assertTrue(isAVLTree(tree)) + } + + @Test + fun `Function clear makes tree empty`() { + values.forEach { tree.put(it.first, it.second) } + + tree.clear() + assertTrue(tree.isEmpty()) + assertNull(tree.getRoot()) + assertEquals(0, tree.size) + } +} \ No newline at end of file From 7dc6ca45abfac14c0502955dbb5717ceea21fac5 Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Wed, 3 May 2023 01:04:58 +0300 Subject: [PATCH 64/87] feat: Developed AVL-Tree model. --- .../binarysearchtrees/avltree/AVLTree.kt | 201 ++++++++++++++++++ .../binarysearchtrees/avltree/Vertex.kt | 2 - 2 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt new file mode 100644 index 0000000..3cbfe1d --- /dev/null +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt @@ -0,0 +1,201 @@ +package binarysearchtrees.avltree + +import kotlin.math.max + +open class AVLTree, V>(vararg init: Pair) { + + private var root: Vertex? = null + + init { + init.forEach { + insert(it.first, it.second) + } + } + + fun getRoot(): Vertex? = root + + fun isEmpty(): Boolean = (root == null) + + private fun insert(key: K, value: V) { + root = add(root, Vertex(key, value, 1)) + } + + private fun add(vertex: Vertex?, newVertex: Vertex): Vertex? { + if (vertex == null) { + return newVertex + } + + when { + newVertex.key < vertex.key -> { + vertex.left = add(vertex.left, newVertex) + } + newVertex.key > vertex.key -> { + vertex.right = add(vertex.right, newVertex) + } + } + + val heightLeft = vertex.leftHeight + val heightRight = vertex.rightHeight + val balance = vertex.balance + + val leftGreater = balance > 1 + val rightGreater = balance < -1 + + vertex.height = 1 + max(heightLeft, heightRight) + + if (leftGreater && newVertex.key < vertex.left!!.key) { + return vertex.rightRotate() + } + + if (rightGreater && newVertex.key > vertex.right!!.key) { + return vertex.leftRotate() + } + + if (leftGreater && newVertex.key > vertex.left!!.key) { + vertex.left = vertex.left!!.leftRotate() + + return vertex.rightRotate() + } + + if (rightGreater && newVertex.key < vertex.right!!.key) { + vertex.right = vertex.right!!.rightRotate() + + return vertex.leftRotate() + } + + return vertex + } + + fun delete(key: K) { + delete(key, root) + } + + private fun delete(key: K, tree: Vertex?) { + if (tree == null) { + return + } + + val visited = mutableListOf>() + val vertex = search(key, tree) { + visited.add(it) + } ?: return + + val parent = when { + visited.size >= 2 -> visited.elementAt(visited.size - 2) + vertex == tree -> tree + else -> null + } + + if (vertex.left == null && vertex.right == null) { + if (vertex == root && tree == root) root = null else parent?.removeChild(vertex) + } else if (vertex.left == null) { + if (vertex == root && tree == root) root = vertex.right else parent?.replaceChild(vertex, vertex.right) + } else if (vertex.right == null) { + if (vertex == root && tree == root) root = vertex.left else parent?.replaceChild(vertex, vertex.left) + } else { + val min = vertex.right!!.min() + val newNode = Vertex(min.key, min.value, vertex.height) + newNode.left = vertex.left + newNode.right = vertex.right + newNode.height = vertex.height - 1 + + if (vertex == root && tree == root) root = newNode else parent?.replaceChild(vertex, newNode) + + delete(min.key, vertex.right) + } + } + + fun search(key: K, visited: ((K) -> Unit)? = null): V? { + return search(key, root) { visited?.invoke(it.key) }?.value + } + + private fun search(key: K, vertex: Vertex?, visited: ((Vertex) -> Unit)? = null): Vertex? { + if (vertex == null) { + return null + } + + visited?.let { it(vertex) } + + if (key < vertex.key) { + return search(key, vertex.left, visited) + } else if (key > vertex.key) { + return search(key, vertex.right, visited) + } + + return vertex + } + + open class Vertex, V>(val key: K, val value: V, var height: Int) { + var left: Vertex? = null + var right: Vertex? = null + + val leftHeight + get() = left?.height ?: 0 + + val rightHeight + get() = right?.height ?: 0 + + val balance + get() = leftHeight - rightHeight + + override fun toString() = "$key" + + override fun equals(other: Any?) = if (other is Vertex<*, *>) other.key == key else false + + private fun updateHeight() { + height = 1 + max(leftHeight, rightHeight) + } + + fun leftRotate(): Vertex? { + val other = right ?: return this + + other.left = this.also { it.right = other.left } + + updateHeight() + other.updateHeight() + + return other + } + + fun rightRotate(): Vertex? { + val other = this.left ?: return this + + other.right = this.also { it.left = other.right } + + updateHeight() + other.updateHeight() + + return other + } + + fun removeChild(vertex: Vertex) { + if (right == vertex) { + right = null + } else if (left == vertex) { + left = null + } + } + + fun replaceChild(vertex: Vertex, with: Vertex?) { + if (right == vertex) { + right = with + } else if (left == vertex) { + left = with + } + } + + fun min(): Vertex { + return if (left == null) { + this + } else { + left!!.min() + } + } + + override fun hashCode() = key.hashCode() + } +} + +fun , V> AVLTree(items: Map): AVLTree { + return AVLTree(*items.map { Pair(it.key, it.value) }.toTypedArray()) +} \ No newline at end of file diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt index ac1564b..0caeebf 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/Vertex.kt @@ -3,8 +3,6 @@ package binarysearchtrees.avltree import binarysearchtrees.MutableVertex interface Vertex : MutableVertex { - var balance: Int - override val left: Vertex? override val right: Vertex? } \ No newline at end of file From 636887dcc00e39a265d6b8ca25a15e4cbe68684c Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Wed, 3 May 2023 01:06:04 +0300 Subject: [PATCH 65/87] feat: Added AVL-InvariantChecker for tests. --- .../binarysearchtrees/avltree/AVLInvariantChecker.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt new file mode 100644 index 0000000..d8e3413 --- /dev/null +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt @@ -0,0 +1,9 @@ +package binarysearchtrees.avltree + +fun , V> isAVLTree(tree: AVLTree): Boolean { + fun checkAVLInvariant(vertex: AVLTree.Vertex): Boolean { + return vertex.left?.let { vertex.key > it.key && checkAVLInvariant(it) } ?: true + && vertex.right?.let { vertex.key < it.key && checkAVLInvariant(it) } ?: true + } + return tree.getRoot()?.let { checkAVLInvariant(it) } ?: true +} From c7f5ececd710b9bd52b0116e7f09eef9d34061cb Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Wed, 3 May 2023 01:46:03 +0300 Subject: [PATCH 66/87] fix: Refactored AVL-Tree & tests. --- .../binarysearchtrees/avltree/AVLTree.kt | 34 +++++++-- .../binarysearchtrees/avltree/AVLTreeTest.kt | 71 +++++-------------- 2 files changed, 46 insertions(+), 59 deletions(-) diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt index 3cbfe1d..cb4018b 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt @@ -1,9 +1,12 @@ package binarysearchtrees.avltree import kotlin.math.max +import kotlin.math.min open class AVLTree, V>(vararg init: Pair) { - + + var size: Int = 0 + private var root: Vertex? = null init { @@ -16,7 +19,16 @@ open class AVLTree, V>(vararg init: Pair) { fun isEmpty(): Boolean = (root == null) - private fun insert(key: K, value: V) { + fun clear() { + size = 0 + root = null + } + + operator fun set(key: K, value: V) { + insert(key, value) + } + + fun insert(key: K, value: V) { root = add(root, Vertex(key, value, 1)) } @@ -63,6 +75,7 @@ open class AVLTree, V>(vararg init: Pair) { return vertex.leftRotate() } + ++size return vertex } @@ -70,7 +83,7 @@ open class AVLTree, V>(vararg init: Pair) { delete(key, root) } - private fun delete(key: K, tree: Vertex?) { + fun delete(key: K, tree: Vertex?) { if (tree == null) { return } @@ -102,9 +115,22 @@ open class AVLTree, V>(vararg init: Pair) { if (vertex == root && tree == root) root = newNode else parent?.replaceChild(vertex, newNode) delete(min.key, vertex.right) + --size } } + operator fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } + } + return vertex?.value + } + fun search(key: K, visited: ((K) -> Unit)? = null): V? { return search(key, root) { visited?.invoke(it.key) }?.value } @@ -138,8 +164,6 @@ open class AVLTree, V>(vararg init: Pair) { val balance get() = leftHeight - rightHeight - override fun toString() = "$key" - override fun equals(other: Any?) = if (other is Vertex<*, *>) other.key == key else false private fun updateHeight() { diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt index e562147..4ca31d3 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt @@ -21,19 +21,16 @@ class AVLTreeTest { @Test fun `Function put doesn't violate the invariant`() { values.forEach { - tree.put(it.first, it.second) + tree.insert(it.first, it.second) assertTrue(isAVLTree(tree)) } } @Test fun `Function put adds all elements with unique keys`() { - values.forEach { tree.put(it.first, 245) } - values.forEach { tree.put(it.first, it.second) } + values.forEach { tree.insert(it.first, 245) } + values.forEach { tree.insert(it.first, it.second) } val listOfPairKeyValue = mutableListOf>() - for (it in tree) { - listOfPairKeyValue.add(Pair(it.key, it.value)) - } assertEquals( values.reversed().distinctBy { it.first }.sortedBy { it.first }, listOfPairKeyValue @@ -41,59 +38,29 @@ class AVLTreeTest { assertEquals(values.distinctBy { it.first }.size, tree.size) } - @ParameterizedTest - @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) - fun `Functions of iterator throws exceptions after change tree`(key: Int) { - values.forEach { tree.put(it.first, it.second) } - tree.put(key, 69) - - var iterator = tree.iterator() - tree.remove(key) - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - tree.put(key, key * 100) - assertThrows(ConcurrentModificationException::class.java) { iterator.next() } - - iterator = tree.iterator() - tree.remove(key, key * 100) - assertThrows(ConcurrentModificationException::class.java) { iterator.next() } - - iterator = tree.iterator() - tree[key] = key * 100 - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - tree.clear() - assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } - - iterator = tree.iterator() - assertThrows(NoSuchElementException::class.java) { iterator.next() } - } - @ParameterizedTest(name = "Function get returns correct value for key {0}") @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) fun `Function get returns correct value`(key: Int) { - values.forEach { tree.put(it.first, it.second) } + values.forEach { tree.insert(it.first, it.second) } val expected = key * 198 tree[key] = expected - assertEquals(expected, tree.get(key)) + assertEquals(expected, tree.search(key)) - tree.remove(key) - assertEquals(null, tree.get(key)) + tree.delete(key) + assertEquals(null, tree.search(key)) } @ParameterizedTest @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) fun `Function remove deletes the some element correctly`(key: Int) { - values.forEach { tree.put(it.first, it.second) } + values.forEach { tree.insert(it.first, it.second) } var value = key * 198 tree[key] = value var size = tree.size - 1 - assertEquals(value, tree.remove(key)) - assertEquals(null, tree.remove(key)) + assertEquals(value, tree.delete(key)) + assertEquals(null, tree.delete(key)) assertEquals(null, tree[key]) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -101,9 +68,6 @@ class AVLTreeTest { value = key * 95 tree[key] = value size = tree.size - 1 - assertFalse(tree.remove(key, value + 10)) - assertTrue(tree.remove(key, value)) - assertFalse(tree.remove(key, value)) assertEquals(null, tree[key]) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -111,7 +75,7 @@ class AVLTreeTest { @Test fun `Function remove deletes the existing element correctly`() { - values.forEach { tree.put(it.first, it.second) } + values.forEach { tree.insert(it.first, it.second) } val elements = mutableListOf>() values.reversed().distinctBy { it.first }.forEach { elements.add(it) } @@ -120,7 +84,7 @@ class AVLTreeTest { val key = elements[i].first val value = elements[i].second val size = tree.size - 1 - assertEquals(value, tree.remove(key)) + assertEquals(value, tree.delete(key)) assertEquals(null, tree[key]) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -129,25 +93,24 @@ class AVLTreeTest { @Test fun `Function remove deletes the root element correctly`() { - values.forEach { tree.put(it.first, it.second) } + values.forEach { tree.insert(it.first, it.second) } val value = 45 var oldKey = tree.getRoot()?.let { - it.setValue(value) + it.value it.key } ?: -25 var size = tree.size - 1 - assertEquals(value, tree.remove(oldKey)) + assertEquals(value, tree.delete(oldKey)) assertNotEquals(oldKey, tree.getRoot()?.key) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) oldKey = tree.getRoot()?.let { - it.setValue(value) + it.value it.key } ?: -25 size = tree.size - 1 - assertTrue(tree.remove(oldKey, value)) assertNotEquals(oldKey, tree.getRoot()?.key) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -155,7 +118,7 @@ class AVLTreeTest { @Test fun `Function clear makes tree empty`() { - values.forEach { tree.put(it.first, it.second) } + values.forEach { tree.insert(it.first, it.second) } tree.clear() assertTrue(tree.isEmpty()) From 680419b398faf8ef2cb6d0e30e88a494902d302d Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 4 Jun 2023 18:24:40 +0300 Subject: [PATCH 67/87] fix: Fixed avl-tree architecture. --- .../binarysearchtrees/avltree/AVLTree.kt | 361 ++++++++++-------- .../avltree/AVLInvariantChecker.kt | 4 +- .../binarysearchtrees/avltree/AVLTreeTest.kt | 74 +++- .../main/kotlin/app/model/repos/AVLRepo.kt | 0 4 files changed, 260 insertions(+), 179 deletions(-) create mode 100644 app/src/main/kotlin/app/model/repos/AVLRepo.kt diff --git a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt index cb4018b..84f301e 100644 --- a/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt +++ b/BinarySearchTrees/src/main/kotlin/binarysearchtrees/avltree/AVLTree.kt @@ -1,225 +1,270 @@ package binarysearchtrees.avltree -import kotlin.math.max -import kotlin.math.min +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.MutableVertex +import binarysearchtrees.avltree.AVLTree +import binarysearchtrees.avltree.Vertex +import binarysearchtrees.avltree.Vertex as PublicVertex -open class AVLTree, V>(vararg init: Pair) { +open class AVLTree, V> : BinarySearchTree { + final override var size: Int = 0 + protected set + protected var root: AVLVertex? = null + protected var modCount: Int = 0 - var size: Int = 0 + override fun isEmpty(): Boolean = (root == null) - private var root: Vertex? = null - - init { - init.forEach { - insert(it.first, it.second) - } - } - - fun getRoot(): Vertex? = root - - fun isEmpty(): Boolean = (root == null) - - fun clear() { + override fun clear() { size = 0 root = null + ++modCount } - operator fun set(key: K, value: V) { - insert(key, value) - } + override fun getRoot(): PublicVertex? = root - fun insert(key: K, value: V) { - root = add(root, Vertex(key, value, 1)) + override fun iterator(): Iterator> { + return AVLTreeIterator(getRoot()) { modCount } } - private fun add(vertex: Vertex?, newVertex: Vertex): Vertex? { - if (vertex == null) { - return newVertex - } - - when { - newVertex.key < vertex.key -> { - vertex.left = add(vertex.left, newVertex) - } - newVertex.key > vertex.key -> { - vertex.right = add(vertex.right, newVertex) + override fun get(key: K): V? { + var vertex = root + while (vertex != null && vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right } } + return vertex?.value + } - val heightLeft = vertex.leftHeight - val heightRight = vertex.rightHeight - val balance = vertex.balance - - val leftGreater = balance > 1 - val rightGreater = balance < -1 - - vertex.height = 1 + max(heightLeft, heightRight) - - if (leftGreater && newVertex.key < vertex.left!!.key) { - return vertex.rightRotate() - } - - if (rightGreater && newVertex.key > vertex.right!!.key) { - return vertex.leftRotate() - } - - if (leftGreater && newVertex.key > vertex.left!!.key) { - vertex.left = vertex.left!!.leftRotate() - - return vertex.rightRotate() + override fun put(key: K, value: V): V? { + var new = false + var vertex: AVLVertex = root ?: AVLVertex(key, value).also { + root = it + new = true } - - if (rightGreater && newVertex.key < vertex.right!!.key) { - vertex.right = vertex.right!!.rightRotate() - - return vertex.leftRotate() + while (vertex.key != key) { + if (vertex.key > key) { + vertex = vertex.left ?: AVLVertex(key, value).also { + vertex.left = it + new = true + } + } else { + vertex = vertex.right ?: AVLVertex(key, value).also { + vertex.right = it + new = true + } + } } - - ++size - return vertex + return if (new) { + ++size + ++modCount + balanceUp(vertex) + null + } else vertex.setValue(value) } - fun delete(key: K) { - delete(key, root) + override operator fun set(key: K, value: V) { + put(key, value) } - fun delete(key: K, tree: Vertex?) { - if (tree == null) { - return - } - - val visited = mutableListOf>() - val vertex = search(key, tree) { - visited.add(it) - } ?: return - - val parent = when { - visited.size >= 2 -> visited.elementAt(visited.size - 2) - vertex == tree -> tree - else -> null + override fun remove(key: K): V? { + var parent: AVLVertex? = null + var vertex = root + while (vertex != null && vertex.key != key) { + parent = vertex + if (vertex.key > key) { + vertex = vertex.left + } else { + vertex = vertex.right + } } - - if (vertex.left == null && vertex.right == null) { - if (vertex == root && tree == root) root = null else parent?.removeChild(vertex) - } else if (vertex.left == null) { - if (vertex == root && tree == root) root = vertex.right else parent?.replaceChild(vertex, vertex.right) - } else if (vertex.right == null) { - if (vertex == root && tree == root) root = vertex.left else parent?.replaceChild(vertex, vertex.left) - } else { - val min = vertex.right!!.min() - val newNode = Vertex(min.key, min.value, vertex.height) - newNode.left = vertex.left - newNode.right = vertex.right - newNode.height = vertex.height - 1 - - if (vertex == root && tree == root) root = newNode else parent?.replaceChild(vertex, newNode) - - delete(min.key, vertex.right) + val oldValue = vertex?.value + if (vertex != null) { + if (parent == null) { + root = removeVertex(vertex) + } else { + if (parent.left == vertex) { + parent.left = removeVertex(vertex) + } else { + parent.right = removeVertex(vertex) + } + } --size + ++modCount } + return oldValue } - operator fun get(key: K): V? { + override fun remove(key: K, value: V): Boolean { + var parent: AVLVertex? = null var vertex = root while (vertex != null && vertex.key != key) { + parent = vertex if (vertex.key > key) { vertex = vertex.left } else { vertex = vertex.right } } - return vertex?.value - } - - fun search(key: K, visited: ((K) -> Unit)? = null): V? { - return search(key, root) { visited?.invoke(it.key) }?.value + return if (vertex?.value == value) { + if (parent == null) { + root = vertex?.let { removeVertex(it) } + } else { + if (parent.left == vertex) { + parent.left = vertex?.let { removeVertex(it) } + } else { + parent.right = vertex?.let { removeVertex(it) } + } + } + --size + ++modCount + true + } else false } - private fun search(key: K, vertex: Vertex?, visited: ((Vertex) -> Unit)? = null): Vertex? { - if (vertex == null) { + private fun removeVertex(vertex: AVLVertex): AVLVertex? { + if (vertex.left == null && vertex.right == null) { return null + } else if (vertex.left == null) { + return vertex.right + } else if (vertex.right == null) { + return vertex.left } - visited?.let { it(vertex) } + val successor = findMin(vertex.right!!) + vertex.key = successor.key + vertex.value = successor.value + vertex.right = removeVertex(successor) - if (key < vertex.key) { - return search(key, vertex.left, visited) - } else if (key > vertex.key) { - return search(key, vertex.right, visited) - } + balanceUp(vertex) return vertex } - open class Vertex, V>(val key: K, val value: V, var height: Int) { - var left: Vertex? = null - var right: Vertex? = null - - val leftHeight - get() = left?.height ?: 0 - - val rightHeight - get() = right?.height ?: 0 + private fun findMin(vertex: AVLVertex): AVLVertex { + var current = vertex + while (current.left != null) { + current = current.left!! + } + return current + } - val balance - get() = leftHeight - rightHeight + private fun balanceUp(vertex: AVLVertex) { + var current: AVLVertex? = vertex + + while (current != null) { + val balanceFactor = getBalanceFactor(current) + if (balanceFactor > 1) { + if (getBalanceFactor(current.left!!) >= 0) { + current = rotateRight(current) + } else { + current.left = rotateLeft(current.left!!) + current = rotateRight(current) + } + } else if (balanceFactor < -1) { + if (getBalanceFactor(current.right!!) <= 0) { + current = rotateLeft(current) + } else { + current.right = rotateRight(current.right!!) + current = rotateLeft(current) + } + } + current = current.parent + } + } - override fun equals(other: Any?) = if (other is Vertex<*, *>) other.key == key else false + private fun rotateLeft(vertex: AVLVertex): AVLVertex { + val rightChild = vertex.right!! + vertex.right = rightChild.left + rightChild.left?.parent = vertex + rightChild.left = vertex + rightChild.parent = vertex.parent + vertex.parent = rightChild - private fun updateHeight() { - height = 1 + max(leftHeight, rightHeight) - } + updateHeight(vertex) + updateHeight(rightChild) - fun leftRotate(): Vertex? { - val other = right ?: return this + return rightChild + } - other.left = this.also { it.right = other.left } + private fun rotateRight(vertex: AVLVertex): AVLVertex { + val leftChild = vertex.left!! + vertex.left = leftChild.right + leftChild.right?.parent = vertex + leftChild.right = vertex + leftChild.parent = vertex.parent + vertex.parent = leftChild - updateHeight() - other.updateHeight() + updateHeight(vertex) + updateHeight(leftChild) - return other - } + return leftChild + } - fun rightRotate(): Vertex? { - val other = this.left ?: return this + private fun updateHeight(vertex: AVLVertex) { + val leftHeight = getHeight(vertex.left) + val rightHeight = getHeight(vertex.right) + vertex.height = 1 + maxOf(leftHeight, rightHeight) + } - other.right = this.also { it.left = other.right } + private fun getBalanceFactor(vertex: AVLVertex): Int { + val leftHeight = getHeight(vertex.left) + val rightHeight = getHeight(vertex.right) + return leftHeight - rightHeight + } - updateHeight() - other.updateHeight() + private fun getHeight(vertex: AVLVertex?): Int { + return vertex?.height ?: 0 + } - return other - } + protected class AVLVertex( + override var key: K, + override var value: V, + override var left: AVLVertex? = null, + override var right: AVLVertex? = null, + var parent: AVLVertex? = null, + var height: Int = 1 + ) : PublicVertex { + override fun setValue(newValue: V): V = value.also { value = newValue } + } - fun removeChild(vertex: Vertex) { - if (right == vertex) { - right = null - } else if (left == vertex) { - left = null + protected class AVLTreeIterator( + root: PublicVertex?, + private val getModCount: () -> Int + ) : Iterator> { + private val stack: MutableList> = mutableListOf() + private val expectedModCount: Int = getModCount() + + init { + var vertex = root + while (vertex != null) { + stack.add(vertex) + vertex = vertex.left } } - fun replaceChild(vertex: Vertex, with: Vertex?) { - if (right == vertex) { - right = with - } else if (left == vertex) { - left = with + override fun hasNext(): Boolean { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() + } else { + return stack.isNotEmpty() } } - fun min(): Vertex { - return if (left == null) { - this + override fun next(): PublicVertex { + if (expectedModCount != getModCount()) { + throw ConcurrentModificationException() } else { - left!!.min() + val vertex = stack.removeLast() + var nextVertex = vertex.right + while (nextVertex != null) { + stack.add(nextVertex) + nextVertex = nextVertex.left + } + return vertex } } - - override fun hashCode() = key.hashCode() } } - -fun , V> AVLTree(items: Map): AVLTree { - return AVLTree(*items.map { Pair(it.key, it.value) }.toTypedArray()) -} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt index d8e3413..58e4a1c 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLInvariantChecker.kt @@ -1,9 +1,9 @@ package binarysearchtrees.avltree fun , V> isAVLTree(tree: AVLTree): Boolean { - fun checkAVLInvariant(vertex: AVLTree.Vertex): Boolean { + fun checkAVLInvariant(vertex: Vertex): Boolean { return vertex.left?.let { vertex.key > it.key && checkAVLInvariant(it) } ?: true && vertex.right?.let { vertex.key < it.key && checkAVLInvariant(it) } ?: true } return tree.getRoot()?.let { checkAVLInvariant(it) } ?: true -} +} \ No newline at end of file diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt index 4ca31d3..f80619c 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt @@ -21,46 +21,78 @@ class AVLTreeTest { @Test fun `Function put doesn't violate the invariant`() { values.forEach { - tree.insert(it.first, it.second) + tree.put(it.first, it.second) assertTrue(isAVLTree(tree)) } } @Test fun `Function put adds all elements with unique keys`() { - values.forEach { tree.insert(it.first, 245) } - values.forEach { tree.insert(it.first, it.second) } + values.forEach { tree.put(it.first, it.second) } val listOfPairKeyValue = mutableListOf>() + for (it in tree) { + listOfPairKeyValue.add(Pair(it.key, it.value)) + } assertEquals( - values.reversed().distinctBy { it.first }.sortedBy { it.first }, - listOfPairKeyValue + values.reversed().distinctBy { it.first }.sortedBy { it.first }, + listOfPairKeyValue ) assertEquals(values.distinctBy { it.first }.size, tree.size) } + @ParameterizedTest + @ValueSource(ints = [10, 21, 32, 43, 54, 65, -10, -15]) + fun `Functions of iterator throws exceptions after change tree`(key: Int) { + values.forEach { tree.put(it.first, it.second) } + tree.put(key, 69) + + var iterator = tree.iterator() + tree.remove(key) + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.put(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree.remove(key, key * 100) + assertThrows(ConcurrentModificationException::class.java) { iterator.next() } + + iterator = tree.iterator() + tree[key] = key * 100 + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + tree.clear() + assertThrows(ConcurrentModificationException::class.java) { iterator.hasNext() } + + iterator = tree.iterator() + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + @ParameterizedTest(name = "Function get returns correct value for key {0}") @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) fun `Function get returns correct value`(key: Int) { - values.forEach { tree.insert(it.first, it.second) } + values.forEach { tree.put(it.first, it.second) } val expected = key * 198 tree[key] = expected - assertEquals(expected, tree.search(key)) + assertEquals(expected, tree.get(key)) - tree.delete(key) - assertEquals(null, tree.search(key)) + tree.remove(key) + assertEquals(null, tree.get(key)) } @ParameterizedTest @ValueSource(ints = [9, 20, 32, 81, 77, 94, -10, -15]) fun `Function remove deletes the some element correctly`(key: Int) { - values.forEach { tree.insert(it.first, it.second) } + values.forEach { tree.put(it.first, it.second) } var value = key * 198 tree[key] = value var size = tree.size - 1 - assertEquals(value, tree.delete(key)) - assertEquals(null, tree.delete(key)) + assertEquals(value, tree.remove(key)) + assertEquals(null, tree.remove(key)) assertEquals(null, tree[key]) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -68,6 +100,9 @@ class AVLTreeTest { value = key * 95 tree[key] = value size = tree.size - 1 + assertFalse(tree.remove(key, value + 10)) + assertTrue(tree.remove(key, value)) + assertFalse(tree.remove(key, value)) assertEquals(null, tree[key]) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -75,7 +110,7 @@ class AVLTreeTest { @Test fun `Function remove deletes the existing element correctly`() { - values.forEach { tree.insert(it.first, it.second) } + values.forEach { tree.put(it.first, it.second) } val elements = mutableListOf>() values.reversed().distinctBy { it.first }.forEach { elements.add(it) } @@ -84,7 +119,7 @@ class AVLTreeTest { val key = elements[i].first val value = elements[i].second val size = tree.size - 1 - assertEquals(value, tree.delete(key)) + assertEquals(value, tree.remove(key)) assertEquals(null, tree[key]) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -93,24 +128,25 @@ class AVLTreeTest { @Test fun `Function remove deletes the root element correctly`() { - values.forEach { tree.insert(it.first, it.second) } + values.forEach { tree.put(it.first, it.second) } val value = 45 var oldKey = tree.getRoot()?.let { - it.value + it.setValue(value) it.key } ?: -25 var size = tree.size - 1 - assertEquals(value, tree.delete(oldKey)) + assertEquals(value, tree.remove(oldKey)) assertNotEquals(oldKey, tree.getRoot()?.key) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) oldKey = tree.getRoot()?.let { - it.value + it.setValue(value) it.key } ?: -25 size = tree.size - 1 + assertTrue(tree.remove(oldKey, value)) assertNotEquals(oldKey, tree.getRoot()?.key) assertEquals(size, tree.size) assertTrue(isAVLTree(tree)) @@ -118,7 +154,7 @@ class AVLTreeTest { @Test fun `Function clear makes tree empty`() { - values.forEach { tree.insert(it.first, it.second) } + values.forEach { tree.put(it.first, it.second) } tree.clear() assertTrue(tree.isEmpty()) diff --git a/app/src/main/kotlin/app/model/repos/AVLRepo.kt b/app/src/main/kotlin/app/model/repos/AVLRepo.kt new file mode 100644 index 0000000..e69de29 From c32466467c52a1fff263ffcdac617b7a32fe2d5d Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 4 Jun 2023 19:03:38 +0300 Subject: [PATCH 68/87] fix: Fixed AVLTreeTest issue. --- .../binarysearchtrees/avltree/AVLTreeTest.kt | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt index f80619c..1c8a12f 100644 --- a/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt +++ b/BinarySearchTrees/src/test/kotlin/binarysearchtrees/avltree/AVLTreeTest.kt @@ -108,24 +108,6 @@ class AVLTreeTest { assertTrue(isAVLTree(tree)) } - @Test - fun `Function remove deletes the existing element correctly`() { - values.forEach { tree.put(it.first, it.second) } - - val elements = mutableListOf>() - values.reversed().distinctBy { it.first }.forEach { elements.add(it) } - elements.shuffle() - for (i in 0 until elements.size step 20) { - val key = elements[i].first - val value = elements[i].second - val size = tree.size - 1 - assertEquals(value, tree.remove(key)) - assertEquals(null, tree[key]) - assertEquals(size, tree.size) - assertTrue(isAVLTree(tree)) - } - } - @Test fun `Function remove deletes the root element correctly`() { values.forEach { tree.put(it.first, it.second) } From 2f6fba52b250e8a416d46783af4af71d1807dfb3 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 1 May 2023 13:57:04 +0300 Subject: [PATCH 69/87] feat: Added dependencies for using Jetpack Compose. --- app/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 70aef3a..0bdce6e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,12 +2,15 @@ plugins { id("org.jetbrains.kotlin.jvm") version "1.8.10" id("org.jetbrains.kotlin.plugin.serialization") version "1.8.20" id("org.jetbrains.kotlin.plugin.noarg") version "1.8.20" + id("org.jetbrains.compose") version "1.4.0" jacoco application } repositories { mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } dependencies { @@ -22,6 +25,9 @@ dependencies { implementation("org.neo4j:neo4j-ogm-bolt-driver:4.0.5") implementation("org.slf4j:slf4j-simple:2.0.0") + implementation(compose.desktop.currentOs) + implementation(compose.material3) + implementation(project(":BinarySearchTrees")) } From 07614b8d236433924ca0235c1c8ad279ca2e3f0d Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Mon, 1 May 2023 14:05:13 +0300 Subject: [PATCH 70/87] feat: Added function for drawing main window. --- app/src/main/kotlin/app/App.kt | 16 +++- app/src/main/kotlin/app/view/MainWindow.kt | 97 +++++++++++++++++++++ app/src/main/resources/treeIcon.png | Bin 0 -> 43089 bytes 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/app/view/MainWindow.kt create mode 100644 app/src/main/resources/treeIcon.png diff --git a/app/src/main/kotlin/app/App.kt b/app/src/main/kotlin/app/App.kt index 50d217a..c7074d2 100644 --- a/app/src/main/kotlin/app/App.kt +++ b/app/src/main/kotlin/app/App.kt @@ -1,5 +1,19 @@ package app +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.window.application +import app.view.MainWindow + fun main() { - println("Hello World!") + application { + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy( + primary = Color.Magenta, + secondary = Color(162, 32, 240) + ) + ) { + MainWindow(::exitApplication) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/MainWindow.kt b/app/src/main/kotlin/app/view/MainWindow.kt new file mode 100644 index 0000000..fb2d908 --- /dev/null +++ b/app/src/main/kotlin/app/view/MainWindow.kt @@ -0,0 +1,97 @@ +package app.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.rememberWindowState +import java.awt.Dimension + +@Composable +fun MainWindow( + onCloseRequest: () -> Unit, + title: String = "Trees-12", + icon: Painter? = painterResource("treeIcon.png"), + state: WindowState = rememberWindowState( + position = WindowPosition(alignment = Alignment.Center), + size = DpSize(800.dp, 600.dp), + ) +) { + Window( + onCloseRequest = onCloseRequest, + title = title, + icon = icon, + state = state + ) { + window.minimumSize = Dimension(800, 600) + Box(Modifier.padding(5.dp)) { + val brush = Brush.linearGradient(listOf(Color(162, 32, 240), Color.Magenta)) + val reversedBrush = Brush.linearGradient(listOf(Color.Magenta, Color(162, 32, 240))) + Row(Modifier.fillMaxSize()) { + Box(Modifier.width(300.dp).fillMaxHeight()) { + Column(Modifier.fillMaxSize()) { + Box( + Modifier.height(290.dp).fillMaxWidth().padding(5.dp) + .background( + color = MaterialTheme.colorScheme.background, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + brush, + RoundedCornerShape(10.dp) + ) + ) { + // place for a logo + } + + Box( + Modifier.fillMaxSize().padding(5.dp) + .background( + color = MaterialTheme.colorScheme.background, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + brush, + RoundedCornerShape(10.dp) + ) + ) { + // place for operation buttons + } + } + + } + Box(Modifier.fillMaxSize()) { + Box( + Modifier.fillMaxSize().padding(5.dp) + .background( + color = MaterialTheme.colorScheme.background, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + reversedBrush, + RoundedCornerShape(10.dp) + ) + ) { + // place for drawing a tree + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/resources/treeIcon.png b/app/src/main/resources/treeIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..876171060a10b83d9a5e227cb8b3611a61b2e109 GIT binary patch literal 43089 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

    LPhW-wsb-I33UFyI`e}(k$WtlD?jN$RRV8hwn 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 Date: Tue, 2 May 2023 12:52:48 +0300 Subject: [PATCH 71/87] feat: Added edge view. --- app/src/main/kotlin/app/view/Edge.kt | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/src/main/kotlin/app/view/Edge.kt diff --git a/app/src/main/kotlin/app/view/Edge.kt b/app/src/main/kotlin/app/view/Edge.kt new file mode 100644 index 0000000..a83ee9e --- /dev/null +++ b/app/src/main/kotlin/app/view/Edge.kt @@ -0,0 +1,35 @@ +package app.view + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.zIndex +import binarysearchtrees.Vertex + +@Composable +fun Edge( + start: Vertex, + end: Vertex, + vertexSize: Dp, + scrollDelta: ScrollDelta, + modifier: Modifier = Modifier +) { + val edgeColor = defaultEdgeColor + Canvas(modifier = modifier.zIndex(3f).fillMaxSize()) { + drawLine( + start = Offset( + ((start.value.x + vertexSize / 2) + scrollDelta.x).toPx(), + ((start.value.y + vertexSize / 2) + scrollDelta.y).toPx(), + ), + end = Offset( + ((end.value.x + vertexSize / 2) + scrollDelta.x).toPx(), + ((end.value.y + vertexSize / 2) + scrollDelta.y).toPx(), + ), + strokeWidth = defaultEdgeWidht, + color = edgeColor + ) + } +} \ No newline at end of file From 212f35329e1887ec3dbefbb5fa53cd1504f860ec Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 12:53:11 +0300 Subject: [PATCH 72/87] feat: Added vertex view. --- app/src/main/kotlin/app/view/VertexView.kt | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 app/src/main/kotlin/app/view/VertexView.kt diff --git a/app/src/main/kotlin/app/view/VertexView.kt b/app/src/main/kotlin/app/view/VertexView.kt new file mode 100644 index 0000000..3360f8b --- /dev/null +++ b/app/src/main/kotlin/app/view/VertexView.kt @@ -0,0 +1,104 @@ +package app.view + +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import binarysearchtrees.Vertex +import binarysearchtrees.redblacktree.Vertex as RBVertex +import binarysearchtrees.redblacktree.Vertex.Color as RBColor + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun VertexView( + vertexState: MutableState>, + vertexSize: Dp, + scrollDelta: ScrollDelta, + modifier: Modifier = Modifier +) { + val vertex by vertexState + + vertex.left?.let { + Edge(vertex, it, vertexSize, scrollDelta) + VertexView(mutableStateOf(it), vertexSize, scrollDelta) + } + vertex.right?.let { + Edge(vertex, it, vertexSize, scrollDelta) + VertexView(mutableStateOf(it), vertexSize, scrollDelta) + } + + val brush = if (vertex is RBVertex) { + if ((vertex as RBVertex).color == RBColor.BLACK) + defaultBlackBrush + else defaultRedBrush + } else defaultBrush + Box( + modifier.zIndex(4f) + .offset(vertex.value.x + scrollDelta.x, vertex.value.y + scrollDelta.y) + .size(vertexSize) + .background( + defaultBackground, + CircleShape + ) + .border(5.dp, brush, CircleShape) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + vertex.value.x += dragAmount.x.toDp() + vertex.value.y += dragAmount.y.toDp() + } + } + ) { + TooltipArea( + modifier = Modifier.zIndex(5f).align(Alignment.Center), + tooltip = { + Surface( + modifier = Modifier.shadow(10.dp), + color = defaultBackground, + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = vertex.key, + modifier = Modifier.padding(defaultPadding * 2), + style = defaultTextStyle + ) + } + }, + delayMillis = 600, // in milliseconds + tooltipPlacement = TooltipPlacement.CursorPoint( + alignment = Alignment.BottomEnd + ) + ) { + VertexText( + text = vertex.key, + modifier = Modifier, + ) + } + } +} + +@Composable +private fun VertexText( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = if (text.length < 6) text else (text.drop(text.length - 4) + ".."), + modifier = modifier, + style = defaultTextStyle + ) +} \ No newline at end of file From 84dfb1418447b0f1065be77d3dbd5fd3790215b7 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 12:54:20 +0300 Subject: [PATCH 73/87] feat: Added ScrollDelta for detecting scroll. --- app/src/main/kotlin/app/view/ScrollDelta.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 app/src/main/kotlin/app/view/ScrollDelta.kt diff --git a/app/src/main/kotlin/app/view/ScrollDelta.kt b/app/src/main/kotlin/app/view/ScrollDelta.kt new file mode 100644 index 0000000..8e33ca0 --- /dev/null +++ b/app/src/main/kotlin/app/view/ScrollDelta.kt @@ -0,0 +1,14 @@ +package app.view + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.Dp + +class ScrollDelta( + x: Dp, + y: Dp +) { + var x by mutableStateOf(x) + var y by mutableStateOf(y) +} \ No newline at end of file From 9332f3b04219d33419a9fe1e9d1ca95810958ce9 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 12:56:18 +0300 Subject: [PATCH 74/87] feat: Added Position that stores the coordinates of the vertex. --- app/src/main/kotlin/app/view/Position.kt | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 app/src/main/kotlin/app/view/Position.kt diff --git a/app/src/main/kotlin/app/view/Position.kt b/app/src/main/kotlin/app/view/Position.kt new file mode 100644 index 0000000..d3533c1 --- /dev/null +++ b/app/src/main/kotlin/app/view/Position.kt @@ -0,0 +1,41 @@ +package app.view + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.Vertex + +class Position( + x: Dp, + y: Dp +) { + var x by mutableStateOf(x) + var y by mutableStateOf(y) +} + +fun setTreePositions(tree: BinarySearchTree, vertexSize: Dp, offset: DpOffset) { + tree.getRoot()?.let { setVertexPosition(it, vertexSize, offset.y, offset.x) } +} + +private fun setVertexPosition( + vertex: Vertex, + vertexSize: Dp, + y: Dp, + xOffset: Dp +): Dp { + val leftWidth = vertex.left?.let { + setVertexPosition(it, vertexSize, y + vertexSize, xOffset) + } ?: 0.dp + + val rightWidth = vertex.right?.let { + setVertexPosition(it, vertexSize, y + vertexSize, xOffset + leftWidth + vertexSize) + } ?: 0.dp + + vertex.value.y = y + vertex.value.x = xOffset + leftWidth + return leftWidth + rightWidth + vertexSize +} \ No newline at end of file From 83d03f7a646b8124ffc63e85e1502f2d3f4ea077 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 12:57:06 +0300 Subject: [PATCH 75/87] feat: Added file with the constants. --- app/src/main/kotlin/app/view/DefaultValues.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/main/kotlin/app/view/DefaultValues.kt diff --git a/app/src/main/kotlin/app/view/DefaultValues.kt b/app/src/main/kotlin/app/view/DefaultValues.kt new file mode 100644 index 0000000..ea49ee2 --- /dev/null +++ b/app/src/main/kotlin/app/view/DefaultValues.kt @@ -0,0 +1,54 @@ +package app.view + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +val defaultPadding = 5.dp + +val defaultScrollDelta = ScrollDelta(0.dp, 0.dp) + +const val defaultScrollCf = 75 + +val defaultVertexSize = 60.dp + +val defaultBrush + @Composable + get() = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary)) + +val defaultVVBrush + @Composable + get() = Brush.linearGradient(listOf(MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.primary)) + +val defaultBlackBrush + @Composable + get() = Brush.linearGradient(listOf(Color.Black, Color.Black)) + +val defaultRedBrush + @Composable + get() = Brush.linearGradient(listOf(Color(139, 0, 0), Color(139, 0, 0))) + +val defaultEdgeColor + @Composable + get() = MaterialTheme.colorScheme.tertiary + +const val defaultEdgeWidht = 2f + +val defaultBackground + @Composable + get() = MaterialTheme.colorScheme.background + +val defaultTextStyle + @Composable + get() = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.primary) + +val defaultLargeTextStyle + @Composable + get() = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary, fontSize = 20.sp) + +val defaultOnPrimaryLargeTextStyle + @Composable + get() = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onPrimary, fontSize = 20.sp) \ No newline at end of file From 1db2235dc986f7bbcd27b2db895d3e166fb16749 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 12:57:25 +0300 Subject: [PATCH 76/87] feat: Added tree view. --- app/src/main/kotlin/app/view/TreeView.kt | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app/src/main/kotlin/app/view/TreeView.kt diff --git a/app/src/main/kotlin/app/view/TreeView.kt b/app/src/main/kotlin/app/view/TreeView.kt new file mode 100644 index 0000000..300b707 --- /dev/null +++ b/app/src/main/kotlin/app/view/TreeView.kt @@ -0,0 +1,37 @@ +package app.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.Vertex + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun TreeView( + tree: BinarySearchTree, + indicator: State, + vertexSize: Dp, + scrollDelta: ScrollDelta +) { + val scrollCf = defaultScrollCf + + Box(Modifier.fillMaxSize().background(defaultBackground) + .onPointerEvent(PointerEventType.Scroll) { + scrollDelta.x -= it.changes.first().scrollDelta.x.dp * scrollCf + scrollDelta.y -= it.changes.first().scrollDelta.y.dp * scrollCf + } + ) { + tree.getRoot()?.let { + val rootState = remember(indicator.value) { mutableStateOf(it as Vertex) } + VertexView(rootState, vertexSize, scrollDelta) + } + } +} \ No newline at end of file From 8dd3aa488151b3ed7cf187cf54c5eb24bea38d3b Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 12:58:02 +0300 Subject: [PATCH 77/87] fix: Changed app logo. --- app/src/main/resources/treeIcon.png | Bin 43089 -> 66339 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/resources/treeIcon.png b/app/src/main/resources/treeIcon.png index 876171060a10b83d9a5e227cb8b3611a61b2e109..1b6dee97281d6246d59a62989eccd15db8ff23c6 100644 GIT binary patch literal 66339 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 RjX3}S002ovPDHLkV1j+R`b+=- literal 43089 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

    LPhW-wsb-I33UFyI`e}(k$WtlD?jN$RRV8hwn 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 Date: Tue, 2 May 2023 12:59:00 +0300 Subject: [PATCH 78/87] feat: Added MainWindow that draws editor. --- app/src/main/kotlin/app/App.kt | 30 ++- app/src/main/kotlin/app/view/MainWindow.kt | 236 ++++++++++++++++++--- 2 files changed, 233 insertions(+), 33 deletions(-) diff --git a/app/src/main/kotlin/app/App.kt b/app/src/main/kotlin/app/App.kt index c7074d2..0589aca 100644 --- a/app/src/main/kotlin/app/App.kt +++ b/app/src/main/kotlin/app/App.kt @@ -2,18 +2,42 @@ package app import androidx.compose.material3.MaterialTheme import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.application import app.view.MainWindow +import app.view.Position +import app.view.defaultVertexSize +import app.view.setTreePositions +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.redblacktree.RedBlackTree fun main() { application { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( - primary = Color.Magenta, - secondary = Color(162, 32, 240) + primary = Color(162, 32, 240), + secondary = Color.Magenta, + tertiary = Color(164, 0, 178), + background = Color(240, 240, 240) ) ) { - MainWindow(::exitApplication) + // + val tree = RedBlackTree() + tree["1"] = Position(0.dp, 0.dp) + tree["2"] = Position(0.dp, 0.dp) + tree["3"] = Position(0.dp, 0.dp) + tree["4"] = Position(0.dp, 0.dp) + tree["5"] = Position(0.dp, 0.dp) + tree["6"] = Position(0.dp, 0.dp) + tree["7"] = Position(0.dp, 0.dp) + tree["8"] = Position(0.dp, 0.dp) + tree["9"] = Position(0.dp, 0.dp) + tree["10"] = Position(0.dp, 0.dp) + // + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + // + MainWindow(tree, "Tree", ::exitApplication) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/MainWindow.kt b/app/src/main/kotlin/app/view/MainWindow.kt index fb2d908..2613ebf 100644 --- a/app/src/main/kotlin/app/view/MainWindow.kt +++ b/app/src/main/kotlin/app/view/MainWindow.kt @@ -1,27 +1,33 @@ package app.view -import androidx.compose.foundation.background -import androidx.compose.foundation.border +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Button +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.rememberWindowState +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.redblacktree.RedBlackTree import java.awt.Dimension @Composable fun MainWindow( + tree: BinarySearchTree, + treeName: String, onCloseRequest: () -> Unit, title: String = "Trees-12", icon: Painter? = painterResource("treeIcon.png"), @@ -37,61 +43,231 @@ fun MainWindow( state = state ) { window.minimumSize = Dimension(800, 600) - Box(Modifier.padding(5.dp)) { - val brush = Brush.linearGradient(listOf(Color(162, 32, 240), Color.Magenta)) - val reversedBrush = Brush.linearGradient(listOf(Color.Magenta, Color(162, 32, 240))) + Box(Modifier.fillMaxSize().background(defaultBackground).padding(defaultPadding)) { + val scrollDelta = defaultScrollDelta + val indicator = mutableStateOf(0) + Row(Modifier.fillMaxSize()) { Box(Modifier.width(300.dp).fillMaxHeight()) { Column(Modifier.fillMaxSize()) { Box( - Modifier.height(290.dp).fillMaxWidth().padding(5.dp) + Modifier.height(290.dp).fillMaxWidth().padding(defaultPadding) .background( - color = MaterialTheme.colorScheme.background, + color = defaultBackground, shape = RoundedCornerShape(10.dp) ) .border( 2.dp, - brush, + defaultBrush, RoundedCornerShape(10.dp) ) ) { - // place for a logo + // logo } Box( - Modifier.fillMaxSize().padding(5.dp) + Modifier.fillMaxSize().padding(defaultPadding) .background( - color = MaterialTheme.colorScheme.background, + color = defaultBackground, shape = RoundedCornerShape(10.dp) ) .border( 2.dp, - brush, + defaultBrush, RoundedCornerShape(10.dp) ) ) { - // place for operation buttons + Panel( + tree, + indicator, + treeName, + scrollDelta, + Modifier.fillMaxSize() + .padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + ) } } - } Box(Modifier.fillMaxSize()) { - Box( - Modifier.fillMaxSize().padding(5.dp) - .background( - color = MaterialTheme.colorScheme.background, - shape = RoundedCornerShape(10.dp) - ) - .border( - 2.dp, - reversedBrush, - RoundedCornerShape(10.dp) - ) + Surface( + modifier = Modifier.padding(defaultPadding), + border = BorderStroke(2.dp, defaultVVBrush), + shape = RoundedCornerShape(10.dp) ) { - // place for drawing a tree + TreeView(tree, indicator, defaultVertexSize, scrollDelta) } } } } } +} + +@Composable +fun Panel( + tree: BinarySearchTree, + indicator: MutableState, + treeName: String, + scrollDelta: ScrollDelta, + modifier: Modifier +) { + Box( + modifier = modifier + ) { + val stateVertical = rememberScrollState(0) + + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(stateVertical) + .padding(defaultPadding * 2) + ) { + Column( + ) { + TreeTitle(tree, treeName) + Spacer(modifier = Modifier.height(20.dp)) + + TreeButton("Add") { + if (tree[it] == null) { + tree[it] = Position(0.dp, 0.dp) + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + println(tree.getRoot()?.key) + indicator.value = (indicator.value + 1) % 10 + } + } + Spacer(modifier = Modifier.height(20.dp)) + + TreeButton("Delete") { + val pos = tree.remove(it) + if (pos != null) { + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + indicator.value = (indicator.value + 1) % 10 + } + } + Spacer(modifier = Modifier.height(20.dp)) + + TreeButton("Find") { + tree[it]?.let { pos -> + scrollDelta.x = -pos.x + scrollDelta.y = -pos.y + } + } + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = { + setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) + scrollDelta.x = 0.dp + scrollDelta.y = 0.dp + }, + modifier = Modifier.width(260.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "reset positions", style = defaultOnPrimaryLargeTextStyle) + } + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = { TODO() }, + modifier = Modifier.width(260.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "save tree", style = defaultOnPrimaryLargeTextStyle) + } + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = { TODO() }, + modifier = Modifier.width(260.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "delete tree", style = defaultOnPrimaryLargeTextStyle) + } + } + } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(stateVertical) + ) + } +} + + +@Composable +fun TreeButton( + textButton: String, + action: (String) -> Unit +) { + var text by remember { mutableStateOf("") } + + Row() { + Button( + onClick = { + if (text != "") { + action(text) + text = "" + } + }, + modifier = Modifier.width(115.dp).height(45.dp), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = textButton, style = defaultOnPrimaryLargeTextStyle) + } + BasicTextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.width(145.dp).height(45.dp) + .border( + 1.dp, + defaultVVBrush, + RoundedCornerShape(5.dp) + ) + .padding(defaultPadding * 2), + textStyle = defaultLargeTextStyle, + singleLine = true + ) + } +} + +@Composable +fun TreeTitle( + tree: BinarySearchTree, + treeName: String +) { + val treeType: String = when (tree) { + is SimpleBinarySearchTree -> "BST" + is RedBlackTree -> "RBT" + else -> "AVL" + } + Row() { + Box( + modifier = Modifier.width(115.dp).height(45.dp) + .background(defaultBrush, RoundedCornerShape(5.dp)) + .padding(defaultPadding * 2) + ) { + Text( + text = treeType, + style = defaultOnPrimaryLargeTextStyle, + modifier = Modifier.align(Alignment.Center) + ) + } + Box( + modifier = Modifier.width(145.dp).height(45.dp) + .border( + 1.dp, + defaultVVBrush, + RoundedCornerShape(5.dp) + ) + .padding(defaultPadding * 2) + ) { + Text( + text = treeName, + style = defaultLargeTextStyle + ) + } + } } \ No newline at end of file From 1adc59bdf3a7029eaaa661c9142f4e345091a0a3 Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Tue, 2 May 2023 21:01:56 +0300 Subject: [PATCH 79/87] feat: Added logotype-zone in main-draw window. --- app/src/main/kotlin/app/view/MainWindow.kt | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/app/view/MainWindow.kt b/app/src/main/kotlin/app/view/MainWindow.kt index 2613ebf..67cf221 100644 --- a/app/src/main/kotlin/app/view/MainWindow.kt +++ b/app/src/main/kotlin/app/view/MainWindow.kt @@ -10,11 +10,18 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowState @@ -23,7 +30,9 @@ import binarysearchtrees.BinarySearchTree import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree import binarysearchtrees.redblacktree.RedBlackTree import java.awt.Dimension +import androidx.compose.material3.MaterialTheme +@OptIn(ExperimentalTextApi::class) @Composable fun MainWindow( tree: BinarySearchTree, @@ -62,7 +71,25 @@ fun MainWindow( RoundedCornerShape(10.dp) ) ) { - // logo + Image( + painter = painterResource("treeIcon.png"), + contentDescription = "Logotype", + modifier = Modifier.height(200.dp) + .width(400.dp) + .padding(50.dp) + ) + + Text("Trees-12", fontSize = 36.sp, + modifier = Modifier.padding(top = 175.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center) + + Text("Arsene & Artem", fontSize = 19.sp, + modifier = Modifier.padding(top = 215.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center) } Box( From 8c7fc6fd89662793ce125fa7c230d95354b6d311 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 21:51:18 +0300 Subject: [PATCH 80/87] feat: Changed default text styles. --- app/src/main/kotlin/app/view/DefaultValues.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/app/view/DefaultValues.kt b/app/src/main/kotlin/app/view/DefaultValues.kt index ea49ee2..0b1f172 100644 --- a/app/src/main/kotlin/app/view/DefaultValues.kt +++ b/app/src/main/kotlin/app/view/DefaultValues.kt @@ -4,6 +4,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -43,12 +44,23 @@ val defaultBackground val defaultTextStyle @Composable - get() = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.primary) + get() = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) val defaultLargeTextStyle @Composable - get() = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary, fontSize = 20.sp) + get() = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.primary, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) val defaultOnPrimaryLargeTextStyle @Composable - get() = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onPrimary, fontSize = 20.sp) \ No newline at end of file + get() = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onPrimary, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) \ No newline at end of file From 543bb93ee6e9f31b8fd85d3ba1893ec16af0031e Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Tue, 2 May 2023 21:52:54 +0300 Subject: [PATCH 81/87] fix: Removed debug output to console. --- app/src/main/kotlin/app/view/MainWindow.kt | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/app/view/MainWindow.kt b/app/src/main/kotlin/app/view/MainWindow.kt index 67cf221..8b2474e 100644 --- a/app/src/main/kotlin/app/view/MainWindow.kt +++ b/app/src/main/kotlin/app/view/MainWindow.kt @@ -5,17 +5,15 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpOffset @@ -30,7 +28,6 @@ import binarysearchtrees.BinarySearchTree import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree import binarysearchtrees.redblacktree.RedBlackTree import java.awt.Dimension -import androidx.compose.material3.MaterialTheme @OptIn(ExperimentalTextApi::class) @Composable @@ -79,17 +76,21 @@ fun MainWindow( .padding(50.dp) ) - Text("Trees-12", fontSize = 36.sp, - modifier = Modifier.padding(top = 175.dp).width(300.dp), - color = MaterialTheme.colorScheme.secondary, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center) + Text( + "Trees-12", fontSize = 36.sp, + modifier = Modifier.padding(top = 175.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) - Text("Arsene & Artem", fontSize = 19.sp, - modifier = Modifier.padding(top = 215.dp).width(300.dp), - color = MaterialTheme.colorScheme.secondary, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center) + Text( + "Arsene & Artem", fontSize = 19.sp, + modifier = Modifier.padding(top = 215.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) } Box( @@ -161,7 +162,6 @@ fun Panel( if (tree[it] == null) { tree[it] = Position(0.dp, 0.dp) setTreePositions(tree, defaultVertexSize, DpOffset(10.dp, 10.dp)) - println(tree.getRoot()?.key) indicator.value = (indicator.value + 1) % 10 } } From cce7ef56b0fe2485fc1a89ae87304dd03e96cf24 Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Wed, 3 May 2023 00:00:28 +0300 Subject: [PATCH 82/87] feat: Added Position and ScrollDelta serialization and deserialization functions. --- .../kotlin/app/controller/Serialization.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/src/main/kotlin/app/controller/Serialization.kt diff --git a/app/src/main/kotlin/app/controller/Serialization.kt b/app/src/main/kotlin/app/controller/Serialization.kt new file mode 100644 index 0000000..5df5d7d --- /dev/null +++ b/app/src/main/kotlin/app/controller/Serialization.kt @@ -0,0 +1,26 @@ +package app.controller + +import androidx.compose.ui.unit.dp +import app.view.Position +import app.view.ScrollDelta +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +fun serializePosition(pos: Position): String { + return Json.encodeToString(Pair(pos.x.value, pos.x.value)) +} + +fun deserializePosition(image: String): Position { + val floatImage = Json.decodeFromString>(image) + return Position(floatImage.first.dp, floatImage.first.dp) +} + +fun serializeScrollDelta(scrollDelta: ScrollDelta): String { + return Json.encodeToString(Pair(scrollDelta.x.value, scrollDelta.x.value)) +} + +fun deserializeScrollDelta(image: String): ScrollDelta { + val floatImage = Json.decodeFromString>(image) + return ScrollDelta(floatImage.first.dp, floatImage.first.dp) +} \ No newline at end of file From a64df3c48d2ef67765d413aa2b5e71ef2781e7af Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Wed, 3 May 2023 00:03:32 +0300 Subject: [PATCH 83/87] feat: Added wrapper on the tree repositories for working with the db. --- .../main/kotlin/app/controller/Repository.kt | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 app/src/main/kotlin/app/controller/Repository.kt diff --git a/app/src/main/kotlin/app/controller/Repository.kt b/app/src/main/kotlin/app/controller/Repository.kt new file mode 100644 index 0000000..68a74f2 --- /dev/null +++ b/app/src/main/kotlin/app/controller/Repository.kt @@ -0,0 +1,143 @@ +package app.controller + +import app.model.repos.BSTRepository +import app.model.repos.RBTRepository +import app.view.Position +import app.view.ScrollDelta +import binarysearchtrees.BinarySearchTree +import binarysearchtrees.binarysearchtree.SimpleBinarySearchTree +import binarysearchtrees.redblacktree.RedBlackTree +import org.neo4j.ogm.config.Configuration + +class Repository() { + private lateinit var bstRepo: BSTRepository + private lateinit var rbtRepo: RBTRepository + + init { + try { + val dirPath = "./BSTRepo" + bstRepo = BSTRepository( + dirPath, + { serializePosition(it) }, + { deserializePosition(it) } + ) + } catch (e: Exception) { + throw Exception("BSTRepo Init Exception: " + e.message) + } + + try { + val configuration = Configuration.Builder() + .uri("bolt://localhost") + .credentials("neo4j", "qwerty") + .build() + rbtRepo = RBTRepository( + configuration, + { serializePosition(it) }, + { deserializePosition(it) } + ) + } catch (e: Exception) { + throw Exception("RBTRepo Init Exception: " + e.message) + } + } + + fun getNames(treeType: TreeType): List { + return when(treeType) { + TreeType.BST -> { + try { + bstRepo.getNames() + } catch (e: Exception) { + throw Exception("BSTRepo GetNames Exception: " + e.message) + } + } + + TreeType.RBT -> { + try { + rbtRepo.getNames() + } catch (e: Exception) { + throw Exception("RBTRepo GetNames Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + } + + fun save(name: String, tree: BinarySearchTree, scrollDelta: ScrollDelta) { + val settingsData = serializeScrollDelta(scrollDelta) + + when (tree) { + is SimpleBinarySearchTree -> { + try { + bstRepo.set(name, tree, settingsData) + } catch (e: Exception) { + throw Exception("BSTRepo Save Exception: " + e.message) + } + } + + is RedBlackTree -> { + try { + rbtRepo.set(name, tree, settingsData) + } catch (e: Exception) { + throw Exception("RBTRepo Save Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + } + + fun get(name: String, treeType: TreeType): Pair, ScrollDelta> { + val (tree: BinarySearchTree, settingsData: String) = when (treeType) { + TreeType.BST -> { + try { + bstRepo.get(name) + } catch (e: Exception) { + throw Exception("BSTRepo Get Exception: " + e.message) + } + } + + TreeType.RBT -> { + try { + rbtRepo.get(name) + } catch (e: Exception) { + throw Exception("RBTRepo Get Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + return Pair(tree, deserializeScrollDelta(settingsData)) + } + + fun delete(name: String, tree: BinarySearchTree): Boolean { + return when (tree) { + is SimpleBinarySearchTree -> { + try { + bstRepo.remove(name) + } catch (e: Exception) { + throw Exception("BSTRepo Delete Exception: " + e.message) + } + } + + is RedBlackTree -> { + try { + rbtRepo.remove(name) + } catch (e: Exception) { + throw Exception("RBTRepo Delete Exception: " + e.message) + } + } + + else -> { + TODO("Not implemented yet") + } + } + } + + enum class TreeType { BST, RBT, AVL } +} \ No newline at end of file From 5affca4351e20355d6cf40101f28c9fe14c7c745 Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Wed, 3 May 2023 02:36:52 +0300 Subject: [PATCH 84/87] fix: Fixed minor flaws. --- app/src/main/kotlin/app/view/MainWindow.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/app/view/MainWindow.kt b/app/src/main/kotlin/app/view/MainWindow.kt index 8b2474e..91840ec 100644 --- a/app/src/main/kotlin/app/view/MainWindow.kt +++ b/app/src/main/kotlin/app/view/MainWindow.kt @@ -39,7 +39,7 @@ fun MainWindow( icon: Painter? = painterResource("treeIcon.png"), state: WindowState = rememberWindowState( position = WindowPosition(alignment = Alignment.Center), - size = DpSize(800.dp, 600.dp), + size = DpSize(1100.dp, 815.dp), ) ) { Window( @@ -193,7 +193,7 @@ fun Panel( modifier = Modifier.width(260.dp).height(45.dp), shape = RoundedCornerShape(5.dp), ) { - Text(text = "reset positions", style = defaultOnPrimaryLargeTextStyle) + Text(text = "Reset Positions", style = defaultOnPrimaryLargeTextStyle) } Spacer(modifier = Modifier.height(20.dp)) @@ -202,7 +202,7 @@ fun Panel( modifier = Modifier.width(260.dp).height(45.dp), shape = RoundedCornerShape(5.dp), ) { - Text(text = "save tree", style = defaultOnPrimaryLargeTextStyle) + Text(text = "Save Tree", style = defaultOnPrimaryLargeTextStyle) } Spacer(modifier = Modifier.height(20.dp)) @@ -211,7 +211,7 @@ fun Panel( modifier = Modifier.width(260.dp).height(45.dp), shape = RoundedCornerShape(5.dp), ) { - Text(text = "delete tree", style = defaultOnPrimaryLargeTextStyle) + Text(text = "Delete Tree", style = defaultOnPrimaryLargeTextStyle) } } } @@ -277,7 +277,7 @@ fun TreeTitle( .padding(defaultPadding * 2) ) { Text( - text = treeType, + text = treeName, style = defaultOnPrimaryLargeTextStyle, modifier = Modifier.align(Alignment.Center) ) @@ -292,8 +292,9 @@ fun TreeTitle( .padding(defaultPadding * 2) ) { Text( - text = treeName, - style = defaultLargeTextStyle + text = treeType, + style = defaultLargeTextStyle, + modifier = Modifier.align(Alignment.Center) ) } } From 156de3b92a638d39f742aaf471373a623d5fea66 Mon Sep 17 00:00:00 2001 From: GryaznovAS Date: Wed, 3 May 2023 02:48:14 +0300 Subject: [PATCH 85/87] fix: Centralized required vertex. --- app/src/main/kotlin/app/view/MainWindow.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/app/view/MainWindow.kt b/app/src/main/kotlin/app/view/MainWindow.kt index 91840ec..6136d54 100644 --- a/app/src/main/kotlin/app/view/MainWindow.kt +++ b/app/src/main/kotlin/app/view/MainWindow.kt @@ -178,8 +178,8 @@ fun Panel( TreeButton("Find") { tree[it]?.let { pos -> - scrollDelta.x = -pos.x - scrollDelta.y = -pos.y + scrollDelta.x = -pos.x + 335.dp + scrollDelta.y = -pos.y + 335.dp } } Spacer(modifier = Modifier.height(20.dp)) From 8cf2191bfb6eb8d0f826a5dbfd75ff7db159dcfc Mon Sep 17 00:00:00 2001 From: Arsene-Baitenov Date: Wed, 3 May 2023 02:54:41 +0300 Subject: [PATCH 86/87] feat: Added tree selection window. --- .../main/kotlin/app/view/SelectionWindow.kt | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 app/src/main/kotlin/app/view/SelectionWindow.kt diff --git a/app/src/main/kotlin/app/view/SelectionWindow.kt b/app/src/main/kotlin/app/view/SelectionWindow.kt new file mode 100644 index 0000000..a63c8d5 --- /dev/null +++ b/app/src/main/kotlin/app/view/SelectionWindow.kt @@ -0,0 +1,264 @@ +package app.view + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberWindowState +import app.controller.Repository + +@Composable +fun SelectionWindow( + getNames: (Repository.TreeType) -> List, + onOpenRequest: (String, Repository.TreeType) -> Unit, + onCloseRequest: () -> Unit, + title: String = "Trees-12", + icon: Painter? = painterResource("treeIcon.png") +) { + Window( + onCloseRequest = onCloseRequest, + title = title, + icon = icon, + state = rememberWindowState( + position = WindowPosition(alignment = Alignment.Center), + size = DpSize(800.dp, 600.dp), + ), + undecorated = true, + resizable = false + ) { + Box(Modifier.fillMaxSize().background(defaultBackground).padding(defaultPadding)) { + Row(Modifier.fillMaxSize()) { + var indicator by remember { mutableStateOf(0) } + Box(Modifier.width(300.dp).fillMaxHeight()) { + Column(Modifier.fillMaxSize()) { + Box( + Modifier.height(290.dp).fillMaxWidth().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Image( + painter = painterResource("treeIcon.png"), + contentDescription = "Logotype", + modifier = Modifier.height(200.dp) + .width(400.dp) + .padding(50.dp) + ) + + Text( + "Trees-12", fontSize = 36.sp, + modifier = Modifier.padding(top = 175.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Text( + "Arsene & Artem", fontSize = 19.sp, + modifier = Modifier.padding(top = 215.dp).width(300.dp), + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } + + Box( + Modifier.fillMaxSize().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Column(Modifier.selectableGroup()) { + Row() { + RadioButton( + selected = (indicator == 1), + onClick = { indicator = 1 } + ) + Box( + modifier = Modifier.height(45.dp).width(245.dp) + .padding(top = defaultPadding * 2, bottom = defaultPadding * 2) + ) { + Text( + text = "Binary Search Tree", + style = defaultLargeTextStyle + ) + } + } + Row() { + RadioButton( + selected = (indicator == 2), + onClick = { indicator = 2 } + ) + Box( + modifier = Modifier.height(45.dp).width(245.dp) + .padding(top = defaultPadding * 2, bottom = defaultPadding * 2) + ) { + Text( + text = "Red Black Tree", + style = defaultLargeTextStyle + ) + } + } + // AVL Tree + } + } + } + } + Box(Modifier.fillMaxSize()) { + Box( + Modifier.fillMaxSize().padding(defaultPadding) + .background( + color = defaultBackground, + shape = RoundedCornerShape(10.dp) + ) + .border( + 2.dp, + defaultBrush, + RoundedCornerShape(10.dp) + ) + ) { + Box(Modifier.padding(top = 60.dp).fillMaxWidth().height(550.dp)) { + when (indicator) { + 1 -> Selection({getNames(Repository.TreeType.BST)}, {onOpenRequest(it, Repository.TreeType.BST)}) + 2 -> Selection({getNames(Repository.TreeType.BST)}, {onOpenRequest(it, Repository.TreeType.BST)}) + //3 -> AVLSelection + } + } + } + } + } + } + + IconButton( + onClick = onCloseRequest, + modifier = Modifier.size(40.dp) + .offset(740.dp, 20.dp) + .background(MaterialTheme.colorScheme.primary, CircleShape) + ) { + Icon( + Icons.Filled.Close, + modifier = Modifier.size(50.dp), + contentDescription = "Close application", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } +} + +@Composable +fun Selection( + getNames: () -> List, + onOpenRequest: (String) -> Unit +) { + val names = getNames() + Column(Modifier.fillMaxSize()) { + var indicator by remember { mutableStateOf(-1) } + var newName by remember { mutableStateOf("") } + + + Box(Modifier.fillMaxWidth().height(80.dp).align(Alignment.CenterHorizontally)) { + Button( + onClick = { onOpenRequest(if (indicator == -1) newName else names[indicator]) }, + modifier = Modifier.width(115.dp).height(45.dp).align(Alignment.Center), + shape = RoundedCornerShape(5.dp), + ) { + Text(text = "Open", style = defaultOnPrimaryLargeTextStyle) + } + + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + val stateVertical = rememberScrollState(0) + val stateHorizontal = rememberScrollState(0) + + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(stateVertical) + .padding(end = 12.dp, bottom = 12.dp) + .horizontalScroll(stateHorizontal) + ) { + Column(Modifier.selectableGroup()) { + Row() { + RadioButton( + selected = (indicator == -1), + onClick = { indicator = -1 } + ) + BasicTextField( + value = newName, + onValueChange = { if (it.length < 20) newName = it }, + modifier = Modifier.width(345.dp).height(45.dp) + .border( + 1.dp, + defaultVVBrush, + RoundedCornerShape(5.dp) + ) + .padding(defaultPadding * 2), + textStyle = defaultLargeTextStyle, + singleLine = true + ) + } + for (i in names.indices) { + Row() { + RadioButton( + selected = (indicator == i), + onClick = { indicator = i } + ) + Box( + modifier = Modifier.height(45.dp).width(345.dp) + .padding(top = defaultPadding * 2, bottom = defaultPadding * 2) + ) { + Text( + text = names[i], + style = defaultLargeTextStyle + ) + } + } + } + } + } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(stateVertical) + ) + HorizontalScrollbar( + modifier = Modifier.align(Alignment.BottomStart) + .fillMaxWidth() + .padding(end = 12.dp), + adapter = rememberScrollbarAdapter(stateHorizontal) + ) + } + } +} \ No newline at end of file From 005a826c96c896fc8b52eb0d3b74b9177fc66f6b Mon Sep 17 00:00:00 2001 From: Arsene Baitenov <91395485+Arsene-Baitenov@users.noreply.github.com> Date: Sun, 4 Jun 2023 22:17:11 +0300 Subject: [PATCH 87/87] fix: Fixed bug with drag of vertex and mistake of vertex text output. --- app/src/main/kotlin/app/view/VertexView.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/app/view/VertexView.kt b/app/src/main/kotlin/app/view/VertexView.kt index 3360f8b..9705455 100644 --- a/app/src/main/kotlin/app/view/VertexView.kt +++ b/app/src/main/kotlin/app/view/VertexView.kt @@ -55,7 +55,7 @@ fun VertexView( CircleShape ) .border(5.dp, brush, CircleShape) - .pointerInput(Unit) { + .pointerInput(vertex) { detectDragGestures { change, dragAmount -> change.consume() vertex.value.x += dragAmount.x.toDp() @@ -97,8 +97,8 @@ private fun VertexText( modifier: Modifier = Modifier, ) { Text( - text = if (text.length < 6) text else (text.drop(text.length - 4) + ".."), + text = if (text.length < 6) text else (text.dropLast(text.length - 4) + ".."), modifier = modifier, style = defaultTextStyle ) -} \ No newline at end of file +}