diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/README.md b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/README.md new file mode 100644 index 000000000..d6c5c9425 --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/README.md @@ -0,0 +1,17 @@ +# Reb-Black Tree +Learn a Red-Black tree data structure and build strong skills creating balanced tree implementation 💪 + +### Objectives (at the end of the training you will be able to...) +* understand the **main idea** behind the **Red-Black Tree** data structure ✅ +* get knowledge in one of the tree balancing strategies +* implement rotations and recoloring algorithms +* catch what is different from simple BinarySearchTree and why it's better +* get stronger knowledge in algorithms field + +* The more about Red Black Tree can be found on [Wiki page](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) +--- +#### 🆕 First time here? – [See Introduction](https://github.com/bobocode-projects/java-fundamentals-exercises/tree/main/0-0-intro#introduction) +#### ➡️ Have any feedback? – [Please fill the form ](https://forms.gle/7U9XZHuTtT5xpjXR6) + +## +
\ No newline at end of file diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/pom.xml b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/pom.xml new file mode 100644 index 000000000..5b59da3b3 --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/pom.xml @@ -0,0 +1,15 @@ + + + + 2-0-data-structures-and-algorithms + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + 2-2-7-red-black-tree + + + \ No newline at end of file diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/BinarySearchTree.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/BinarySearchTree.java new file mode 100644 index 000000000..d7462484d --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/BinarySearchTree.java @@ -0,0 +1,32 @@ +package com.bobocode.cs; + +import java.util.function.Consumer; + +public interface BinarySearchTree> { + /** + * insert an element + * @return true if element did not exist in the tree and was inserted successfully + */ + boolean insert(T element); + + /** + * @return true if tree contains element + */ + boolean contains(T element); + + /** + * @return number of elements in the tree + */ + int size(); + + /** + * @return max. number of transition between root node and any other node; 0 - if tree is empty or contains 1 element + */ + int depth(); + + /** + * traverse the tree in element's natural order + * @param consumer accepts ref. to node during traversing + */ + void inOrderTraversal(Consumer consumer); +} diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java new file mode 100644 index 000000000..23f5374cb --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java @@ -0,0 +1,77 @@ +package com.bobocode.cs; + +import java.util.function.Consumer; + +import com.bobocode.util.ExerciseNotCompletedException; + +/** + * {@link RedBlackBinarySearchTree} is an implementation of a {@link BinarySearchTree} that is based on nodes, + * coloring, rotations and recursion. A tree node is represented as a nested class {@link Node}. It holds an element (a value), + * references to the parent node, left/right child nodes and also a field type of {@link Color} which is Enum + * with two possible values RED and BLACK
+ * The BinarySearch tree considered as a RedBlackTree when it meets next properties:
+ * 1. Every {@link Node} is either RED or BLACK {@link Color}
+ * 2. RED node can't have RED children nodes
+ * 3. The root is always BLACK
+ * 4. For each node, all simple paths from the node to descendant leaves(null nodes) contain the same number of BLACK nodes
+ * 5. Every leaf(null node) is null and should be treated as a BLACK node
+ * + * To fulfill these requirements the tree need additional steps after insertion of new node to re-balance itself using + * recoloring of nodes and rotations. Every newly inserted node should be RED before re-balancing. + * + * Red-Black Tree example:
+ *.........................7(B).................................
+ *..................../..........\..............................
+ *.................../............\.............................
+ *.................3(B)............10(R)........................
+ *................/...\........../......\.......................
+ *.............../.....\......../........\......................
+ *.............2(R)....null...9(B)........12(B).................
+ *............/....\........./....\....../....\.................
+ *.........../......\......./......\..../......\................
+ *.........null....null...null...null..null....33(R)............
+ *............................................/....\............
+ * ........................................../......\...........
+ * ........................................null.....null........
+ * + *

+ * TODO: to get the most out of your learning, visit our website + *

+ * + * @param a type of elements that are stored in the tree + * @author Serhii Bondarenko + * @author Taras Boychuk + */ + +public class RedBlackBinarySearchTree> implements BinarySearchTree { + + public static > RedBlackBinarySearchTree of(T... elements) { + throw new ExerciseNotCompletedException(); + } + + @Override + public boolean insert(T element) { + throw new ExerciseNotCompletedException(); + } + + @Override + public boolean contains(T element) { + throw new ExerciseNotCompletedException(); + } + + @Override + public int size() { + throw new ExerciseNotCompletedException(); + } + + @Override + public int depth() { + throw new ExerciseNotCompletedException(); + } + + @Override + public void inOrderTraversal(Consumer consumer) { + throw new ExerciseNotCompletedException(); + } + +} diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java new file mode 100644 index 000000000..9da939c52 --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java @@ -0,0 +1,884 @@ +package com.bobocode.cs; + +import static java.lang.reflect.Modifier.isStatic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.apache.commons.lang3.tuple.Pair; +import org.assertj.core.api.Condition; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; + +import com.bobocode.reflection.ReflectionUtils; + +import lombok.SneakyThrows; + +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +@DisplayName("RedBlack Tree") +class RedBlackBinarySearchTreeTest { + + @Nested + @Order(1) + @DisplayName("1. Nested classes tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class NodeClassTest { + + @Test + @Order(1) + @DisplayName("1. Color is a enum") + @SneakyThrows + void colorIsEnum() { + var colorClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); + + assertTrue(colorClass.isEnum()); + } + + @Test + @Order(2) + @DisplayName("2. Color has two values 'RED' and 'BLACK'") + @SneakyThrows + void colorHasTwoValues() { + var colorClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); + var declaredFields = colorClass.getDeclaredFields(); + + assertThat(declaredFields).hasSize(3); + assertThat(declaredFields).anyMatch(f -> f.getName().equals(COLOR_RED)) + .anyMatch(f -> f.getName().equals(COLOR_BLACK)); + } + + @Test + @Order(3) + @DisplayName("3. Node is a static nested class") + @SneakyThrows + void nodeIsStaticClass() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + + assertTrue(isStatic(nodeClass.getModifiers())); + } + + @Test + @Order(4) + @DisplayName("4. Node class has one type parameter") + @SneakyThrows + void nodeHasOneTypeParameter() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var typeParametersList = nodeClass.getTypeParameters(); + + assertThat(typeParametersList).hasSize(1); + } + + @Test + @Order(5) + @DisplayName("5. Node class has field that holds value object") + @SneakyThrows + void nodeHasElementField() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); + + assertThat(fields).withFailMessage(String.format("Expected one of the %s for value field", + NODE_ELEMENT_FIELD_NAME_LIST)).anyMatch(NODE_ELEMENT_FIELD_NAME_VALIDATOR); + } + + @Test + @Order(6) + @DisplayName("6. Node class has field references to left and right nodes") + @SneakyThrows + void nodeHasLeftAndRightFields() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); + + assertThat(fields).withFailMessage(String.format("Expected the next field names for left and right nodes %s, %s", + NODE_LEFT_FIELD_NAME_LIST, NODE_RIGHT_FIELD_NAME_LIST)) + .anyMatch(NODE_LEFT_FIELD_NAME_VALIDATOR).anyMatch(NODE_RIGHT_FIELD_NAME_VALIDATOR); + } + + @Test + @Order(7) + @DisplayName("7. Node class has 'parent' field") + @SneakyThrows + void nodeHasParentField() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); + + assertThat(fields).withFailMessage("Expected %s for parent field name", NODE_PARENT_FIELD_NAME_LIST) + .anyMatch(NODE_PARENT_FIELD_NAME_VALIDATOR); + } + + @Test + @Order(8) + @DisplayName("8. Node class has 'color' field") + @SneakyThrows + void nodeHasColorField() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); + + assertThat(fields).withFailMessage("Expected %s for color field name", NODE_COLOR_FIELD_NAME_LIST) + .anyMatch(NODE_COLOR_FIELD_NAME_VALIDATOR); + } + } + + + @Nested + @Order(2) + @DisplayName("2. RB-tree fields tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeFieldsTest { + + @Order(1) + @DisplayName("1. RB-tree has 'root' field") + @Test + public void hasRootField() { + var redBlackTreeClass = RedBlackBinarySearchTree.class; + var declaredFields = redBlackTreeClass.getDeclaredFields(); + + assertThat(declaredFields).anyMatch(f -> f.getName().equals(SIZE_FIELD_NAME)); + } + + @Order(2) + @DisplayName("2. RB-tree has 'size' field") + @Test + public void hasSizeField() { + var redBlackTreeClass = RedBlackBinarySearchTree.class; + var declaredFields = redBlackTreeClass.getDeclaredFields(); + + assertThat(declaredFields).anyMatch(f -> f.getName().equals(SIZE_FIELD_NAME)); + } + } + + @Nested + @Order(3) + @DisplayName("3. RB-tree insertion method and re-balancing tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeInsertTest { + + @Order(1) + @DisplayName("1. RB-tree returns true when element is inserted") + @Test + public void returnsTrueWhenElementIsInserted() { + var tree = new RedBlackBinarySearchTree(); + var isInserted = tree.insert(1); + + assertTrue(isInserted); + } + + @Order(2) + @DisplayName("2. RB-tree throws exception when element is null") + @Test + public void throwsExceptionWhenElementIsNull() { + var tree = new RedBlackBinarySearchTree(); + + assertThatNullPointerException().isThrownBy(() -> tree.insert(null)); + } + + @Order(3) + @DisplayName("3. RB-tree returns false when duplicate element is inserted") + @Test + public void returnsFalseWhenInsertedElementIsDuplication() { + var tree = new RedBlackBinarySearchTree(); + var firstInsert = tree.insert(10); + var secondInsert = tree.insert(10); + + assertTrue(firstInsert); + assertFalse(secondInsert); + } + + @Order(4) + @DisplayName("4. RB-tree containing elements after insertion") + @Test + public void elementsContainingInTreeAfterInsertion() { + var tree = new RedBlackBinarySearchTree(); + int first = 42, second = 24, third = 33; + tree.insert(first); + tree.insert(second); + tree.insert(third); + + assertNotNull(getElementFromTree(tree, first)); + assertNotNull(getElementFromTree(tree, second)); + assertNotNull(getElementFromTree(tree, third)); + } + + @Order(5) + @DisplayName("5. RB-tree size growing after insertion") + @Test + public void sizeIsGrowingAfterInsertion() { + var tree = new RedBlackBinarySearchTree(); + int first = 1, second = 2; + tree.insert(first); + tree.insert(second); + + var internalSizeValue = getInternalSizeValue(tree); + assertThat(internalSizeValue).isEqualTo(2); + } + + + @Order(6) + @DisplayName("6. After insertion of a new element the recoloring of the nodes occurs during tree re-balancing") + @Test + public void recoloringOccurs() { + //Insert 10 -> 5 -> 15 + int first = 10, second = 5, third = 15, fourth = 20; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + integerTree.insert(third); + + //The root is black + var rootNode = getNodeByElement(integerTree, first); + assertThat(rootNode.color()).isEqualTo(COLOR_BLACK); + + //Children of the root is RED + var leftChild = rootNode.left(); + var rightChild = rootNode.right(); + assertThat(leftChild.color()).isEqualTo(COLOR_RED); + assertThat(rightChild.color()).isEqualTo(COLOR_RED); + + //Inserting new element(20) should invoke recolouring + //......10(B).......................10(B)......... + //...../.....\...................../....\........ + //....5(R)...15(R)........---->...5(B)...15(B)... + ///............\............................\.... + //..............20(R).......................20(R) + integerTree.insert(fourth); + var insertedNode = getNodeByElement(integerTree, fourth); + + assertThat(rootNode.color()).isEqualTo(COLOR_BLACK); + assertThat(leftChild.color()).isEqualTo(COLOR_BLACK); + assertThat(rightChild.color()).isEqualTo(COLOR_BLACK); + assertThat(insertedNode.color()).isEqualTo(COLOR_RED); + assertThat(rightChild.right().target()).isEqualTo(insertedNode.target()); + } + + @Order(7) + @DisplayName("7. After insertion of a new element the tree is balanced through right rotation") + @Test + public void elementInsertedAsLeftChildOfParentAndParentIsLeftChildOfGrandParent() { + int first = 10, second = 5, third = 0; + + var integerTree = new RedBlackBinarySearchTree(); + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootLeftChild = getNodeByElement(integerTree, second); + assertThat(startingRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootLeftChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.left().target()).isEqualTo(startingRootLeftChild.target()); + + //Inserting new node should invoke right-rotation over a current root node + //Insertion: 10 -> 5 -> 0 + //Re-balancing through right rotation: Rotate right over the node(10). Recolor node(10) and node(5) + //......10(B)............5(B)...... + //...../................/....\..... + //....5(R).......-->...0(R)...10(R) + ///../............................. + //.0(R)............................ + integerTree.insert(third); + var insertedNode = getNodeByElement(integerTree, third); + + //Node(5) should become new root, the newly inserted node will be left child of new root + // and old root becomes right child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(startingRootLeftChild.target()); + assertThat(newRoot.left().target()).isEqualTo(insertedNode.target()); + assertThat(newRoot.right().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + assertThat(insertedNode.color()).isEqualTo(COLOR_RED); + } + + + @Order(8) + @DisplayName("8. After insertion of a new element the tree is balanced through left-right rotation") + @Test + public void elementInsertedAsRightChildOfParentAndParentIsLeftChildOfGrandParent() { + //Insertion: 10 -> 5 -> 7 + int first = 10, second = 5, third = 7; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootLeftChild = getNodeByElement(integerTree, second); + + //Inserting new node should invoke left-right rotation + //Re-balancing through left-right rotation: Rotate left over node(5) and then right rotate over node(10) + //Recolor node(7) and node(10) + //.....10(B)............7(B)...... + //..../................/....\..... + //...5(R).......-->...5(R)...10(R) + //....\........................... + //.....7(R)....................... + integerTree.insert(third); + var newNode = getNodeByElement(integerTree, third); + + // Newly inserted node(7) should become new root, the node(10) will be right child of new root + // and node(5) will be left child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(newNode.target()); + assertThat(newRoot.left().target()).isEqualTo(startingRootLeftChild.target()); + assertThat(newRoot.right().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootLeftChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + } + + + @Order(9) + @DisplayName("9. After insertion of a new element the tree is balanced through left rotation") + @Test + public void elementInsertedAsARightChildOfParentAndParentIsRightChildOfGrandParent() { + //Insertion: 0 -> 10 -> 20 + int first = 0, second = 10, third = 20; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootRightChild = getNodeByElement(integerTree, second); + assertThat(startingRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootRightChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.right().target()).isEqualTo(startingRootRightChild.target()); + + //Inserting new node should invoke left-rotation + //Re-balancing through left rotation: Rotate left over the node(0). Recolor node(0) and node(10) + //.......0(B)...............10(B)..... + //..........\............../....\..... + //.........10(R).....-->..0(R)...20(R) + ///...........\....................... + //............20(R)................... + integerTree.insert(third); + var insertedNode = getNodeByElement(integerTree, third); + + //Node(10) should become new root, node(20) will be right child of new root + // and node(0) becomes left child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(startingRootRightChild.target()); + assertThat(newRoot.right().target()).isEqualTo(insertedNode.target()); + assertThat(newRoot.left().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + assertThat(insertedNode.color()).isEqualTo(COLOR_RED); + } + + + @Order(10) + @DisplayName("10. After insertion of a new element the tree is balanced through right-left rotation") + @Test + public void elementInsertedAsLeftChildOfParentAndParentIsRightChildOfGrandParent() { + //Insertion: 0 -> 10 -> 5 + int first = 0, second = 10, third = 5; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootRightChild = getNodeByElement(integerTree, second); + + //Inserting new node should invoke right-left rotation + //Re-balancing through right-left rotation: Rotate right over the node(10) ant then rotate left over node(0). + // Recolor node(0) and node(5) + //.......0(B)...............5(B)...... + //..........\............../....\..... + //.........10(R).....-->..0(R)...10(R) + ///......./........................... + //......5(R).......................... + integerTree.insert(third); + var newNode = getNodeByElement(integerTree, third); + + // Newly inserted node(5) should become new root, the node(10) will be right child of new root + // and node(0) will be left child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(newNode.target()); + assertThat(newRoot.right().target()).isEqualTo(startingRootRightChild.target()); + assertThat(newRoot.left().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootRightChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + } + + + @Order(11) + @DisplayName("11. Recoloring is invoking re-balance on the top recoloured node") + @Test + public void rotationOccursAfterRecolouring() { + //Insertion -> 5, 10, 17, 15, 25, 20, 28 + var startingValues = List.of(5, 10, 17, 15, 25, 20, 28); + int newValue = 30; + var integerTree = new RedBlackBinarySearchTree(); + for (var value: + startingValues) { + integerTree.insert(value); + } + + //Insertion of new node(30) should invoke re-coloring and then rotation: + //...............10(B)..................................................17(B)......................... + //............../.....\.............................................../........\...................... + //............./.......\............................................./......... \..................... + //...........5(B).....17(R)........................................10(R)....... 25(R)................. + //.................../.....\...............------->............../......\......./....\................ + //................../.......\.................................../........\...../......\............... + //................15(B).....25(B)..............................5(B)....15(B)..20(B)...28(B)........... + //........................./.....\......................................................\............. + //......................../.......\......................................................\............ + //.......................20(R)...28(R)...................................................30(R)........ + //..................................\................................................................. + //...................................\................................................................ + //...................................30(R)............................................................ + integerTree.insert(newValue); + var node30 = getNodeByElement(integerTree, newValue); + var newRoot = getInternalRootField(integerTree); + var node17 = getNodeByElement(integerTree, 17); + var node10 = getNodeByElement(integerTree, 10); + var node25 = getNodeByElement(integerTree, 25); + var node15 = getNodeByElement(integerTree, 15); + var node28 = getNodeByElement(integerTree, 28); + + assertThat(newRoot.target()).isEqualTo(node17.target()); + assertThat(newRoot.left().target()).isEqualTo(node10.target()); + assertThat(newRoot.right().target()).isEqualTo(node25.target()); + assertThat(node10.right().target()).isEqualTo(node15.target()); + assertThat(node30.parent().target()).isEqualTo(node28.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(node10.color()).isEqualTo(COLOR_RED); + assertThat(node25.color()).isEqualTo(COLOR_RED); + assertThat(node15.color()).isEqualTo(COLOR_BLACK); + assertThat(node28.color()).isEqualTo(COLOR_BLACK); + } + + + @Order(12) + @DisplayName("12. RB-tree root is always black for different inputs") + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void rootIsAlwaysBlack(List elementsList) { + var tree = fillTree(elementsList); + NodeProxy root = getInternalRootField(tree); + var color = root.color(); + + assertThat(color).isEqualTo(COLOR_BLACK); + } + + @Order(13) + @DisplayName("13. RB-tree has only RED or BLACK nodes for different inputs") + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void allNodesAreEitherRedOrBlack(List elementsList) { + var tree = fillTree(elementsList); + NodeProxy root = getInternalRootField(tree); + + Condition colorCondition = + new Condition<>(s -> s.equalsIgnoreCase(COLOR_BLACK) || s.equalsIgnoreCase(COLOR_RED), + "Color condition"); + + assertNodeColors(root, colorCondition); + } + + @Order(14) + @DisplayName("14. RB-tree RED nodes doesn't have RED children for different inputs") + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void redNodesDoesntHaveRedChildren(List elementsList) { + var tree = fillTree(elementsList); + NodeProxy root = getInternalRootField(tree); + + Predicate assertionPredicate = n -> n.color().equals(COLOR_BLACK) || (n.left() == null || + n.left().color().equals(COLOR_BLACK) && + (n.right() == null || n.right().color().equals(COLOR_BLACK))); + + traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root, assertionPredicate); + } + + @Order(15) + @DisplayName("15. RB-tree each paths from a node to any of it's descending leaves has the same number " + + "of black nodes for different inputs") + @SneakyThrows + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void allPathsFromRootToLeavesContainsTheSameNumberOfBlackNodes(List elementsList) { + var tree = fillTree(elementsList); + NodeProxy root = getInternalRootField(tree); + + var nodeToDepthPairList = new ArrayList>(); + countDepthForEachLeaf(root, nodeToDepthPairList, 0); + var allLeavesDepth = nodeToDepthPairList.stream().map(Pair::getValue).toList(); + + assertThat(allLeavesDepth).containsOnly(allLeavesDepth.get(0)); + } + + private void assertNodeColors(NodeProxy iterator, Condition colorCondition) { + if (iterator != null) { + assertNodeColors(iterator.left(), colorCondition); + + assertNotNull(iterator.color()); + assertThat((String) iterator.color()).is(colorCondition); + + assertNodeColors(iterator.right(), colorCondition); + } + } + + private void traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(NodeProxy root, Predicate assertionPredicate) { + if (root != null) { + traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root.left(), assertionPredicate); + + assertThat(assertionPredicate).overridingErrorMessage( + String.format("Violation of RB-tree properties: The next RED node {%s} has at least one RED child", + root)).accepts(root); + + traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root.right(), assertionPredicate); + } + } + + private void countDepthForEachLeaf(NodeProxy node, List> nodeToDepthCounter, + int depthCounter) { + if (node != null) { + if (node.color().equals(COLOR_BLACK)) { + ++depthCounter; + } + countDepthForEachLeaf(node.left(), nodeToDepthCounter, depthCounter); + + if (node.left() == null || node.right() == null) { + nodeToDepthCounter.add(Pair.of(node, depthCounter)); + } + + countDepthForEachLeaf(node.right(), nodeToDepthCounter, depthCounter); + } + } + + } + + @Nested + @Order(4) + @DisplayName("4. RB-tree other methods tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeSizeTest { + + private static RedBlackBinarySearchTree stringTree = new RedBlackBinarySearchTree<>(); + + @BeforeAll + public static void prepareTree() { + stringTree.insert("Jon"); + stringTree.insert("Bon"); + stringTree.insert("Jovi"); + } + + @Order(1) + @DisplayName("1. size() method. RB-tree returns size == 0 when tree is empty") + @Test + public void returnsZeroSizeWhenTreeIsEmpty() { + var tree = new RedBlackBinarySearchTree(); + var size = tree.size(); + + assertThat(size).isEqualTo(0); + } + + @Order(2) + @DisplayName("2. size() method. RB-tree returns proper size after insertion") + @Test + public void returnsProperSizeAfterInsertion() { + var tree = new RedBlackBinarySearchTree(); + tree.insert("R2-D2"); + tree.insert("C-3PO"); + tree.insert("Chewie"); + tree.insert("Obi Wan"); + + var size = tree.size(); + + assertThat(size).isEqualTo(4); + } + + + @Order(3) + @DisplayName("3. contains() method. RB-tree returns true when element is present") + @Test + public void returnsTrueWhenElementIsPresent() { + assertTrue(stringTree.contains("Bon")); + } + + @Order(4) + @DisplayName("4. contains() method. RB-tree returns false when element is not present") + @Test + public void returnsFalseWhenElementIsNotPresent() { + assertFalse(stringTree.contains("Betty")); + } + + @Order(5) + @DisplayName("5. contains() method. RB-tree throws exception when searching element is null") + @Test + public void throwsExceptionWhenElementIsNull() { + assertThatNullPointerException().isThrownBy(() -> stringTree.contains(null)); + } + + @Order(6) + @DisplayName("6. of() method. RB-tree of() method creates new tree with proper size") + @Test + public void ofMethodCreatesTreeWithProperSize() { + var stringTree = RedBlackBinarySearchTree.of("Tinky-Winky", "Dipsy", "Laa Laa", "Po"); + + assertThat(stringTree.size()).isEqualTo(4); + + } + + @Order(7) + @DisplayName("7. depth() method. RB-tree returns depth == 0 when tree is empty") + @Test + public void returnsZeroDepthWhenTreeIsEmpty() { + var localTimeRedBlackTree = new RedBlackBinarySearchTree(); + + assertThat(localTimeRedBlackTree.depth()).isEqualTo(0); + } + + @Order(8) + @DisplayName("8. depth() method. RB-tree depth tests") + @ParameterizedTest + @MethodSource("depthTestArguments") + public void returnsProperDepthWithReBalanceAfterInsertion(int depth, Integer... elements) { + var rbTree = RedBlackBinarySearchTree.of(elements); + + assertThat(rbTree.depth()).isEqualTo(depth); + } + + @Order(9) + @DisplayName("9. inOrderTraversal() method. RB-tree has correct storing order") + @SneakyThrows + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void hasCorrectStoredOrder(List elementsList) { + var tree = fillTree(elementsList); + var container = new CopyOnWriteArrayList(); + Consumer consumer = container::add; + tree.inOrderTraversal(consumer); + MatcherAssert.assertThat(container, isInAscendingOrder()); + } + + private > Matcher> isInAscendingOrder() { + return new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(List comparables) { + for (int i = 0; i < comparables.size() - 1; i++) { + if (comparables.get(i).compareTo(comparables.get(i + 1)) >= 0) { + return false; + } + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("The order is not correct. Elements should appear in ascending order"); + } + }; + } + + + public static Stream depthTestArguments() { + return Stream.of(Arguments.of(0, new Integer[] {10}), Arguments.of(1, new Integer[] {1, 2, 3}), + Arguments.of(2, new Integer[] {10, 20, 40, 50}), + Arguments.of(3, new Integer[] {-11, 7, 15, 2, 4, 900, 77, 345, 789, 1000})); + } + + } + + public static final String NODE_CLASS_NAME = "Node"; + public static final String COLOR_CLASS_NAME = "Color"; + public static final String PACKAGE_NAME = "com.bobocode.cs"; + public static final String TEST_CLASS_NAME = "RedBlackBinarySearchTree"; + public static final String SIZE_FIELD_NAME = "size"; + public static final String ROOT_FIELD_NAME = "root"; + public static final List NODE_PARENT_FIELD_NAME_LIST = List.of("parent"); + public static final List NODE_COLOR_FIELD_NAME_LIST = List.of("color"); + public static final List NODE_LEFT_FIELD_NAME_LIST = List.of("left", "leftChild"); + public static final List NODE_RIGHT_FIELD_NAME_LIST = List.of("right", "rightChild"); + public static final List NODE_ELEMENT_FIELD_NAME_LIST = List.of("element", "data", "value", "key"); + public static final Predicate NODE_PARENT_FIELD_NAME_VALIDATOR = f -> NODE_PARENT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_LEFT_FIELD_NAME_VALIDATOR = f -> NODE_LEFT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_RIGHT_FIELD_NAME_VALIDATOR = f -> NODE_RIGHT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_ELEMENT_FIELD_NAME_VALIDATOR = f -> NODE_ELEMENT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_COLOR_FIELD_NAME_VALIDATOR = f -> NODE_COLOR_FIELD_NAME_LIST.contains(f.getName()); + public static final String COLOR_RED = "RED"; + public static final String COLOR_BLACK = "BLACK"; + + + private static class ColorProxy { + + private Class targetClass; + private Object target; + private Method method; + + @SneakyThrows + public ColorProxy(Object target) { + this.targetClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); + this.target = target; + this.method = targetClass.getMethod("name"); + } + + @SneakyThrows + public Object getColor() { + return method.invoke(target); + } + + + + } + private static class NodeProxy { + private Class targetClass; + + private Object target; + private Field elementField; + private Field parentField; + private Field colorField; + private Field leftField; + private Field rightField; + @SneakyThrows + public NodeProxy(Object target) { + Objects.requireNonNull(target); + this.targetClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + this.target = targetClass.cast(target); + this.elementField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_ELEMENT_FIELD_NAME_VALIDATOR); + this.parentField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_PARENT_FIELD_NAME_VALIDATOR); + this.leftField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_LEFT_FIELD_NAME_VALIDATOR); + this.rightField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_RIGHT_FIELD_NAME_VALIDATOR); + this.colorField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_COLOR_FIELD_NAME_VALIDATOR); + } + @SneakyThrows + public Object element() { + return elementField.get(target); + } + + @SneakyThrows + public Object color() { + return new ColorProxy(colorField.get(target)).getColor(); + } + + @SneakyThrows + public NodeProxy parent() { + return Optional.ofNullable(parentField.get(target)) + .map(NodeProxy::new) + .orElse(null); + } + + @SneakyThrows + public NodeProxy left() { + return Optional.ofNullable(leftField.get(target)) + .map(NodeProxy::new) + .orElse(null); + } + + @SneakyThrows + public NodeProxy right() { + return Optional.ofNullable(rightField.get(target)) + .map(NodeProxy::new) + .orElse(null); + } + + public Object target() { + return target; + } + + + } + @SneakyThrows + private NodeProxy getInternalRootField(RedBlackBinarySearchTree tree) { + var node = ReflectionUtils.getAccessibleFieldByPredicate(tree.getClass(), f -> f.getName().equals(ROOT_FIELD_NAME)).get(tree); + return new NodeProxy(node); + } + + @SneakyThrows + private int getInternalSizeValue(RedBlackBinarySearchTree tree) { + return (int) ReflectionUtils.getAccessibleFieldByPredicate(tree.getClass(), f -> f.getName().equals(SIZE_FIELD_NAME)).get(tree); + } + + private > NodeProxy getNodeByElement(RedBlackBinarySearchTree tree, T element) { + var node = getInternalRootField(tree); + while (node != null) { + if (node.element().equals(element)) { + return node; + } else if (((Comparable) node.element()).compareTo(element) < 0) { + node = node.right(); + } else { + node = node.left(); + } + } + return null; + } + + private > T getElementFromTree(RedBlackBinarySearchTree tree, T element) { + var internalRootField = getInternalRootField(tree); + return findElementInTree(internalRootField, element); + } + + private > T findElementInTree(NodeProxy node, T element) { + while (node != null) { + if (node.element().equals(element)) { + return element; + } else if (((Comparable) node.element()).compareTo(element) < 0) { + node = node.right(); + } else { + node = node.left(); + } + } + return null; + } + + private static > RedBlackBinarySearchTree fillTree(List elementList) { + var tree = new RedBlackBinarySearchTree(); + for (var element: + elementList) { + tree.insert(element); + } + return tree; + } + + static class RbTreeArgumentProvider implements ArgumentsProvider { + + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(Arguments.of(List.of(1, 2, 3)), Arguments.of( + List.of("Twilight Sparkle", "Fluttershy", "Pinkie Pie", "Applejack", "Rainbow Dash", + "Princess Luna", "Sunset Shimmer", "Wonderbolts", "Firecracker Burst", "Princess Celestia")), + Arguments.of(List.of(17, 23, 36, 10, 5, 18, 44, 7, 0, 64, 3, 99)), + Arguments.of(List.of(7.62, 5.56, 12.7, 152d, 11.43, 9.0, 155d, 5.45, 75d, 80d)), + Arguments.of( + List.of(DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.TUESDAY, DayOfWeek.SATURDAY, + DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.SUNDAY)), Arguments.of( + IntStream.iterate(-10_000, i -> i + 1).limit(20_000).boxed().toList())); + } + } +} \ No newline at end of file diff --git a/2-0-data-structures-and-algorithms/pom.xml b/2-0-data-structures-and-algorithms/pom.xml index a08d31d22..936f7d0e9 100644 --- a/2-0-data-structures-and-algorithms/pom.xml +++ b/2-0-data-structures-and-algorithms/pom.xml @@ -11,6 +11,7 @@ 2-2-4-linked-list 2-2-5-array-list 2-2-6-binary-search-tree + 2-2-7-red-black-tree 2-2-9-hash-table data-structures-and-algorithms-util diff --git a/java-fundamentals-util/src/main/java/com/bobocode/reflection/ReflectionUtils.java b/java-fundamentals-util/src/main/java/com/bobocode/reflection/ReflectionUtils.java new file mode 100644 index 000000000..a7f707e85 --- /dev/null +++ b/java-fundamentals-util/src/main/java/com/bobocode/reflection/ReflectionUtils.java @@ -0,0 +1,18 @@ +package com.bobocode.reflection; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Predicate; + +public class ReflectionUtils { + + public static Field getAccessibleFieldByPredicate(Class targetClass, Predicate fieldNamePredicate) { + var field = Arrays.stream(targetClass.getDeclaredFields()) + .filter(fieldNamePredicate) + .findFirst() + .orElseThrow(); + field.setAccessible(true); + return field; + } + +}