diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..7fcb440 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,29 @@ +name: Gradle Build&Test +on: + push: + branches-ignore: + - main + pull_request: + branches: + - main +jobs: + gradle: + + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + + steps: + + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 11 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Build + run: ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1c583d --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +# idea +.idea +*.iws +*.iml +*.ipr +/out/ +/.idea/ + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build +/gradle/ +/App/test.db diff --git a/App/build.gradle b/App/build.gradle new file mode 100644 index 0000000..783940d --- /dev/null +++ b/App/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version "${KOTLIN_JVM_VERSION}" + id 'application' +} + +application { + mainClass = 'app.AppKt' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(path: ':lib') + + testImplementation "${KOTLIN_TEST_JUNIT}" +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/App/src/main/kotlin/app/App.kt b/App/src/main/kotlin/app/App.kt new file mode 100644 index 0000000..0629a88 --- /dev/null +++ b/App/src/main/kotlin/app/App.kt @@ -0,0 +1,464 @@ +package app + +import databases.avl.Neo4jRepository +import databases.json.RBTBase +import databases.sqlite.BTBase +import exceptions.NodeAlreadyExistsException +import exceptions.NodeNotFoundException +import guiClasses.components.Frame +import guiClasses.components.KeyTextField +import guiClasses.components.MenuClass +import guiClasses.components.TreePanel +import guiControl.painters.AVLPainter +import guiControl.painters.BTPainter +import guiControl.painters.RBTPainter +import nodes.Color +import trees.AVLTree +import trees.BinaryTree +import trees.RBTree +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import java.io.File +import javax.swing.GroupLayout +import javax.swing.JButton +import javax.swing.JFrame +import javax.swing.JOptionPane + + +/** + * Объект, хранящий отдельно каждое из деревьев + * (позволяет параллельно работать сразу со всеми) + */ +private object Trees { + var binTree: BinaryTree = BinaryTree() + var AVLTree: AVLTree = AVLTree() + var RBTree: RBTree = RBTree() +} + +/** + * Возможные виды деревьев, которые доступны пользователю RBTree + */ +enum class TreeTypes { + BINARY, + AVL, + RB, + None +} + +/** + * Константы с сообщениями ошибок, именами файлов бд + */ +object Constants { + const val BinaryBaseName = "Binary Tree Data.db" + const val RBTBaseName = "Red-Black Tree Data.yml" + + const val NotFoundErrorMessage = "Tree node with such key not found" + const val NotChosenErrorMessage = "You must select a tree to perform this action" + const val AlreadyExistsErrorMessage = "Tree node with the same key already exists" + const val InputErrorMessage = "Entered value is not a number or is too large" + const val DataReadError = "Unable to read data from file" + const val TreeAlreadyClearErrorMessage = "There are no more nodes in tree" + +} + +/** + * Дерево, выбранное пользователем на данный момент + */ +private var currentTree: TreeTypes = TreeTypes.None + +private lateinit var treeFrame: JFrame +private lateinit var treePanel: TreePanel + +private lateinit var menuFrame: JFrame +fun main() { + menuFrameInit() + treeFrameInit() + loadDatabase() +} + +/** + * Вытаскивает деревья из баз данных + */ +private fun loadDatabase() { + if (File(Constants.RBTBaseName).exists()) { + try { + val base = RBTBase(Constants.RBTBaseName, + serializeValue = { value -> value?.toString() ?: "null" }, + deserializeValue = { value -> + if (value == "null") + 0 + else + value.toInt() + }, + deserializeKey = { value -> value.toInt() }) + + Trees.RBTree = base.loadTree() + } catch (ex: Exception) { + showMessage(Constants.DataReadError) + } + } + if (File(Constants.BinaryBaseName).exists()) { + try { + val base = BTBase(Constants.BinaryBaseName, + serializeValue = { value -> value?.toString() ?: "null" }, + deserializeValue = { value -> + if (value == "null") + 0 + else + value.toInt() + }, + deserializeKey = { value -> value.toInt() }) + + Trees.binTree = base.loadTree() + } catch (ex: Exception) { + showMessage(Constants.DataReadError) + } + } + try { + val base = Neo4jRepository( + "bolt://localhost:7687", + "neo4j", + "password", + serializeValue = { value -> value?.toString() ?: "null" }, + deserializeValue = { value -> + if (value == "null") + 0 + else + value?.toInt() + }, + deserializeKey = { value -> value.toInt() }) + + val tree = base.loadTree() + + if (tree != null) { + Trees.AVLTree = tree + } + } catch (ex: Exception) { + showMessage(Constants.DataReadError) + } +} + +private fun treeFrameInit() { + treeFrame = Frame("Treeple", 1000, 700, 360, 50) + treePanel = TreePanel() + treeFrame.add(treePanel) + + treeFrame.addComponentListener(object : ComponentAdapter() { + override fun componentResized(componentEvent: ComponentEvent) { + treeRepaint() + } + }) + + Trees.binTree.run { + add(100) + add(120) + add(-10) + } + + Trees.RBTree.run { + add(100) + add(120) + add(-10) + } + + Trees.AVLTree.run { + add(100) + add(120) + add(-10) + } + +} + +/** + * Выводит сообщение об ошибке на экран + */ +private fun showMessage(text: String, frame: JFrame = menuFrame, messageType: Int = JOptionPane.ERROR_MESSAGE) { + JOptionPane.showMessageDialog(frame, text, "An error has occurred", messageType) +} + +private fun treeRepaint() { + when (currentTree) { + TreeTypes.BINARY -> { + if (Trees.binTree.root == null) { + treePanel.clearTree() + return + } + val painter = BTPainter(Trees.binTree, width = treeFrame.width) + treePanel.changeTree(painter.lines, painter.nodes) + } + + TreeTypes.AVL -> { + if (Trees.AVLTree.root == null) { + treePanel.clearTree() + return + } + val painter = AVLPainter(Trees.AVLTree, width = treeFrame.width) + treePanel.changeTree(painter.lines, painter.nodes) + } + + TreeTypes.RB -> { + if (Trees.RBTree.root == null) { + treePanel.clearTree() + return + } + val painter = RBTPainter(Trees.RBTree, width = treeFrame.width) + treePanel.changeTree(painter.lines, painter.nodes) + } + + else -> {} + } +} + +/** + * Заполняет menuFrame компонентами + */ +private fun menuFrameInit() { + menuFrame = Frame("Treeple Menu", 300, 400, 50, 50) + + val addButton = JButton("Add") + val addTextField = KeyTextField(addButton) + + addButton.addActionListener { + if (addTextField.text.toIntOrNull() != null) { + val key = addTextField.text.toInt() + try { + when (currentTree) { + TreeTypes.RB -> Trees.RBTree.add(key) + TreeTypes.BINARY -> Trees.binTree.add(key) + TreeTypes.AVL -> Trees.AVLTree.add(key) + + else -> showMessage(Constants.NotChosenErrorMessage) + } + + } catch (ex: NodeAlreadyExistsException) { + showMessage(Constants.AlreadyExistsErrorMessage) + } + } else + showMessage(Constants.InputErrorMessage) + + addTextField.text = "" + treeRepaint() + } + + val removeButton = JButton("Remove") + val removeTextField = KeyTextField(removeButton) + + removeButton.addActionListener { + if (removeTextField.text.toIntOrNull() != null) { + val key = removeTextField.text.toInt() + try { + when (currentTree) { + TreeTypes.RB -> Trees.RBTree.remove(key) + TreeTypes.BINARY -> Trees.binTree.remove(key) + TreeTypes.AVL -> Trees.AVLTree.remove(key) + + else -> showMessage(Constants.NotChosenErrorMessage) + } + + } catch (ex: NodeNotFoundException) { + showMessage(Constants.NotFoundErrorMessage) + } + } else + showMessage(Constants.InputErrorMessage) + + removeTextField.text = "" + treeRepaint() + } + + val searchButton = JButton("Search") + val searchTextField = KeyTextField(searchButton) + + searchButton.addActionListener { + var origNodeColor: Color = Color.BLACK + if (searchTextField.text.toIntOrNull() != null) { + val key = searchTextField.text.toInt() + try { + when (currentTree) { + TreeTypes.RB -> {var node = Trees.RBTree.search(key); origNodeColor = node.color; node.color = Color.YELLOW} + TreeTypes.BINARY -> {var node = Trees.binTree.search(key); origNodeColor = node.color; node.color = Color.YELLOW} + TreeTypes.AVL -> {var node = Trees.AVLTree.search(key); origNodeColor = node.color; node.color = Color.YELLOW} + + else -> showMessage(Constants.NotChosenErrorMessage) + } + + } catch (ex: NodeNotFoundException) { + showMessage(Constants.NotFoundErrorMessage) + } + } else + showMessage(Constants.InputErrorMessage) + + treeRepaint() + + if (searchTextField.text.toIntOrNull() != null) { + val key = searchTextField.text.toInt() + try { + when (currentTree) { + TreeTypes.RB -> {var node = Trees.RBTree.search(key); node.color = origNodeColor} + TreeTypes.BINARY -> {var node = Trees.binTree.search(key); node.color = origNodeColor} + TreeTypes.AVL -> {var node = Trees.AVLTree.search(key); node.color = origNodeColor} + + else -> showMessage(Constants.NotChosenErrorMessage) + } + + } catch (ex: NodeNotFoundException) { + showMessage(Constants.NotFoundErrorMessage) + } + } else + showMessage(Constants.InputErrorMessage) + searchTextField.text = "" + } + + + val saveButton = JButton("Save") + val clearButton = JButton("Clear") + + clearButton.addActionListener { + when (currentTree) { + TreeTypes.RB -> { + if (Trees.RBTree.root == null) + showMessage(Constants.TreeAlreadyClearErrorMessage) + else + Trees.RBTree = RBTree() + } + + TreeTypes.BINARY -> { + if (Trees.binTree.root == null) + showMessage(Constants.TreeAlreadyClearErrorMessage) + else + Trees.binTree = BinaryTree() + } + + TreeTypes.AVL -> { + if (Trees.AVLTree.root == null) + showMessage(Constants.TreeAlreadyClearErrorMessage) + else + Trees.AVLTree = AVLTree() + } + + else -> showMessage(Constants.NotChosenErrorMessage) + } + treeRepaint() + } + + saveButton.addActionListener { + when (currentTree) { + TreeTypes.BINARY -> { + val base = BTBase(Constants.BinaryBaseName, + serializeValue = { value -> value?.toString() ?: "null" }, + deserializeValue = { value -> + if (value == "null") + 0 + else + value.toInt() + }, + deserializeKey = { value -> value.toInt() }) + base.saveTree(Trees.binTree) + } + + TreeTypes.RB -> { + val base = RBTBase( + Constants.RBTBaseName, + serializeValue = { value -> value?.toString() ?: "null" }, + deserializeValue = { value -> + if (value == "null") + 0 + else + value.toInt() + }, + deserializeKey = { value -> value.toInt() }) + base.saveTree(Trees.RBTree) + } + + TreeTypes.AVL -> { + val base = Neo4jRepository( + "bolt://localhost:7687", + "neo4j", + "password", + serializeValue = { value -> value?.toString() ?: "null" }, + deserializeValue = { value -> + if (value == "null") + 0 + else + value?.toInt() + }, + deserializeKey = { value -> value.toInt() }) + base.saveTree(Trees.AVLTree) + } + + else -> showMessage(Constants.NotChosenErrorMessage) + } + } + + menuFrame.jMenuBar = MenuClass( + onBinSelected = { + currentTree = TreeTypes.BINARY + treeRepaint() + }, + onAVLSelected = { + currentTree = TreeTypes.AVL + treeRepaint() + }, + onRBTSelected = { + currentTree = TreeTypes.RB + treeRepaint() + } + ) + + // contentPane - контейнер для компонентов + val layout = GroupLayout(menuFrame.contentPane) + + layout.autoCreateContainerGaps = true + layout.autoCreateGaps = true + + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.CENTER) + .addGroup( // Группа с кнопками и TextFields + layout.createSequentialGroup() + .addGroup( // Группа с кнопками + // GroupLayout.Alignment.LEADING - выравнивание по левому краю в горизонтальном измерении + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(addButton) + .addComponent(removeButton) + .addComponent(searchButton) + ) + .addGroup( // Группа с TextFields + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(addTextField) + .addComponent(removeTextField) + .addComponent(searchTextField) + ) + ) + .addComponent(saveButton) + .addComponent(clearButton) + ) + + layout.setVerticalGroup( + layout.createSequentialGroup() + .addGroup( + layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(addButton) + .addComponent(addTextField) + ) + + .addGroup( + layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(removeButton) + .addComponent(removeTextField) + ) + + .addGroup( + layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(searchButton) + .addComponent(searchTextField) + ) + + .addGroup( + layout.createSequentialGroup() + .addComponent(saveButton) + .addComponent(clearButton) + ) + + ) + + menuFrame.contentPane.layout = layout +} \ No newline at end of file diff --git a/App/src/main/kotlin/guiClasses/components/Frame.kt b/App/src/main/kotlin/guiClasses/components/Frame.kt new file mode 100644 index 0000000..b168337 --- /dev/null +++ b/App/src/main/kotlin/guiClasses/components/Frame.kt @@ -0,0 +1,29 @@ +package guiClasses.components + +import java.awt.Dimension +import javax.swing.ImageIcon +import javax.swing.JFrame + +class Frame(name: String, width: Int, height: Int, locX: Int, locY: Int) : JFrame() { + init { + // Устанавливаем заголовок окна + title = name + + // Устанавливаем размер окна + setSize(width, height) + minimumSize = Dimension(width, height) + + // Устанавливаем положение окна на экране + setLocation(locX, locY) + + // Установка изображения + val icon = ImageIcon("tree.jpg").image + iconImage = icon + + // Устанавливаем операцию закрытия окна + defaultCloseOperation = EXIT_ON_CLOSE + + // Отображаем окно + isVisible = true + } +} diff --git a/App/src/main/kotlin/guiClasses/components/KeyTextField.kt b/App/src/main/kotlin/guiClasses/components/KeyTextField.kt new file mode 100644 index 0000000..4c3d6e1 --- /dev/null +++ b/App/src/main/kotlin/guiClasses/components/KeyTextField.kt @@ -0,0 +1,21 @@ +package guiClasses.components + +import javax.swing.JButton +import javax.swing.JTextField + +class KeyTextField( + private val button: JButton +) : JTextField() { + + private val textField = JTextField("Key") + init { + textField.also { + addActionListener { + button.doClick() + } + } + + add(textField) + + } +} \ No newline at end of file diff --git a/App/src/main/kotlin/guiClasses/components/MenuClass.kt b/App/src/main/kotlin/guiClasses/components/MenuClass.kt new file mode 100644 index 0000000..3117a83 --- /dev/null +++ b/App/src/main/kotlin/guiClasses/components/MenuClass.kt @@ -0,0 +1,62 @@ +package guiClasses.components + +import java.awt.Color +import javax.swing.JMenu +import javax.swing.JMenuBar +import javax.swing.JMenuItem + +class MenuClass( + private val onBinSelected: () -> Unit, + private val onAVLSelected: () -> Unit, + private val onRBTSelected: () -> Unit + +) : JMenuBar() { + + /** + * Визуально обозначает элементы как неактивные + * */ + private fun resetMenuItemsChoosing() { + menuItems.forEach { it.background = Color.WHITE } + } + + /** + * Визуально обозначает цветом переданный эл-т как выделенный + * */ + private fun updateMenuItemsChoosing(item: JMenuItem) { + item.background = Color.PINK + menuItems.filter { it != item }.forEach { it.background = Color.WHITE } + } + + private val menu = JMenu("Tree select") + private val menuItems = arrayOf( + JMenuItem("Binary Tree"), + JMenuItem("AVL-Tree"), + JMenuItem("Red-black Tree") + ) + + init { + menuItems.forEach { menu.add(it) } + resetMenuItemsChoosing() + + // Слушатель событий для элемента меню "Binary Tree" + menuItems[0].addActionListener { + updateMenuItemsChoosing(menuItems[0]) + onBinSelected() + } + + // Слушатель событий для элемента меню "AVL-Tree" + menuItems[1].addActionListener { + updateMenuItemsChoosing(menuItems[1]) + onAVLSelected() + } + + // Слушатель событий для элемента меню "Red-black Tree" + menuItems[2].addActionListener { + updateMenuItemsChoosing(menuItems[2]) + onRBTSelected() + } + + add(menu) + } + +} \ No newline at end of file diff --git a/App/src/main/kotlin/guiClasses/components/TreePanel.kt b/App/src/main/kotlin/guiClasses/components/TreePanel.kt new file mode 100644 index 0000000..47e89e7 --- /dev/null +++ b/App/src/main/kotlin/guiClasses/components/TreePanel.kt @@ -0,0 +1,91 @@ +package guiClasses.components + +import guiControl.LineView +import guiControl.NodeView +import java.awt.* +import javax.swing.JPanel + +/** + * Фрейм на котором отрисовываются все деревья + */ +class TreePanel( + private val nodeSize: Int = 30, + private val lineColor: Color = Color.BLACK, + size: Dimension = Dimension(100, 700) +) : JPanel() { + + private var linesToDraw: List = listOf() + private var nodesToDraw: List = listOf() + + init { + preferredSize = size + } + + fun changeTree(newLines: List, newNodes: List) { + this.linesToDraw = newLines + this.nodesToDraw = newNodes + + repaint() + } + + fun clearTree() { + linesToDraw = listOf() + nodesToDraw = listOf() + + repaint() + } + + override fun paintComponent(g: Graphics) { + super.paintComponent(g) + + // Сглаживание + val g2d = (g as Graphics2D).also { + val rh = RenderingHints( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON + ) + it.setRenderingHints(rh) + } + + if (nodesToDraw.isEmpty()) { + val x = width / 2 - 90 + val y = height / 2 - 10 + + g2d.color = Color.gray + g2d.font = Font("Tahoma", Font.TRUETYPE_FONT, 24) + g2d.drawString("Tree is empty ;(", x, y) + + return + } + + for (line in linesToDraw) + drawLine(line, g2d) + + for (node in nodesToDraw) + drawNode(node, g2d) + } + + private fun drawNode(node: NodeView, graphics: Graphics2D) { + graphics.color = node.color + + // Рисуем овал (ноду) + graphics.fillOval( + node.point.x - nodeSize / 2, + node.point.y - nodeSize / 2, + nodeSize, + nodeSize + ) + + graphics.color = Color.BLACK + graphics.drawString( + node.value, + node.point.x - node.value.length * 4, + node.point.y + nodeSize + ) + } + + private fun drawLine(line: LineView, graphics: Graphics2D) { + graphics.color = lineColor + graphics.drawLine(line.from.x, line.from.y, line.to.x, line.to.y) + } +} \ No newline at end of file diff --git a/App/src/main/kotlin/guiControl/LineView.kt b/App/src/main/kotlin/guiControl/LineView.kt new file mode 100644 index 0000000..27a6c57 --- /dev/null +++ b/App/src/main/kotlin/guiControl/LineView.kt @@ -0,0 +1,8 @@ +package guiControl + +import java.awt.Point + +data class LineView ( + val from: Point, + val to: Point +) \ No newline at end of file diff --git a/App/src/main/kotlin/guiControl/NodeView.kt b/App/src/main/kotlin/guiControl/NodeView.kt new file mode 100644 index 0000000..e2432cd --- /dev/null +++ b/App/src/main/kotlin/guiControl/NodeView.kt @@ -0,0 +1,10 @@ +package guiControl + +import java.awt.Color +import java.awt.Point + +data class NodeView( + val point: Point, + val color: Color, + val value: String +) \ No newline at end of file diff --git a/App/src/main/kotlin/guiControl/painters/AVLPainter.kt b/App/src/main/kotlin/guiControl/painters/AVLPainter.kt new file mode 100644 index 0000000..0310d0b --- /dev/null +++ b/App/src/main/kotlin/guiControl/painters/AVLPainter.kt @@ -0,0 +1,18 @@ +package guiControl.painters + +import nodes.AVLNode +import nodes.Color +import trees.AVLTree + +class AVLPainter( + tree: AVLTree, + nodeMargin: Int = 30, + nodeSize: Int = 20, + width: Int +) : AbstractPainter, AVLTree>(tree, nodeMargin, nodeSize, width) { + override fun getNodeColor(node: AVLNode): java.awt.Color{ + return if (node.color == Color.YELLOW){ + java.awt.Color.YELLOW + } else java.awt.Color.GRAY + } +} \ No newline at end of file diff --git a/App/src/main/kotlin/guiControl/painters/AbstractPainter.kt b/App/src/main/kotlin/guiControl/painters/AbstractPainter.kt new file mode 100644 index 0000000..bbcfb33 --- /dev/null +++ b/App/src/main/kotlin/guiControl/painters/AbstractPainter.kt @@ -0,0 +1,67 @@ +package guiControl.painters + +import exceptions.NullNodeException +import guiControl.LineView +import guiControl.NodeView +import nodes.AbstractNode +import trees.AbstractTree +import java.awt.Point + +abstract class AbstractPainter, TreeType : AbstractTree>( + tree: TreeType, + private val nodeMargin: Int = 20, + private val nodeSize: Int = 30, + width: Int +) { + val nodes: MutableList = mutableListOf() + val lines: MutableList = mutableListOf() + + init { + getViewNodes(tree.root ?: throw NullNodeException(), width / 2, nodeMargin) + + } + + /** + * Позволяет определить каким цветом рисовать ноду + */ + protected abstract fun getNodeColor(node: NodeType): java.awt.Color + + /** + * Рекурсивно пробегает дерево и заполняет nodes и lines эл-ами для отрисовки + */ + private fun getViewNodes(node: NodeType, x: Int, y: Int, n: Int = 1) { + nodes.add(NodeView(Point(x, y), getNodeColor(node), node.key.toString())) + + var nextX: Int + var nextY: Int + + if (node.left != null) { + nextX = x - (x / (2 * n)) + nextY = y + nodeMargin + nodeSize + + lines.add(LineView(Point(x, y), Point(nextX, nextY))) + + getViewNodes( + node.left ?: throw NullNodeException(), + nextX, + nextY, + n + 1 + ) + } + if (node.right != null) { + + nextX = x + (x / (2 * n)) + nextY = y + nodeMargin + nodeSize + + getViewNodes( + node.right ?: throw NullNodeException(), + nextX, + nextY, + (2 * n) + 1 + ) + + lines.add(LineView(Point(x, y), Point(nextX, nextY))) + } + } + +} \ No newline at end of file diff --git a/App/src/main/kotlin/guiControl/painters/BTPainter.kt b/App/src/main/kotlin/guiControl/painters/BTPainter.kt new file mode 100644 index 0000000..c1f6a1e --- /dev/null +++ b/App/src/main/kotlin/guiControl/painters/BTPainter.kt @@ -0,0 +1,17 @@ +package guiControl.painters + +import nodes.BinaryNode +import nodes.Color +import trees.BinaryTree +class BTPainter( + tree: BinaryTree, + nodeMargin: Int = 30, + nodeSize: Int = 20, + width: Int +) : AbstractPainter, BinaryTree>(tree, nodeMargin, nodeSize, width) { + override fun getNodeColor(node: BinaryNode): java.awt.Color{ + return if (node.color == Color.YELLOW){ + java.awt.Color.YELLOW + } else java.awt.Color.DARK_GRAY + } +} \ No newline at end of file diff --git a/App/src/main/kotlin/guiControl/painters/RBTPainter.kt b/App/src/main/kotlin/guiControl/painters/RBTPainter.kt new file mode 100644 index 0000000..187c627 --- /dev/null +++ b/App/src/main/kotlin/guiControl/painters/RBTPainter.kt @@ -0,0 +1,19 @@ +package guiControl.painters + +import nodes.Color +import nodes.RBNode +import trees.RBTree +class RBTPainter( + tree: RBTree, + nodeMargin: Int = 30, + nodeSize: Int = 20, + width: Int +) : AbstractPainter, RBTree>(tree, nodeMargin, nodeSize, width) { + override fun getNodeColor(node: RBNode): java.awt.Color { + return if (node.color == Color.RED){ + java.awt.Color.RED + } else if (node.color == Color.BLACK){ + java.awt.Color.BLACK + } else java.awt.Color.YELLOW + } +} \ No newline at end of file diff --git a/App/tree.jpg b/App/tree.jpg new file mode 100644 index 0000000..724ed68 Binary files /dev/null and b/App/tree.jpg differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3ad65ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Pogorelov Ilya, Sokolovskiy Ilya, Zaytsev Dmitriy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c625cb6 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Описание проекта + +Этот проект содержит реализацию операций удаления/вставки/поиска в трёх типах деревьев: бинарное дерево поиска, AVL-дерево и красно-черное дерево. + +# Функциональность ++ Создание и управление AVL, RBT и бинарным деревом. ++ Сохранение и загрузка деревьев в базы данных (json - RB, SQLite - Binary, neo4j - AVL). ++ GUI с возможностью *изменять/создавать новые/сохранять* деревья. + +# Запуск +``` +# Необходимо скачать проект с github +git clone https://github.com/spbu-coding-2022/trees-10.git + +# Для сборки используется Gradle +./gradle build + +# Запуск +./gradle App:run +``` +В проекте имеются два модулья - lib и App. lib содержит реализацию деревьев и предназначен для их использования, либо добавления новых. App - модуль, содержащий реализацию GUI. + +# Использование GUI + +В **'Treeple Menu'** выбираете необходимое вам дерево. Чтобы добавить элемент в дерево, введите число(значение) вашего элемента перед кнопкой **'Add'**. После нажатия на кнопку дерево обновится. + +Чтобы удалить элемент из дерева, введите значение вашего элемента перед кнопкой **'Remove'**. После нажатия дерево обновится. + +Чтобы сохранить дерево, нажмите кнопку 'Save'. При повторном запуске приложения ваше сохраненное дерево отобразится автоматически, либо выведется сообщение об ошибке. + +# License + +Проект использует MIT License. Подробная информация: [LICENSE](https://github.com/spbu-coding-2022/trees-10/blob/main/LICENSE). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..736302b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.9' + +services: + neo4j: + image: neo4j:latest + container_name: neo4j + ports: + - "7474:7474" + - "7687:7687" + environment: + NEO4J_AUTH: neo4j/password \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..77b36bc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +KOTLIN_JVM_VERSION=1.8.10 +KOTLIN_TEST_JUNIT=org.jetbrains.kotlin:kotlin-test-junit5 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bdc9a83 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..79a61d4 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 0000000..d4beedd --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version "${KOTLIN_JVM_VERSION}" + id 'org.jetbrains.kotlin.plugin.serialization' version "${KOTLIN_JVM_VERSION}" + id 'org.jetbrains.kotlin.plugin.noarg' version '1.8.10' + id 'java-library' +} + + +repositories { + mavenCentral() +} + + +dependencies { + testImplementation "${KOTLIN_TEST_JUNIT}" + + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' + + implementation("org.xerial:sqlite-jdbc:3.32.3.2") + + implementation("io.github.microutils:kotlin-logging-jvm:2.0.6") + + implementation("org.slf4j:slf4j-simple:1.7.29") + + implementation("org.neo4j:neo4j-ogm-core:4.0.5") + runtimeOnly("org.neo4j:neo4j-ogm-bolt-driver:4.0.5") +} + + +tasks.named('test') { + useJUnitPlatform() + testLogging { + events "PASSED", "SKIPPED", "FAILED" + } +} + + +noArg { + annotation("org.neo4j.ogm.annotation.NodeEntity") + annotation("org.neo4j.ogm.annotation.RelationshipEntity") +} \ No newline at end of file diff --git a/lib/src/main/kotlin/databases/IBase.kt b/lib/src/main/kotlin/databases/IBase.kt new file mode 100644 index 0000000..438dc7a --- /dev/null +++ b/lib/src/main/kotlin/databases/IBase.kt @@ -0,0 +1,29 @@ +package databases + +import java.awt.Point + +/** + * Общий интерфейс для всех баз данных + */ +interface IBase { + + /** + * Сохраняет переданное дерево в базу данных. + */ + fun saveTree(tree: TreeType) + + /** + * Выгружает дерево из базы данных. + */ + fun loadTree(): TreeType + + /** + * Выполняет поиск координаты в сохранённом в базе данных дереве. + */ + fun getPoint(key: K): Point + + /** + * Изменяет координату в сохранённой ноде с указанной координатой. + */ + fun setPoint(key: K, p: Point) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/databases/avl/Neo4jRepository.kt b/lib/src/main/kotlin/databases/avl/Neo4jRepository.kt new file mode 100644 index 0000000..d1c79de --- /dev/null +++ b/lib/src/main/kotlin/databases/avl/Neo4jRepository.kt @@ -0,0 +1,146 @@ +package databases.avl + +import nodes.AVLNode +import org.neo4j.ogm.annotation.* +import org.neo4j.ogm.config.Configuration +import org.neo4j.ogm.cypher.ComparisonOperator +import org.neo4j.ogm.cypher.Filter +import org.neo4j.ogm.session.SessionFactory +import trees.AVLTree +import java.awt.Point + +@NodeEntity +class AVLNodeEntity( + @Property + val key: String, + + @Property + var value: String?, + + @Property + var height: Int, + + @Property + var x: Int = 0, + + @Property + var y: Int = 0, + + @Relationship(type = "LEFT") + var left: AVLNodeEntity? = null, + + @Relationship(type = "RIGHT") + var right: AVLNodeEntity? = null, +) { + @Id + @GeneratedValue + var id: Long? = null +} + +@NodeEntity +class AVLTreeEntity( + @Property + var name: String, + + @Relationship(type = "ROOT") + var root: AVLNodeEntity? = null, +) { + @Id + @GeneratedValue + var id: Long? = null +} + +class Neo4jRepository, V>( + uri: String, + username: String, + password: String, + private val serializeKey: (key: K) -> String = { value -> value.toString() }, + private val deserializeKey: (strKey: String) -> K, + private val serializeValue: (value: V?) -> String = { value -> value.toString() }, + private val deserializeValue: (strValue: String?) -> V? +) { + private val configuration: Configuration = Configuration.Builder() + .uri(uri) + .credentials(username, password) + .build(); + + private val sessionFactory = SessionFactory(configuration, "databases.avl") + private val session = sessionFactory.openSession() + + private fun AVLTree.toEntity(name: String = "Default"): AVLTreeEntity { + return AVLTreeEntity( + name, + root?.toEntity() + ) + } + + private fun AVLNode.toEntity(x: Int = 0, y: Int = 0): AVLNodeEntity { + return AVLNodeEntity( + serializeKey(key), + serializeValue(value), + height, + x, + y, + left?.toEntity(), + right?.toEntity() + ) + } + + private fun AVLTreeEntity.toTree(): AVLTree { + return AVLTree().also { + it.root = this.root?.toNode() + } + } + + private fun AVLNodeEntity.toNode(): AVLNode { + return AVLNode(deserializeKey(key), deserializeValue(value)).also { + it.left = this.left?.toNode() + it.right = this.right?.toNode() + it.height = this.height + } + } + + private fun findTree(name: String = "Default") = session.loadAll( + AVLTreeEntity::class.java, + Filter("name", ComparisonOperator.EQUALS, name), -1 + ) + + fun saveTree(tree: AVLTree, name: String = "Default") { + deleteTree(name) + session.save(tree.toEntity(name)) + } + + fun loadTree(name: String = "Default"): AVLTree? { + val treeEntity = findTree(name).singleOrNull() + + return treeEntity?.toTree() + } + + fun deleteTree(name: String = "Default") { + session.query( + "MATCH (t: AVLTreeEntity {name: \$name})-[*0..]-(x)" + + "DETACH DELETE x", + mapOf("name" to name) + ) + } + + fun getPoint(key: K, name: String = "Default"): Point { + val node = session.queryForObject(AVLNodeEntity::class.java, + "MATCH(t:AVLTreeEntity {name: \$name})" + + "MATCH(n:AVLNodeEntity {key: \$key})<-[*0..]-(t)" + + "return n", + mapOf("name" to name, "key" to serializeKey(key)) + ) + + return Point(node.x, node.y) + } + + fun setPoint(key: K, point: Point, name: String = "Default") { + session.query( + "MATCH(t:AVLTreeEntity {name: \$name})" + + "MATCH(n:AVLNodeEntity {key: \$key})<-[*0..]-(t)" + + "SET n.x = \$x, n.y = \$y", + mapOf("name" to name, "key" to serializeKey(key), "x" to point.x, "y" to point.y) + ) + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/databases/json/JsonRBNode.kt b/lib/src/main/kotlin/databases/json/JsonRBNode.kt new file mode 100644 index 0000000..3f384fb --- /dev/null +++ b/lib/src/main/kotlin/databases/json/JsonRBNode.kt @@ -0,0 +1,15 @@ +package databases.json + +import kotlinx.serialization.Serializable +import nodes.Color + +@Serializable +data class JsonRBNode( + val value: String, + val key: String, + val color: Color, + var x: Int, + var y: Int, + val left: JsonRBNode?, + val right: JsonRBNode? +) \ No newline at end of file diff --git a/lib/src/main/kotlin/databases/json/JsonRBTree.kt b/lib/src/main/kotlin/databases/json/JsonRBTree.kt new file mode 100644 index 0000000..8fde951 --- /dev/null +++ b/lib/src/main/kotlin/databases/json/JsonRBTree.kt @@ -0,0 +1,8 @@ +package databases.json + +import kotlinx.serialization.Serializable + +@Serializable +data class JsonRBTree ( + val root: JsonRBNode? +) \ No newline at end of file diff --git a/lib/src/main/kotlin/databases/json/RBTBase.kt b/lib/src/main/kotlin/databases/json/RBTBase.kt new file mode 100644 index 0000000..4962620 --- /dev/null +++ b/lib/src/main/kotlin/databases/json/RBTBase.kt @@ -0,0 +1,100 @@ +package databases.json + +import databases.IBase +import exceptions.NodeNotFoundException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import nodes.RBNode +import trees.RBTree +import java.awt.Point +import java.io.File + +class RBTBase, V>( + private val dbPath: String, + private val serializeKey: (key: K) -> String = { value -> value.toString() }, + private val deserializeKey: (strKey: String) -> K, + private val serializeValue: (value: V?) -> String = { value -> value.toString() }, + private val deserializeValue: (strValue: String) -> V +) : IBase, K> { + + private var file: File = File(dbPath) + override fun saveTree(tree: RBTree) { + file.printWriter().use { out -> + out.write(tree.convertToJson()) + } + } + + override fun loadTree(): RBTree = Json.decodeFromString(file.readText()).convertToTree() + + override fun setPoint(key: K, p: Point) { + val savedTree = Json.decodeFromString(file.readText()) + + if (savedTree.root?.changeCoordinate(key, p) != true) + throw NodeNotFoundException() + + file.printWriter().use { out -> + out.write(Json.encodeToString(savedTree)) + } + + } + + override fun getPoint(key: K): Point { + return Json.decodeFromString(file.readText()).root?.searchCoordinate(key) + ?: throw NodeNotFoundException() + } + + private fun JsonRBNode.searchCoordinate(key: K): Point? { + if (this.key == key) + return Point(this.x, this.y) + else { + this.left?.searchCoordinate(key)?.also { + return Point(it.x, it.y) + } + this.right?.searchCoordinate(key)?.also { + return Point(it.x, it.y) + } + } + + return null + } + + private fun JsonRBNode.changeCoordinate(key: K, p: Point): Boolean { + return if (this.key == key) { + this.x = p.x + this.y = p.y + true + } else { + if (this.left?.changeCoordinate(key, p) != true) + this.right?.changeCoordinate(key, p) == true + else + true + } + } + + private fun JsonRBTree.convertToTree(): RBTree = + RBTree().also { + it.root = this.root?.convertToNode() + } + + private fun JsonRBNode.convertToNode(): RBNode = + RBNode(deserializeKey(this.key), deserializeValue(this.value)).also { + it.color = this.color + it.left = this.left?.convertToNode() + it.right = this.right?.convertToNode() + } + + private fun RBTree.convertToJson() = Json.encodeToString(JsonRBTree(root?.convertToJson())) + private fun RBNode.convertToJson(x: Int = 0, y: Int = 0): JsonRBNode = + JsonRBNode( + serializeValue(this.value), + serializeKey(this.key), + this.color, + x, + y, + this.left?.convertToJson(), + this.right?.convertToJson() + ) + + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/databases/sqlite/BTBase.kt b/lib/src/main/kotlin/databases/sqlite/BTBase.kt new file mode 100644 index 0000000..2809267 --- /dev/null +++ b/lib/src/main/kotlin/databases/sqlite/BTBase.kt @@ -0,0 +1,136 @@ +package databases.sqlite + +import databases.IBase +import exceptions.NodeNotFoundException +import exceptions.NullNodeException +import nodes.BinaryNode +import trees.BinaryTree +import java.awt.Point +import java.io.Closeable +import java.sql.DriverManager +import java.sql.SQLException + + +class BTBase, V>( + dbPath: String, + private val serializeKey: (key: K) -> String = { value -> value.toString() }, + private val deserializeKey: (strKey: String) -> K, + private val serializeValue: (value: V?) -> String = { value -> value.toString() }, + private val deserializeValue: (strValue: String) -> V +) : IBase, K>, Closeable { + object DbConstants { + const val DB_NAME = "BinaryNodes" + } + + private val connection = DriverManager.getConnection("jdbc:sqlite:$dbPath") + ?: throw SQLException("Cannot connect to database") + private val createBaseStatement by lazy { connection.prepareStatement("CREATE TABLE if not exists ${DbConstants.DB_NAME} (key varchar(255) NOT NULL, value varchar(255), x double NOT NULL, y double NOT NULL);") } + private val addNodeStatement by lazy { connection.prepareStatement("INSERT INTO ${DbConstants.DB_NAME} (key, value, x, y) VALUES (?, ?, ?, ?);") } + private val setPointStatement by lazy { connection.prepareStatement("UPDATE ${DbConstants.DB_NAME} SET x=?, y=? WHERE key=?;") } + private val getPointStatement by lazy { connection.prepareStatement("SELECT x, y FROM ${DbConstants.DB_NAME} WHERE key=?;") } + private val getValueStatement by lazy { connection.prepareStatement("SELECT value FROM ${DbConstants.DB_NAME} WHERE key=?;") } + private val getNodesStatement by lazy { connection.prepareStatement("SELECT key, value, x, y, value FROM ${DbConstants.DB_NAME}") } + private val dropDatabaseStatement by lazy { connection.prepareStatement("DELETE FROM ${DbConstants.DB_NAME};") } + + init { + createBaseStatement.execute() + } + + override fun saveTree(tree: BinaryTree) { + clear() + if (tree.root == null) + return + tree.toList().forEach { + addNode(it) + } + } + + override fun loadTree(): BinaryTree { + val tree = BinaryTree() + + val stateRes = getNodesStatement.executeQuery() + + while (stateRes.next()) + tree.add(deserializeKey(stateRes.getString(1)), deserializeValue(stateRes.getString(2))) + return tree + } + + override fun setPoint(key: K, p: Point) { + if (!nodeExists(key)) + throw NodeNotFoundException() + + setPointStatement.setInt(1, p.x) + setPointStatement.setInt(2, p.y) + setPointStatement.setString(3, serializeKey(key)) + + setPointStatement.execute() + } + + override fun getPoint(key: K): Point { + if (!nodeExists(key)) + throw NodeNotFoundException() + + getPointStatement.setString(1, serializeKey(key)) + + val stateRes = getPointStatement.executeQuery() + + val p = Point() + + while (stateRes.next()) { + p.x = stateRes.getInt(1) + p.y = stateRes.getInt(2) + } + + return p + } + + private fun nodeExists(key: K): Boolean { + getValueStatement.setString(1, serializeKey(key)) + val stateRes = getValueStatement.executeQuery() + + return stateRes.next() + } + + private fun addNode(node: BinaryNode, p: Point = Point(0, 0)) { + addNodeStatement.setString(1, serializeKey(node.key)) + addNodeStatement.setString(2, serializeValue(node.value)) + addNodeStatement.setInt(3, p.x) + addNodeStatement.setInt(4, p.y) + + addNodeStatement.execute() + } + + private fun clear() { + dropDatabaseStatement.execute() + } + + override fun close() { + createBaseStatement.close() + addNodeStatement.close() + getNodesStatement.close() + dropDatabaseStatement.close() + getPointStatement.close() + getValueStatement.close() + setPointStatement.close() + + connection.close() + } + + private fun BinaryTree.toList(): MutableList> = + this.root?.toList(mutableListOf()) ?: throw NullNodeException() + + private fun BinaryNode.toList(list: MutableList>): MutableList> { + var myList: MutableList> = list + + myList.add(this) + this.left?.let { + myList = it.toList(myList) + } + this.right?.let { + myList = it.toList(myList) + } + + return myList + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/exceptions/TreeExceptions.kt b/lib/src/main/kotlin/exceptions/TreeExceptions.kt new file mode 100644 index 0000000..b2190f2 --- /dev/null +++ b/lib/src/main/kotlin/exceptions/TreeExceptions.kt @@ -0,0 +1,12 @@ +package exceptions + +/** + * Общий класс для всех исключений, связанных с деревьями + * */ +open class TreeException(message: String) : Exception(message) + +class NodeNotFoundException() : TreeException("Подходящий для выполнения операции узел дерева не найден") + +class NodeAlreadyExistsException() : TreeException("Узел дерева с таким ключом уже существует") + +class NullNodeException() : TreeException("Узел дерева принял значение null") \ No newline at end of file diff --git a/lib/src/main/kotlin/nodes/AVLNode.kt b/lib/src/main/kotlin/nodes/AVLNode.kt new file mode 100644 index 0000000..f5509ef --- /dev/null +++ b/lib/src/main/kotlin/nodes/AVLNode.kt @@ -0,0 +1,6 @@ +package nodes + +class AVLNode, V>(key: K, value: V?) : AbstractNode>(key, value){ + var height: Int = 1 + override var color: Color = Color.GRAY +} diff --git a/lib/src/main/kotlin/nodes/AbstractNode.kt b/lib/src/main/kotlin/nodes/AbstractNode.kt new file mode 100644 index 0000000..c62772a --- /dev/null +++ b/lib/src/main/kotlin/nodes/AbstractNode.kt @@ -0,0 +1,17 @@ +package nodes + +abstract class AbstractNode, V, node : AbstractNode>(key: K, value: V?) : + Comparable> { + var key: K = key + protected set + var value: V? = value + internal set + var right: node? = null + internal set + var left: node? = null + internal set + abstract var color: Color + override fun compareTo(other: AbstractNode): Int = this.key.compareTo(other.key) +} + +enum class Color { RED, BLACK, YELLOW, DARK_GRAY, GRAY} \ No newline at end of file diff --git a/lib/src/main/kotlin/nodes/BinaryNode.kt b/lib/src/main/kotlin/nodes/BinaryNode.kt new file mode 100644 index 0000000..37f0929 --- /dev/null +++ b/lib/src/main/kotlin/nodes/BinaryNode.kt @@ -0,0 +1,93 @@ +package nodes + +import exceptions.NodeAlreadyExistsException +import exceptions.NodeNotFoundException +import exceptions.NullNodeException + +/** + * Класс узла для бинарного дерева. + * + * Позволяет рекурсивно реализовывать операции *поиска*, *удаления*, *добавления*. + * + * @property key ключ для поиска. + * @property value значение узла. + * @author Dmitriy Zaytsev + */ +class BinaryNode, V>(key: K, value: V?) : + AbstractNode>(key, value) { + override var color: Color = Color.DARK_GRAY + fun search(key: K): BinaryNode? = + when (key.compareTo(this.key)) { + 1 -> this.right?.search(key) + 0 -> this + -1 -> this.left?.search(key) + else -> null + } + fun remove(root: BinaryNode?, key: K): BinaryNode? { + if (key == this.key) { // когда remove вызывается для удаляемой вершины + if (right == null && left == null) + return null // просто стираем ноду + + // Простой случай - если есть только 1 потомок + else if (left == null) + return right + else if (right == null) + return left + + // Случай где есть два потомка + else { + // Находим минимальное дерево + val minNode = findMin(right) ?: throw NullNodeException() + + // Перенимаем его key и value + minNode.let { + this.key = it.key + this.value = it.value + // Удаляем минимальное дерево + this.right = right?.remove(right, it.key) + } + return this + } + + } else { + if (left == null && right == null) + throw NodeNotFoundException() + // Идём дальше по дереву искать что удалить + if (key < this.key) + this.left = left?.remove(left, key) + else + this.right = right?.remove(right, key) + return root + } + } + + fun add(key: K, value: V?) { + val compare = key.compareTo(this.key) + + if (compare == 1) { + if (right == null) + right = BinaryNode(key, value) + else + right?.add(key, value) + } else if (compare == 0) + // Попытка добавления новой ноды с уже существующим в дереве ключом + throw NodeAlreadyExistsException() + else { + if (left == null) + left = BinaryNode(key, value) + else + left?.add(key, value) + } + } + + /** + * @param[node] Узел для которого ищется минимальный эл-т. + * @return Наименьший узел. + */ + private fun findMin(node: BinaryNode?): BinaryNode? { + return if (node?.left != null) + findMin(node.left) + else + node + } +} diff --git a/lib/src/main/kotlin/nodes/RBTNode.kt b/lib/src/main/kotlin/nodes/RBTNode.kt new file mode 100644 index 0000000..859a520 --- /dev/null +++ b/lib/src/main/kotlin/nodes/RBTNode.kt @@ -0,0 +1,8 @@ +package nodes + +class RBNode, V>(key: K, value: V?) : + AbstractNode>(key, value) { + + var parent: RBNode? = null + override var color: Color = Color.BLACK +} diff --git a/lib/src/main/kotlin/trees/AVLTree.kt b/lib/src/main/kotlin/trees/AVLTree.kt new file mode 100644 index 0000000..94d64e5 --- /dev/null +++ b/lib/src/main/kotlin/trees/AVLTree.kt @@ -0,0 +1,148 @@ +package trees + +import exceptions.NodeAlreadyExistsException +import exceptions.NodeNotFoundException +import exceptions.NullNodeException +import nodes.AVLNode +import kotlin.math.max + +class AVLTree, V> : AbstractTree>() { + override fun add(key: K, value: V?) { + fun addRecursive(node: AVLNode? , key: K, value: V?): AVLNode { + if (node == null) { + return AVLNode(key, value) + } + + val delta: Int = key.compareTo(node.key) + + if (delta < 0) { + node.left = addRecursive(node.left , key , value) + } else if(delta > 0) { + node.right = addRecursive(node.right , key , value) + } else { + throw NodeAlreadyExistsException() + } + + return rebalance(node) ?: throw NullNodeException() + } + + root = addRecursive(root , key , value) + } + + override fun remove(key: K) { + fun removeRecursive(node: AVLNode?, key: K): AVLNode? { + if (node == null) { + throw NodeNotFoundException() + } + + if (key < node.key) { + node.left = removeRecursive(node.left , key) + } else if (key > node.key) { + node.right = removeRecursive(node.right , key) + } else { + if (node.left == null) { + return node.right + } else if (node.right == null) { + return node.left + } + + val minNode = findMin(node) ?: throw NullNodeException() + node.value = minNode.value + node.right = removeRecursive(node.right, minNode.key) + + } + + return rebalance(node) ?: throw NullNodeException() + } + + root = removeRecursive(root, key) + } + + override fun search(key: K): AVLNode { + fun searchRecursive(node: AVLNode?, key: K): AVLNode? { + if (node == null) { + return null + } + + return if (key < node.key) { + searchRecursive(node.left, key) + } else if (key > node.key) { + searchRecursive(node.right, key) + } else { + node + } + } + + return searchRecursive(root, key) ?: throw NodeNotFoundException() + } + + private fun findMin(node: AVLNode?): AVLNode? = if (node?.left != null) findMin(node.left) else node + + private fun rebalance(node: AVLNode): AVLNode? { + node.height = 1 + max(getHeight(node.left) , getHeight(node.right)) + + val balance: Int = getBalance(node) + val leftBalance: Int = getBalance(node.left) + val rightBalance: Int = getBalance(node.right) + + if (balance > 1) { + return if (leftBalance >= 0) { + rotateRight(node) + } else{ + node.left = rotateLeft(node.left) + rotateRight(node) + } + } + if (balance < -1) { + return if (rightBalance <= 0) { + rotateLeft(node) + } else { + node.right = rotateRight(node.right) + rotateLeft(node) + } + } + + + return node + } + + private fun getBalance(node: AVLNode?): Int { + if (node == null) { + return 0 + } + return getHeight(node.left) - getHeight(node.right) + } + + private fun getHeight(node: AVLNode?): Int { + if (node == null) { + return 0 + } + return node.height + } + + private fun rotateRight(node: AVLNode?): AVLNode? { + val x = node?.left + val b = x?.right + + x?.right = node + node?.left = b + + node?.height = 1 + max(getHeight(node?.left) , getHeight(node?.right)) + x?.height = 1 + max(getHeight(x?.left) , getHeight(x?.right)) + + return x + } + + private fun rotateLeft(node: AVLNode?): AVLNode? { + val y = node?.right + val b = y?.left + + y?.left = node + node?.right = b + + node?.height = 1 + max(getHeight(node?.left) , getHeight(node?.right)) + y?.height = 1 + max(getHeight(y?.left) , getHeight(y?.right)) + + return y + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/AbstractTree.kt b/lib/src/main/kotlin/trees/AbstractTree.kt new file mode 100644 index 0000000..1bf7649 --- /dev/null +++ b/lib/src/main/kotlin/trees/AbstractTree.kt @@ -0,0 +1,11 @@ +package trees + +import nodes.AbstractNode + +abstract class AbstractTree, V, node : AbstractNode> { + var root : node? = null + internal set + abstract fun search(key : K) : node? + abstract fun add(key : K, value : V? = null) + abstract fun remove(key : K) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/BinaryTree.kt b/lib/src/main/kotlin/trees/BinaryTree.kt new file mode 100644 index 0000000..b334103 --- /dev/null +++ b/lib/src/main/kotlin/trees/BinaryTree.kt @@ -0,0 +1,20 @@ +package trees + +import exceptions.NodeNotFoundException +import nodes.BinaryNode + +class BinaryTree, V> : AbstractTree>() { + override fun search(key : K) : BinaryNode = root?.search(key) + ?: throw NodeNotFoundException() + override fun remove(key: K) { + if (root == null) + throw NodeNotFoundException() + root = root?.remove(this.root, key) + } + override fun add(key : K, value : V?) { + if (root == null) + root = BinaryNode(key, value) + else + root?.add(key, value) + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/RBTree.kt b/lib/src/main/kotlin/trees/RBTree.kt new file mode 100644 index 0000000..ade800f --- /dev/null +++ b/lib/src/main/kotlin/trees/RBTree.kt @@ -0,0 +1,301 @@ +package trees + +import exceptions.NodeAlreadyExistsException +import exceptions.NodeNotFoundException +import exceptions.NullNodeException +import nodes.Color +import nodes.RBNode + + +class RBTree, V> : AbstractTree>() { + override fun search(key: K): RBNode { + var node = root + while (node != null) { + node = when { + key < node.key -> node.left + key > node.key -> node.right + else -> return node //когда нода найдена - возвращаем ее + } + } + throw NodeNotFoundException() + } + + override fun add(key: K, value: V?) { + if (contains(key)) { // проверяем, существует ли нода с таким ключом, если да - выводим сообщение + throw NodeAlreadyExistsException() + } + val newNode = RBNode(key, value) + if (root == null) { // если нет корня - записываем в корень + root = newNode + root?.color = Color.BLACK + return + } + + var node = root + var parent: RBNode? = null + while (node != null) { //спускаемся вниз по дереву поэлементно до null-листа + parent = node + node = if (key < node.key) node.left else node.right + } + + newNode.color = Color.RED + newNode.parent = parent + + if (key < (parent?.key ?: throw NullNodeException())) { //заменяем нужный null-лист элементом + parent.left = newNode + } else { + parent.right = newNode + } + + fixInsert(newNode) //проверяем на баланс + } + + override fun remove(key: K) { + val node = search(key) //проверяем на существование ноду + removeNode(node) + } + + private fun removeNode(node: RBNode) { + var replaceNode2: RBNode + var replaceNode1: RBNode + var yOriginalColor = node.color + if (node.right != null || node.left != null) { + if (node.left == null) { + replaceNode1 = node.right ?: throw NullNodeException() + transplant(node, node.right) + } else if (node.right == null) { + replaceNode1 = node.left ?: throw NullNodeException() + transplant(node, node.left) + } else { + replaceNode2 = node.right ?: throw NullNodeException() + while (replaceNode2.left != null) { + replaceNode2 = replaceNode2.left!! + } + yOriginalColor = replaceNode2.color + replaceNode1 = if (replaceNode2.right != null) { + replaceNode2.right ?: throw NullNodeException() + } else { + replaceNode2 + } + /*if (replaceNode == y) { + replaceNode.parent = y.parent + } else { + if (y.parent == node) { + replaceNode.parent = y + } else { + transplant(y, y.right) + y.right = node.right + y.right?.parent = y + replaceNode.parent = y.parent + } + } + transplant(node, y) + y.right = node.right + y.left = node.left + y.left?.parent = y + y.color = node.color*/ + if (replaceNode2.right != null && replaceNode2.parent != node){ + replaceNode1.parent = replaceNode2 + transplant(replaceNode2, replaceNode2.right) + transplant(node, replaceNode2) + replaceNode2.left = node.left + replaceNode2.right = node.right + replaceNode2.left?.parent = replaceNode2 + replaceNode2.right?.parent = replaceNode2 + replaceNode2.color = node.color + } else if (replaceNode2.right == null && replaceNode2.parent != node) { + replaceNode1.parent = replaceNode2.parent + transplant(node, replaceNode2) + replaceNode2.left = node.left + replaceNode2.right = node.right + replaceNode2.left?.parent = replaceNode2 + replaceNode2.right?.parent = replaceNode2 + replaceNode2.color = node.color + } else if (replaceNode2.right != null){ + replaceNode1.parent = replaceNode2 + transplant(replaceNode2, replaceNode2.right) + transplant(node, replaceNode2) + replaceNode2.left = node.left + replaceNode2.right = node.right + replaceNode2.left?.parent = replaceNode2 + replaceNode2.right?.parent = replaceNode2 + replaceNode2.color = node.color + } else { + replaceNode1.parent = replaceNode2.parent + transplant(node, replaceNode2) + replaceNode2.left = node.left + replaceNode2.left?.parent = replaceNode2 + replaceNode2.color = node.color + } + } + if (yOriginalColor == Color.BLACK) { + fixDelete(replaceNode1) + } + } else { + if (node.parent == null) { + root = null + } else { + if (node.parent?.left == node) { + node.parent?.left = null + } else { + node.parent?.right = null + } + } + } + } + + private fun fixInsert(node: RBNode) { + var newNode = node + while (newNode.parent?.color == Color.RED) { // балансируем, пока идут две красные ноды подряд + if (newNode.parent == newNode.parent?.parent?.left) { //если отец - левый сын дедушки + val uncle = newNode.parent?.parent?.right + if (uncle?.color == Color.RED) { + newNode.parent?.color = Color.BLACK + uncle.color = Color.BLACK + newNode.parent?.parent?.color = Color.RED + newNode = newNode.parent?.parent ?: throw NullNodeException() + } else { // если дядя черный или его нет + if (newNode == newNode.parent?.right) { + newNode = newNode.parent ?: throw NullNodeException() + leftRotate(newNode) + } + newNode.parent?.color = Color.BLACK + newNode.parent?.parent?.color = Color.RED + newNode.parent?.parent?.let { rightRotate(it) } ?: throw NullNodeException() + } + } else { //если отец - правый сын дедушки + val uncle = newNode.parent?.parent?.left + if (uncle?.color == Color.RED) { + newNode.parent?.color = Color.BLACK + uncle.color = Color.BLACK + newNode.parent?.parent?.color = Color.RED + newNode = newNode.parent?.parent ?: throw NullNodeException() + } else { //если дядя черный или его нет + if (newNode == newNode.parent?.left) { + newNode = newNode.parent ?: throw NullNodeException() + rightRotate(newNode) + } + newNode.parent?.color = Color.BLACK + newNode.parent?.parent?.color = Color.RED + newNode.parent?.parent?.let { leftRotate(it) } ?: throw NullNodeException() + } + } + } + root?.color = Color.BLACK + } + + private fun fixDelete(node: RBNode?) { + var x = node + while (x != root && x?.color == Color.BLACK) { + if (x == x.parent!!.left) { + var w = x.parent!!.right ?: throw NullNodeException() + if (w.color == Color.RED) { + w.color = Color.BLACK + x.parent!!.color = Color.RED + leftRotate(x.parent!!) + w = x.parent!!.right ?: throw NullNodeException() + } + if ((w.left?.color ?: Color.BLACK) == Color.BLACK && (w.right?.color ?: Color.BLACK) == Color.BLACK) { + w.color = Color.RED + x = x.parent!! + } else { + if ((w.right?.color ?: Color.BLACK) == Color.BLACK) { + w.left?.color = Color.BLACK + w.color = Color.RED + rightRotate(w) + w = x.parent!!.right ?: throw NullNodeException() + } + w.color = x.parent!!.color + x.parent!!.color = Color.BLACK + w.right?.color = Color.BLACK + leftRotate(x.parent!!) + x = root ?: throw NullNodeException() + } + } else { + var w = x.parent!!.left ?: throw NullNodeException() + if (w.color == Color.RED) { + w.color = Color.BLACK + x.parent!!.color = Color.RED + rightRotate(x.parent!!) + w = x.parent!!.left ?: throw NullNodeException() + } + if ((w.right?.color ?: Color.BLACK) == Color.BLACK && (w.left?.color ?: Color.BLACK) == Color.BLACK) { + w.color = Color.RED + x = x.parent!! + } else { + if ((w.left?.color ?: Color.BLACK) == Color.BLACK) { + w.right?.color = Color.BLACK + w.color = Color.RED + leftRotate(w) + w = x.parent!!.left ?: throw NullNodeException() + } + w.color = x.parent!!.color + x.parent!!.color = Color.BLACK + w.left?.color = Color.BLACK + rightRotate(x.parent!!) + x = root ?: throw NullNodeException() + } + } + } + x?.color = Color.BLACK + root?.color = Color.BLACK + } + + + private fun leftRotate(node: RBNode) { + val rightChild = node.right ?: throw NullNodeException() + node.right = rightChild.left + if (rightChild.left != null) { + rightChild.left!!.parent = node + } + rightChild.parent = node.parent + if (node.parent == null) { + root = rightChild + } else if (node == node.parent!!.left) { + node.parent!!.left = rightChild + } else { + node.parent!!.right = rightChild + } + rightChild.left = node + node.parent = rightChild + } + + private fun rightRotate(node: RBNode) { + val leftChild = node.left ?: throw NullNodeException() + node.left = leftChild.right + if (leftChild.right != null) { + leftChild.right!!.parent = node + } + leftChild.parent = node.parent + if (node.parent == null) { + root = leftChild + } else if (node == node.parent!!.right) { + node.parent!!.right = leftChild + } else { + node.parent!!.left = leftChild + } + leftChild.right = node + node.parent = leftChild + } + + private fun transplant(u: RBNode?, v: RBNode?) { + if (u?.parent == null) { + root = v ?: throw NullNodeException() + } else if (u == u.parent!!.left) { + u.parent!!.left = v ?: throw NullNodeException() + } else { + u.parent!!.right = v ?: throw NullNodeException() + } + v.parent = u?.parent + } + fun contains(key: K): Boolean { + var node = root + while (node != null) { + if (key == node.key) { + return true + } + node = if (key < node.key) node.left else node.right + } + return false + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/trees/AVLTreeTest.kt b/lib/src/test/kotlin/trees/AVLTreeTest.kt new file mode 100644 index 0000000..73c1606 --- /dev/null +++ b/lib/src/test/kotlin/trees/AVLTreeTest.kt @@ -0,0 +1,190 @@ +package trees + +import exceptions.NodeAlreadyExistsException +import exceptions.NodeNotFoundException +import nodes.AVLNode +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import java.time.Duration.ofMillis +import kotlin.math.abs +import kotlin.random.Random +import kotlin.test.BeforeTest + +class AVLTreeTest { + + private lateinit var tree: AVLTree + + @BeforeTest + fun init() { + tree = AVLTree() + } + + @Nested + inner class `Remove check` { + @Test + @DisplayName("Root remove") + fun `Root remove`() { + tree.add(100, "root") + tree.remove(100) + assertEquals(null, tree.testSearch(100)?.value) + } + + @Test + @DisplayName("Non-existence element remove check") + fun `Non-existence element remove check`() { + assertThrows(NodeNotFoundException::class.java) { tree.remove(100) } + } + + @Test + @DisplayName("Simple element remove") + fun `Simple element remove`() { + tree.add(8, "root") + tree.add(10, "a") + tree.add(14, "b") + tree.add(13) + tree.remove(13) + assertAll("elements", + Executable { assertEquals("root", tree.testSearch(8)?.value) }, + Executable { assertEquals("a", tree.testSearch(10)?.value) }, + Executable { assertEquals("b", tree.testSearch(14)?.value) }, + Executable { assertEquals(null, tree.testSearch(13)?.value) }, + Executable { assertEquals(true, invariantCheck(tree.testSearch(10))) } + ) + } + + @Test + @DisplayName("Element with one child node remove") + fun `Element with one child node remove`() { + tree.add(8, "root") + tree.add(10, "a") + tree.add(14) + tree.add(13, "c") + tree.remove(14) + assertAll("elements", + Executable { assertEquals("root", tree.testSearch(8)?.value) }, + Executable { assertEquals("a", tree.testSearch(10)?.value) }, + Executable { assertEquals("c", tree.testSearch(13)?.value) }, + Executable { assertEquals(null, tree.testSearch(14)?.value) }, + Executable { assertEquals(true, invariantCheck(tree.testSearch(10))) } + ) + } + + @Test + @DisplayName("Element with two child nodes remove") + fun `Element with two child nodes remove`() { + tree.add(8, "root") + tree.add(4, "a") + tree.add(1, "b") + tree.add(6, "c") + tree.add(7, "d") + tree.add(3, "e") + + tree.remove(3) + assertAll("elements", + Executable { assertEquals("a", tree.testSearch(4)?.value) }, + Executable { assertEquals("b", tree.testSearch(1)?.value) }, + Executable { assertEquals(null, tree.testSearch(3)?.value) }, + Executable { assertEquals("c", tree.testSearch(6)?.value) }, + Executable { assertEquals("d", tree.testSearch(7)?.value) }, + Executable { assertEquals("root", tree.testSearch(8)?.value) }, + Executable { assertEquals(true, invariantCheck(tree.testSearch(4))) } + ) + } + } + + @Nested + inner class `Add check` { + @Test + @DisplayName("Simple add") + fun `Simple add`() { + tree.add(30, "root") + assertEquals("root", tree.testSearch(30)?.value) + } + + @Test + @DisplayName("Non-existence element remove check") + fun `Non-existence element remove check`() { + tree.add(100) + + assertThrows(NodeAlreadyExistsException::class.java) { tree.add(100) } + } + + @Test + @DisplayName("Left rotation on add") + fun `Left rotation on add`() { + tree.add(1, "root") + tree.add(2, "a") + tree.add(3, "b") + assertEquals("root", tree.testSearch(2)?.left?.value) + assertEquals(true, invariantCheck(tree.testSearch(2))) + } + + @Test + @DisplayName("Right rotation on add") + fun `Right rotation on add`() { + tree.add(3, "root") + tree.add(2, "a") + tree.add(1, "b") + assertEquals("root", tree.testSearch(2)?.right?.value) + assertEquals(true, invariantCheck(tree.testSearch(2))) + } + + @Test + @DisplayName("Multiply add") + fun `Multiply add`() { + assertTimeout(ofMillis(1000)) { + val list: List = (List(100000) { Random.nextInt(1, 100000) }).distinct().toMutableList() + for (item in list) + tree.add(item, "0") + assertEquals(tree.testSearch(list.last())?.value, "0") + } + } + + } + + @Nested + inner class `Search check` { + @Test + @DisplayName("Non-existence element search") + fun `Non-existence element search`() { + assertThrows(NodeNotFoundException::class.java) { + tree.search(100) + } + } + + @Test + @DisplayName("Existence element search") + fun `Existence element search`() { + tree.add(100, "root") + + assertEquals("root", tree.search(100).value) + } + } + + private fun , V> AVLTree.testSearch(key: K): AVLNode? { + fun , V> AVLNode.recursiveSearch(key: K): AVLNode? = + when (key.compareTo(this.key)) { + 1 -> this.right?.recursiveSearch(key) + 0 -> this + -1 -> this.left?.recursiveSearch(key) + else -> null + } + return this.root?.recursiveSearch(key) + } + + private fun getBalance(node: AVLNode?): Int { + return if (node == null) + 0 + else getHeight(node.left) - getHeight(node.right) + } + + private fun getHeight(node: AVLNode?): Int = node?.height ?: 0 + fun invariantCheck(node: AVLNode?): Boolean { + return if (node == null) + true + else abs(getBalance(node)) <= 1 && invariantCheck(node.left) && invariantCheck(node.right) + } +} diff --git a/lib/src/test/kotlin/trees/BinaryTreeTest.kt b/lib/src/test/kotlin/trees/BinaryTreeTest.kt new file mode 100644 index 0000000..80adf4d --- /dev/null +++ b/lib/src/test/kotlin/trees/BinaryTreeTest.kt @@ -0,0 +1,219 @@ +package trees + +import exceptions.NodeAlreadyExistsException +import exceptions.NodeNotFoundException +import nodes.BinaryNode +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import java.time.Duration.ofMillis +import kotlin.random.Random +import kotlin.test.BeforeTest + +class BinaryTreeTest { + + private lateinit var tree: BinaryTree + + @BeforeTest + fun init() { + tree = BinaryTree() + } + + @Nested + inner class `Search check` { + @Test + @DisplayName("Existing elements search") + fun `Existing elements search`() { + tree.add(100, "root") + tree.add(150, "root -> right") + tree.add(50, "root -> left") + tree.add(30, "root -> left -> left") + + assertAll( + Executable { assertEquals("root", tree.search(100).value) }, + Executable { assertEquals("root -> right", tree.search(150).value) }, + Executable { assertEquals("root -> left", tree.search(50).value) }, + Executable { assertEquals("root -> left -> left", tree.search(30).value) }) + } + + @Test + @DisplayName("Not-existent elements search") + fun `Not-existent elements search`() { + assertThrows(NodeNotFoundException::class.java) { tree.search(100) } + } + } + + @Nested + inner class `Remove check` { + + @Test + @DisplayName("Root remove") + fun `Root remove`() { + tree.add(100, "root") + + tree.remove(100) + + assertFalse(tree.nodeExists(100, null)) + } + + @Test + @DisplayName("Node without children remove") + fun `Remove node without children`() { + tree.add(8, "root") + tree.add(10, "a") + tree.add(14, "b") + tree.add(13) + + tree.remove(13) + + // Проверка что каждый assertEquals не выбрасывает исключений + // В случае исключения выведется сообщение о нём + assertAll("elements", + Executable { assertTrue(tree.nodeExists(8, "root")) }, + Executable { assertTrue(tree.nodeExists(10, "a")) }, + Executable { assertTrue(tree.nodeExists(14, "b")) }, + Executable { assertFalse(tree.nodeExists(13, null)) }, + Executable { assertTrue(tree.root?.childrenCheck() ?: false) } + ) + } + + @Test + @DisplayName("Element with one child node remove") + fun `Element with one child node remove`() { + tree.add(8, "root") + tree.add(10, "a") + tree.add(14) + tree.add(13, "c") + + tree.remove(14) + + assertAll("elements", + Executable { assertTrue(tree.nodeExists(8, "root")) }, + Executable { assertTrue(tree.nodeExists(10, "a")) }, + Executable { assertTrue(tree.nodeExists(13, "c")) }, + Executable { assertFalse(tree.nodeExists(14, null)) }, + Executable { assertTrue(tree.root?.childrenCheck() ?: false) } + ) + } + + @Test + @DisplayName("Non-existent element remove") + fun `Non-existent element remove`() { + tree.add(100) + tree.add(120) + tree.add(130) + tree.add(109) + assertThrows(NodeNotFoundException::class.java) { tree.remove(150) } + } + + @Test + @DisplayName("Non-existent root remove") + fun `Non-existent root remove`() { + assertThrows(NodeNotFoundException::class.java) { tree.remove(100) } + } + + @Test + @DisplayName("Element with two child nodes remove") + fun `Element with two child nodes remove`() { + tree.add(8, "root") + tree.add(10, "a") + tree.add(14) + tree.add(13) + tree.add(3, "abc") + tree.add(1, "b") + tree.add(6, "c") + tree.add(4) + tree.add(7, "d") + + tree.remove(3) + + assertAll("elements", + Executable { assertTrue(tree.nodeExists(10, "a")) }, + Executable { assertTrue(tree.nodeExists(1, "b")) }, + Executable { assertFalse(tree.nodeExists(3, "abc")) }, + Executable { assertTrue(tree.nodeExists(6, "c")) }, + Executable { assertTrue(tree.nodeExists(7, "d")) }, + Executable { assertTrue(tree.nodeExists(8, "root")) }, + Executable { assertTrue(tree.root?.childrenCheck() ?: false) } + ) + } + } + + @Nested + inner class `Add Check` { + @Test + @DisplayName("Root add") + fun `Root add`() { + tree.add(30, "root") + + assertTrue(tree.nodeExists(30, "root")) + } + + @Test + @DisplayName("Equal keys add") + fun `Equal keys add`() { + tree.add(100) + + assertThrows(NodeAlreadyExistsException::class.java) { tree.add(100) } + } + + @Test + @DisplayName("Add to different nodes") + fun `Add to different subtrees`() { + tree.add(100, "root") + tree.add(150, "root -> right") + tree.add(80, "root -> left") + + assertAll("elements", + Executable { assertTrue(tree.nodeExists(100, "root")) }, + Executable { assertTrue(tree.nodeExists(150, "root -> right")) }, + Executable { assertTrue(tree.nodeExists(80, "root -> left")) }, + Executable { assertTrue(tree.root?.childrenCheck() ?: false) } + ) + } + + @Test + @DisplayName("Large number of elements add") + fun `Add a large number of elements`() { + assertTimeout(ofMillis(1000)) { + val list: List = (List(100000) { Random.nextInt(1, 100000) }).distinct().toMutableList() + + for (item in list) + tree.add(item, "a") + } + } + } + + /** + * Проверка на то, что левый узел меньше, а правый больше + * @return True - узлы размещены как положено + * */ + private fun , V> BinaryNode.childrenCheck(): Boolean = + (this.left == null || this.left?.compareTo(this) == -1) + && (this.right == null || this.right?.compareTo(this) == 1) + + /** + * Выполняет рекурсивный поиск ноды + * (нужна чтобы тесты не опирались на ф-ию поиска класса BinaryNode) + */ + private fun , V> BinaryNode.recursiveSearch(key: K): BinaryNode? = + when (key.compareTo(this.key)) { + 1 -> this.right?.recursiveSearch(key) + 0 -> this + -1 -> this.left?.recursiveSearch(key) + else -> null + } + + /*** + * Проверяет ноду на существование + * @return True - найден узел с совпадающим ключом и значением + */ + private fun , V> BinaryTree.nodeExists(key: K, value: V?): Boolean { + if (this.root == null) + return false + val searchRes = this.root?.recursiveSearch(key) + return searchRes != null && searchRes.value == value + } +} diff --git a/lib/src/test/kotlin/trees/RBTreeTest.kt b/lib/src/test/kotlin/trees/RBTreeTest.kt new file mode 100644 index 0000000..e480ce6 --- /dev/null +++ b/lib/src/test/kotlin/trees/RBTreeTest.kt @@ -0,0 +1,356 @@ +import exceptions.* +import nodes.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import trees.RBTree +import kotlin.random.Random +import kotlin.test.BeforeTest + +class RBTreeTest { + + private lateinit var tree: RBTree + + @BeforeTest + fun init() { + tree = RBTree() + } + + @Nested + inner class `Search check` { + @Test + @DisplayName("Existing elements search") + fun `Existing elements search`() { + tree.add(100, "root") + tree.add(150, "root -> right") + tree.add(50, "root -> left") + tree.add(30, "root -> left -> left") + + assertEquals("root -> left -> left", tree.search(30).value) + } + + @Test + @DisplayName("Not-existent elements search") + fun `Not-existent elements search`() { + assertThrows(NodeNotFoundException::class.java) { tree.search(100) } + } + } + + @Nested + inner class `Add check` { + @Test + @DisplayName("Root element add") + fun `Root element add`() { + tree.add(100) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Add one element to root") + fun `Add one element to root`() { + tree.add(1) + tree.add(2) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Balance with three elements add check") + fun `Balance with three elements check`() { + tree.add(1) + tree.add(2) + tree.add(3) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Balance and repaint adding check") + fun `Balance and repaint simple check`() { + tree.add(2) + tree.add(1) + tree.add(3) + tree.add(4) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Complex rotate check") + fun `Rotate check`() { + tree.add(2) + tree.add(3) + tree.add(4) + tree.add(5) + tree.add(1) + tree.add(7) + tree.add(6) + tree.add(9) + + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Complex rotate and repaint check") + fun `Rotate and repaint check`() { + tree.add(2) + tree.add(1) + tree.add(4) + tree.add(3) + tree.add(5) + tree.add(6) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Random elements add") + fun `Random elements add`() { + val list: List = (List(100000) { Random.nextInt(1, 100000) }).distinct().toMutableList() + + for (item in list) + tree.add(item, "a") + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Add elements existence check") + fun `New elements existence check`() { + tree.add(100, "root") + tree.add(150) + tree.add(125) + tree.add(200) + tree.add(50) + tree.add(25) + tree.add(60) + + assertAll( + Executable { assertTrue(tree.nodeExists(100, "root")) }, + Executable { assertTrue(tree.nodeExists(150, null)) }, + Executable { assertTrue(tree.nodeExists(125, null)) }, + Executable { assertTrue(tree.nodeExists(200, null)) }, + Executable { assertTrue(tree.nodeExists(50, null)) }, + Executable { assertTrue(tree.nodeExists(25, null)) }, + Executable { assertTrue(tree.nodeExists(60, null)) } + ) + } + + @Test + @DisplayName("Equal keys add") + fun `Equals keys add`() { + tree.add(100) + + assertThrows(NodeAlreadyExistsException::class.java) { tree.add(100) } + } + } + + @Nested + inner class `Remove check` { + @Test + @DisplayName("Root element del") + fun `Root element del`() { + tree.add(100, "root") + tree.add(120) + tree.add(50) + + tree.remove(100) + + assertFalse(tree.nodeExists(100, "root")) + } + + @Test + @DisplayName("Node with no children del") + fun `Node with no children del`() { + tree.add(100, "a") + tree.add(120, "b") + + tree.remove(120) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Node with one children del") + fun `Node with one children del`() { + tree.add(100, "a") + tree.add(120, "b") + + tree.remove(100) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Black node simple remove test") + fun `Black node simple remove test`() { + tree.add(100, "P") + tree.add(120, "S") + tree.add(50, "Sl") + tree.add(125, "Sr") + tree.add(115, "Sr") + + tree.remove(120) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Red node with black children remove test") + fun `Red node with black children remove test`() { + tree.add(13) + tree.add(17) + tree.add(8) + tree.add(25) + tree.add(1) + tree.add(11) + tree.add(15) + tree.add(27) + + tree.remove(17) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Black node with red children remove test") + fun `Black node with red children remove test`() { + tree.add(13) + tree.add(17) + tree.add(8) + tree.add(25) + tree.add(1) + tree.add(11) + + tree.remove(8) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Random tree element del") + @RepeatedTest(10) + fun `Random tree element del`() { + val list: List = (List(1000) { Random.nextInt(1, 100000) }).distinct().toMutableList() + + for (item in list) + tree.add(item, "a") + + tree.remove(list.asSequence().shuffled().first()) + + assertTrue(tree.rulesCheck()) + } + + @Test + @DisplayName("Non-existent element del") + fun `Non-existent element del`() { + assertThrows(NodeNotFoundException::class.java) { tree.remove(100) } + } + } + + /*** + * Проверяет ноду на существование + * @return True - найден узел с совпадающим ключом и значением + */ + private fun , V> RBTree.nodeExists(key: K, value: V?): Boolean { + if (this.root == null) + return false + val searchRes = this.root?.recursiveSearch(key) + return searchRes != null && searchRes.value == value + } + + /** + * Выполняет рекурсивный поиск ноды + * (нужна чтобы тесты не опирались на ф-ию поиска класса BinaryNode) + */ + private fun , V> RBNode.recursiveSearch(key: K): RBNode? = + when (key.compareTo(this.key)) { + 1 -> this.right?.recursiveSearch(key) + 0 -> this + -1 -> this.left?.recursiveSearch(key) + else -> null + } + + /** + * Проверка дерева на соответствие правилам красно-чёрных деревьев + * @return True - узлы размещены как положено + * */ + private fun , V> RBTree.rulesCheck(): Boolean { + + if (this.root?.childrenCheck() == false) + throw TreeException("Неверное расположение узлов, key слева должен быть меньше, а key справа - больше") + if (this.root?.color != Color.BLACK) + throw TreeException("root у RBTree не можешь быть красным") + //if (this.root?.checkEndNodesColor() == false) + //throw TreeException("Конечные узлы RBTree не могут быть красными") + if (this.root?.checkEndNodesColor() == false) + throw TreeException("У красного узла родительский узел может быть только чёрным") + + this.root?.getBlackNodesCount() // Если что само выбросит исключение + + return true + } + + /** + * Проверка на то, что левый узел меньше, а правый больше + * @return True - узлы размещены как положено + * */ + private fun , V> RBNode.childrenCheck(): Boolean = + (this.left == null || this.left?.compareTo(this) == -1) + && (this.right == null || this.right?.compareTo(this) == 1) + && (this.right == null || right?.childrenCheck() == true) + && (this.left == null || left?.childrenCheck() == true) + + /** + * Проверяет, что у каждого красного узла родительский узел - чёрный + * @return True - условие верно + */ + private fun , V> RBNode.checkNodeParentColor(): Boolean { + return if (this.color == Color.RED && this.parent?.color != Color.BLACK) + false + else + (this.right == null || this.right?.checkEndNodesColor() == true) && + (this.left == null || this.left?.checkEndNodesColor() == true) + } + + /** + * @return Число чёрных нод на пути из ноды в лист + */ + private fun , V> RBNode.getBlackNodesCount(count: Int = 0): Int { + var mCount = count + if (this.color == Color.BLACK) + mCount++ + var leftCount = 0 + var rightCount = 0 + if (this.left != null) + leftCount += this.left?.getBlackNodesCount() ?: throw NullNodeException() + if (this.right != null) + rightCount += this.right?.getBlackNodesCount() ?: throw NullNodeException() + + // Теперь смотрим, выполняется ли условие + if (leftCount != rightCount) + throw TreeException("Узел [${this.key}, ${this.value}] содержит разное кол-во чёрных узлов в левом [$leftCount] и правом [$rightCount] поддереве") + + mCount += leftCount + + return mCount + } + + /** + * Проверяет что конечные узлы чёрные + * @return True - всё верно + */ + private fun , V> RBNode.checkEndNodesColor(): Boolean { + return if (this.right == null && this.left == null && this.color == Color.BLACK) + true + else + (this.right == null || this.right?.checkEndNodesColor() == true) && + (this.left == null || this.right?.checkEndNodesColor() == true) + } + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..ed4a47b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'trees-10' +include('lib', "App")