diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f735b2df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: Run Tests + +on: + [ push, pull_request ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install mypy + run: | + python -m pip install mypy + + - name: Run mypy + run: | + mypy . + + - name: Run tests + run: | + python ./scripts/run_tests.py diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..624cd195 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +exclude = venv/ diff --git a/project/treap.py b/project/treap.py new file mode 100644 index 00000000..35264f00 --- /dev/null +++ b/project/treap.py @@ -0,0 +1,280 @@ +from collections.abc import MutableMapping +from typing import Optional + + +class TreapNode: + """A node in the Treap data structure. + + A TreapNode represents a single node in the Treap, which combines properties + of a binary search tree (BST) and a heap. Each node stores a key, a value, and + references to its left and right children. + + Attributes: + key: The key of the node, used for BST ordering. + value: The value associated with the key, used for heap ordering. + left (Optional[TreapNode]): The left child of the node, or None. + right (Optional[TreapNode]): The right child of the node, or None. + """ + + key = None + value = None + left = None + right = None + + def __init__(self, key, value): + """Initialize a new TreapNode with a key and value. + + Args: + key: The key of the node, used for BST ordering. + value: The value of the node, used for heap ordering. + """ + self.key = key + self.value = value + self.left = None + self.right = None + + +class Treap(MutableMapping): + """A Treap data structure that combines a binary search tree and a heap. + + The Treap (Tree + Heap) is a randomized self-balancing binary search tree where + each node has a key and a value. The keys maintain BST properties (left subtree + keys are less than the node's key, right subtree keys are greater), and the values + maintain heap properties (a parent's value is greater than its children's values). + This implementation inherits from MutableMapping, providing dictionary-like + functionality. + + Attributes: + root (Optional[TreapNode]): The root node of the Treap, or None if the Treap + is empty. + """ + + def __init__(self): + """Initialize an empty Treap.""" + self.root = None + + def __getitem__(self, key): + """Retrieve the value associated with the given key. + + Args: + key: The key to look up in the Treap. + + Returns: + The value associated with the key. + + Raises: + KeyError: If the key is not found in the Treap. + """ + node = self.find(self.root, key) + if node is not None: + return node.value + raise KeyError(f"Key {key} not found in Treap") + + def __setitem__(self, key, value): + """Insert or update a key-value pair in the Treap. + + Args: + key: The key to insert or update. + value: The value to associate with the key. + """ + self.insert_node(key, value) + + def __delitem__(self, key): + """Remove a key-value pair from the Treap. + + Args: + key: The key to remove. + + Raises: + KeyError: If the key is not found in the Treap. + """ + self.root = self.remove(self.root, key) + + def __iter__(self): + """Return an iterator over the keys in the Treap. + + The keys are returned in sorted order (inorder traversal). + + Returns: + An iterator yielding the keys in the Treap. + """ + return self.inorder(self.root) + + def __len__(self): + """Return the number of nodes in the Treap. + + Returns: + int: The number of key-value pairs in the Treap. + """ + return self.count_nodes(self.root) + + def inorder(self, node): + """Perform an inorder traversal of the Treap starting from the given node. + + Args: + node: The root node of the subtree to traverse, or None. + + Yields: + The keys of the nodes in inorder (sorted) order. + """ + if node is not None: + yield from self.inorder(node.left) + yield node.key + yield from self.inorder(node.right) + + def count_nodes(self, node): + """Count the number of nodes in the subtree rooted at the given node. + + Args: + node: The root node of the subtree to count, or None. + + Returns: + int: The number of nodes in the subtree. + """ + if node is None: + return 0 + return 1 + self.count_nodes(node.left) + self.count_nodes(node.right) + + def preorder(self, node: Optional[TreapNode]): + """Perform a preorder traversal of the Treap starting from the given node. + + Args: + node: The root node of the subtree to traverse, or None. + + Yields: + The keys of the nodes in preorder (root, left, right) order. + """ + if node is not None: + yield node.key + yield from self.preorder(node.left) + yield from self.preorder(node.right) + + def postorder(self, node: Optional[TreapNode]): + """Perform a postorder traversal of the Treap starting from the given node. + + Args: + node: The root node of the subtree to traverse, or None. + + Yields: + The keys of the nodes in postorder (left, right, root) order. + """ + if node is not None: + yield from self.postorder(node.left) + yield from self.postorder(node.right) + yield node.key + + def insert_node(self, key, value): + """Insert a new key-value pair into the Treap. + + Args: + key: The key to insert. + value: The value to associate with the key. + """ + new_node = TreapNode(key, value) + if self.root is None: + self.root = new_node + return + else: + left, right = self.split(self.root, key) + self.root = self.merge(left, new_node) + self.root = self.merge(self.root, right) + + def remove(self, root: Optional[TreapNode], key): + """Remove a node with the given key from the subtree. + + Args: + root: The root node of the subtree to remove from, or None. + key: The key to remove. + + Returns: + Optional[TreapNode]: The new root of the subtree after removal, or None + if the subtree is empty. + + Raises: + KeyError: If the key is not found in the Treap. + """ + if root is None: + return None + elif root.key < key: + root.right = self.remove(root.right, key) + elif root.key > key: + root.left = self.remove(root.left, key) + else: + root = self.merge(root.left, root.right) + return root + + def split(self, root: Optional[TreapNode], key): + """Split the Treap into two subtrees based on the given key. + + Args: + root: The root node of the subtree to split, or None. + key: The key to split on. + + Returns: + Tuple[Optional[TreapNode], Optional[TreapNode]]: A tuple of two subtrees: + - The left subtree contains all nodes with keys less than the given key. + - The right subtree contains all nodes with keys greater than or equal + to the given key. + """ + if root is None: + return None, None + elif root.key < key: + (left, right) = self.split(root.right, key) + root.right = left + return root, right + else: + (left, right) = self.split(root.left, key) + root.left = right + return left, root + + def merge(self, left: Optional[TreapNode], right: Optional[TreapNode]): + """Merge two Treaps into one while maintaining BST and heap properties. + + Args: + left: The root of the left Treap, or None. + right: The root of the right Treap, or None. + + Returns: + Optional[TreapNode]: The root of the merged Treap, or None if both inputs + are None. + + Notes: + Assumes that all keys in the left Treap are less than all keys in the right + Treap. The heap property is maintained by comparing the values of the nodes. + """ + if left is None: + return right + elif right is None: + return left + else: + if (left.value is None) | (right.value is None): + return None + + assert left.value is not None + assert right.value is not None + + if left.value > right.value: + left.right = self.merge(left.right, right) + return left + else: + right.left = self.merge(right.left, left) + return right + + def find(self, root: Optional[TreapNode], key): + """Find a node with the given key in the subtree. + + Args: + root: The root node of the subtree to search in, or None. + key: The key to search for. + + Returns: + Optional[TreapNode]: The node with the given key, or None if the key is not + found. + """ + if root is None: + return None + elif root.key < key: + return self.find(root.right, key) + elif root.key > key: + return self.find(root.left, key) + return root diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..eba99035 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +import pytest + +from project.treap import Treap, TreapNode + + +@pytest.fixture +def empty_tree(): + return Treap() + + +@pytest.fixture +def filled_tree(): + tree = Treap() + # Добавляем узлы, чтобы значения value у родителя всегда были больше значений детей + tree.insert_node(10, 100) + tree.insert_node(5, 80) + tree.insert_node(15, 60) + tree.insert_node(7, 90) + tree.insert_node(12, 70) + tree.insert_node(18, 50) + return tree diff --git a/tests/test_basic.py b/tests/test_basic.py deleted file mode 100644 index 4811167b..00000000 --- a/tests/test_basic.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import project # on import will print something from __init__ file - - -def setup_module(module): - print("basic setup module") - - -def teardown_module(module): - print("basic teardown module") - - -def test_1(): - assert 1 + 1 == 2 - - -def test_2(): - assert "1" + "1" == "11" diff --git a/tests/test_treap_contains_len.py b/tests/test_treap_contains_len.py new file mode 100644 index 00000000..92778d68 --- /dev/null +++ b/tests/test_treap_contains_len.py @@ -0,0 +1,19 @@ +import pytest + + +def test_contains_operator(filled_tree, empty_tree): + assert 10 not in empty_tree, "Empty tree should not contain any key" + + assert 10 in filled_tree, "Key 10 should be in the filled tree" + assert 5 in filled_tree, "Key 5 should be in the filled tree" + assert 15 in filled_tree, "Key 15 should be in the filled tree" + assert 7 in filled_tree, "Key 7 should be in the filled tree" + assert 12 in filled_tree, "Key 12 should be in the filled tree" + assert 18 in filled_tree, "Key 18 should be in the filled tree" + assert 100 not in filled_tree, "Key 100 should not be in the filled tree" + + +def test_len_function(filled_tree, empty_tree): + # Проверка количества узлов в дереве + assert len(filled_tree) == 6, "Tree should have 6 nodes" + assert len(empty_tree) == 0, "Empty tree should have 0 nodes" diff --git a/tests/test_treap_delete.py b/tests/test_treap_delete.py new file mode 100644 index 00000000..290e097c --- /dev/null +++ b/tests/test_treap_delete.py @@ -0,0 +1,9 @@ +import pytest + + +def test_delete_item(filled_tree): + # Проверка удаления узла + filled_tree.__delitem__(5) + with pytest.raises(KeyError): + _ = filled_tree[5] + assert filled_tree[10] == 100, "Root node with key 10 should still exist" diff --git a/tests/test_treap_insert_get.py b/tests/test_treap_insert_get.py new file mode 100644 index 00000000..33af29c7 --- /dev/null +++ b/tests/test_treap_insert_get.py @@ -0,0 +1,14 @@ +import pytest + + +def test_insert_and_get_item(empty_tree): + # Проверка вставки и получения значения + empty_tree[10] = 100 + empty_tree[5] = 80 + assert empty_tree[10] == 100, "Value for key 10 should be 100" + assert empty_tree[5] == 80, "Value for key 5 should be 80" + + +def test_get_nonexistent_key(empty_tree): + with pytest.raises(KeyError): + _ = empty_tree[100] diff --git a/tests/test_treap_properties.py b/tests/test_treap_properties.py new file mode 100644 index 00000000..5cdb3469 --- /dev/null +++ b/tests/test_treap_properties.py @@ -0,0 +1,17 @@ +import pytest + + +def test_heap_property(filled_tree): + # Проверка свойства кучи (value родителя > value детей) для Treap + def check_heap_property(node): + if node is None: + return True + if node.left and node.left.value > node.value: + return False + if node.right and node.right.value > node.value: + return False + return check_heap_property(node.left) and check_heap_property(node.right) + + assert check_heap_property( + filled_tree.root + ), "Heap property (parent value > children values) should hold for all nodes" diff --git a/tests/test_treap_traversal.py b/tests/test_treap_traversal.py new file mode 100644 index 00000000..0038b735 --- /dev/null +++ b/tests/test_treap_traversal.py @@ -0,0 +1,24 @@ +import pytest + + +def test_inorder_key_bst(filled_tree): + expected_list = [5, 7, 10, 12, 15, 18] + assert expected_list == list(filled_tree.inorder(filled_tree.root)) + + +def test_inorder_traversal(filled_tree): + # Проверка обхода inorder + expected_inorder = [5, 7, 10, 12, 15, 18] + assert ( + list(filled_tree.inorder(filled_tree.root)) == expected_inorder + ), "Inorder traversal does not match expected output" + + +def test_preorder_traversal(filled_tree): + expected_preorder = [10, 7, 5, 12, 15, 18] + assert list(filled_tree.preorder(filled_tree.root)) == expected_preorder + + +def test_postorder_traversal(filled_tree): + expected_postorder = [5, 7, 18, 15, 12, 10] + assert list(filled_tree.postorder(filled_tree.root)) == expected_postorder