diff --git a/README.md b/README.md index 518a54e..0b25c99 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ -# data-structures -Data Structures in Python. Code Fellows 401. +# Data-Structures + +**Author**: Chelsea Dole + +**Resources/Shoutouts**: Nathan Moore (lab partner/amigo) + +**Testing Tools**: pytest, pytest-cov + +## Data Structures: + +* **Binary Search Tree** — *a BST is a "tree shaped" data structure containing nodes. Each node can have a maximum of two children or "leaves," and all node values are properly located based on its parents and siblings values. Nodes to the left of the "root"/head node have values smaller than the root. Those to the right have values larger than the root. There are no duplicate values.* + +## Time Complexities: + +* balance() = *This BST function returns the balance (or size difference) between the left and right parts of the tree. Its runtime is O(1), because it always takes the same amount of time to run regardless of tree size, and only performs simple subtraction.* + +* size() = *This BST function returns the number of nodes/leaves in a tree. Its runtime is O(1), because runtime never changes regardless of tree size. It only returns the value of tree_size, which is created at BST initialization, and changed during the insertion of nodes.* + +* insert() = *This BST function inserts a new node into a tree, and uses a helper function called find_home() to find its correctly sorted place in the tree. This function is, depending on the tree, anywhere between O(logn) and O(n), if it's a relatively balanced tree, every decision will reduce the number of nodes one has to traverse. But if it's a one-sided tree, one may look over every node -- making it O(n).* + +* search() = *This BST function is a reference to check_for_equivalence(), which is recursive, and has a runtime of O(n^2), because every time you're re-calling check_for_equivalence, it looks at every node's equivalence.* + +* contains() = *This BST function looks at search(), described above, and for the same reasons has runtime of O(n^2).* + +* depth() = *This BST function returns the number of "levels" that the tree has, by finding which of the two sides has the greatest depth, and returning that. It has a runtime of O(1), because no matter the size of the tree, it only performs a comparison operation.* + +* in_order() = *This BST traversal function traverses the tree and returns a generator that outputs the node values in numerical order. It has a runtime of O(n), not because you visit every node once (you visit them more than once here) but because the work you do/time you take is constant and grows constantly per node addition.* + +* pre_order() = *This BST traversal function returns a generator that outputs the node values in order of the furthest left parent, its left child, then its right child. This traveral then backs up to the parent, and repeats until the whole tree has been traversed. Like in_order, it has a runtime of O(n), not because you visit every node once (you visit them more than once here) but because the work you do/time you take is constant and grows constantly per node addition.* + +* post_order() = *This BST traversal function returns a generator that outputs the node values in order of the bottom-most left node, the bottom-most right node, and then those nodes' parent. Then it backs up, and repeats this action with the parent as a new child node, until the whole tree has been traversed. Like in_order and pre_order, it has a runtime of O(n), not because you visit every node once (you visit them more than once here) but because the work you do/time you take is constant and grows constantly per node addition.* + +* breadth_first() = *This BST traversal returns a generator that outputs the node values in order of their "levels". It produces first the root, then all nodes (left to right) in the first depth level, then all nodes (left to right) in the second depth level, et cetera. Like in_order, pre_order, and post_order, it has a runtime of O(n), not because you visit every node once (you visit them more than once here) but because the work you do/time you take is constant and grows constantly per node addition.* + +* delete() = *This BST method deletes a node if it exists, and returns None if it does not exist. It also rebalances the BST (using the internal _rebalance() method, and resets variables for tree_size. Its time complexity is O(n), because worst case scenario the node to delete is at the very end of a imbalanced tree, and therefore it must go through every node.* + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6751995 --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +"""Setup module for Chelsea's data structures.""" + +from setuptools import setup + +setup( + name='Data structures', + description='Various data structures in python', + author='Chelsea Dole', + author_email='chelseadole@gmail', + package_dir={' ': 'src'}, + py_modules=['bst'], + install_requires=['timeit'], + extras_require={ + 'test': ['pytest', 'pytest-cov', 'pytest-watch', 'tox'], + 'development': ['ipython']}) diff --git a/src/bst.py b/src/bst.py new file mode 100644 index 0000000..8b3d7eb --- /dev/null +++ b/src/bst.py @@ -0,0 +1,482 @@ +"""Implementation of Binary Search Tree data structure.""" + + +class Node(object): + """Define the Node-class object.""" + + def __init__(self, value, left=None, right=None, parent=None): + """Constructor for the Node class.""" + self.val = value + self.left = left + self.right = right + self.parent = parent + self.depth = 0 + + +class BST(object): + """Define the BST-class object.""" + + def __init__(self, starting_values=None): + """Constructor for the BST class.""" + self.tree_size = 0 + self.left_depth = 0 + self.right_depth = 0 + self.visited = [] + self.max_depth_reached = 0 + + if starting_values is None: + self.root = None + + elif isinstance(starting_values, (list, str, tuple)): + self.root = Node(starting_values[0]) + self.tree_size += 1 + for i in range(len(starting_values) - 1): + self.insert(starting_values[i + 1]) + + else: + raise TypeError('Only iterables or None\ + are valid parameters!') + + def balance(self, starting_node=None): + """Return the current balance of a tree or subtree.""" + if not starting_node: + return self.right_depth - self.left_depth + + r_depth = 0 + l_depth = 0 + + if starting_node.right: + r_depth += self._reassess_depths(starting_node.right) + + if starting_node.left: + l_depth += self._reassess_depths(starting_node.left) + + return r_depth - l_depth + + def size(self): + """Return the current size of the BST.""" + return self.tree_size + + def insert(self, value): + """Insert a new node into the BST, and adjust the balance.""" + new_node = Node(value) + self.tree_size += 1 + + if self.root: + if new_node.val > self.root.val: + if self.root.right: + self._find_home(new_node, self.root.right) + if new_node.depth > self.right_depth: + self.right_depth = new_node.depth + else: + new_node.parent = self.root + self.root.right = new_node + self.root.right.depth = 1 + if self.root.right.depth > self.right_depth: + self.right_depth = self.root.right.depth + + elif new_node.val < self.root.val: + if self.root.left: + self._find_home(new_node, self.root.left) + if new_node.depth > self.left_depth: + self.left_depth = new_node.depth + else: + new_node.parent = self.root + self.root.left = new_node + self.root.left.depth = 1 + if self.root.left.depth > self.left_depth: + self.left_depth = self.root.left.depth + + else: + self.root = new_node + + def _find_home(self, node_to_add, node_to_check): + """. + Check if the node_to_add belongs on the left or right + of the node_to_check, then place it there if that spot is empty, + otherwise recur. + """ + if node_to_add.val > node_to_check.val: + if node_to_check.right: + self._find_home(node_to_add, node_to_check.right) + else: + node_to_add.parent = node_to_check + node_to_check.right = node_to_add + node_to_check.right.depth = node_to_check.depth + 1 + # self._rebalance(node_to_add.parent) + + elif node_to_add.val < node_to_check.val: + if node_to_check.left: + self._find_home(node_to_add, node_to_check.left) + else: + node_to_add.parent = node_to_check + node_to_check.left = node_to_add + node_to_check.left.depth = node_to_check.depth + 1 + # self._rebalance(node_to_add.parent) + + def search(self, value): + """If a value is in the BST, return its node.""" + return self._check_for_equivalence(value, self.root) + + def contains(self, value): + """Return whether or not a value is in the BST.""" + return bool(self.search(value)) + + def _check_for_equivalence(self, value, node_to_check): + """. + Check if the value matches that of the node_to_check + if it does, return the node. If it doesn't, go left or right + as appropriate and recur. If you reach a dead end, return None. + """ + try: + if value == node_to_check.val: + return node_to_check + + except AttributeError: + return None + + if value > node_to_check.val and node_to_check.right: + return self._check_for_equivalence(value, node_to_check.right) + + elif value < node_to_check.val and node_to_check.left: + return self._check_for_equivalence(value, node_to_check.left) + + def depth(self, starting_node=None): + """Return the depth of the tree or subtree.""" + if not starting_node: + if self.left_depth > self.right_depth: + return self.left_depth + return self.right_depth + + def in_order(self): + """Return a generator to perform an in-order traversal.""" + self.visited = [] + + if self.root is None: + raise IndexError("Tree is empty!") + + gen = self._in_order_gen() + return gen + + def _in_order_gen(self): + """Recursive helper method for in-order traversal.""" + current = self.root + + while len(self.visited) < self.tree_size: + if current.left: + if current.left.val not in self.visited: + current = current.left + continue + + if current.val not in self.visited: + self.visited.append(current.val) + yield current.val + + if current.right: + if current.right.val not in self.visited: + current = current.right + continue + + current = current.parent + + def pre_order(self): + """Return a generator to perform an pre-order traversal.""" + self.visited = [] + + if self.root is None: + raise IndexError("Tree is empty!") + + gen = self._pre_order_gen() + return gen + + def _pre_order_gen(self): + """Recursive helper method for pre-order traversal.""" + current = self.root + + while len(self.visited) < self.tree_size: + if current.val not in self.visited: + self.visited.append(current.val) + yield current.val + + if current.left: + if current.left.val not in self.visited: + current = current.left + continue + + if current.right: + if current.right.val not in self.visited: + current = current.right + continue + + current = current.parent + + def post_order(self): + """Return a generator to perform an post-order traversal.""" + self.visited = [] + + if self.root is None: + raise IndexError("Tree is empty!") + + gen = self._post_order_gen() + return gen + + def _post_order_gen(self): + """Recursive helper method for post-order traversal.""" + current = self.root + + while len(self.visited) < self.tree_size: + if current.left: + if current.left.val not in self.visited: + current = current.left + continue + + if current.right: + if current.right.val not in self.visited: + current = current.right + continue + + if current.val not in self.visited: + self.visited.append(current.val) + yield current.val + + current = current.parent + + def breadth_first(self): + """Return a generator to perform a breadth-first traversal.""" + self.visited = [] + + if self.root is None: + raise IndexError("Tree is empty!") + + gen = self._breadth_first_gen(self.root) + return gen + + def _breadth_first_gen(self, root_node): + """Helper generator for breadth-first traversal.""" + queue = [self.root] + while queue: + current = queue[0] + yield current.val + queue = queue[1:] + + if current not in self.visited: + self.visited.append(current) + + if current.left: + if current.left not in self.visited: + queue.append(current.left) + + if current.right: + if current.right not in self.visited: + queue.append(current.right) + + def delete(self, val): + """Delete the node with the given value from the tree.""" + node = self.search(val) + if node is None: + return + + self.tree_size -= 1 + is_root = node == self.root + first_move_right = val > self.root.val + if node.parent: + last_move_right = node == node.parent.right + else: + last_move_right = False + + if node.left is None and node.right is None: + if is_root: + self.root = None + return + if last_move_right: + node.parent.right = None + # self._rebalance(node.parent) + return + node.parent.left = None + # self._rebalance(node.parent) + return + + if node.left is None: + if is_root: + node.right.parent = None + self.root = node.right + self.root.depth = 0 + self.right_depth = self._reassess_depths(self.root.right) + return + if last_move_right: + node.parent.right = node.right + node.right.parent = node.parent + + else: + node.parent.left = node.right + node.right.parent = node.parent + + if first_move_right: + self.right_depth = self._reassess_depths(node.right) + # self._rebalance(node.parent) + return + self.left_depth = self._reassess_depths(node.right) + # self._rebalance(node.parent) + return + + if node.right is None: + if is_root: + node.left.parent = None + self.root = node.left + self.root.depth = 0 + self.left_depth = self._reassess_depths(self.root.left) + return + if last_move_right: + node.parent.right = node.left + node.left.parent = node.parent + + node.parent.left = node.left + node.left.parent = node.parent + if first_move_right: + self.right_depth = self._reassess_depths(node.left) + # self._rebalance(node.parent) + return + self.left_depth = self._reassess_depths(node.left) + # self._rebalance(node.parent) + return + else: + replacement_node = self._locate_replacement_node(node) + self.delete(replacement_node.val) + + if is_root: + self.root = replacement_node + replacement_node.parent = None + replacement_node.depth = 0 + + else: + replacement_node.parent = node.parent + replacement_node.depth = node.depth + + replacement_node.left = node.left + replacement_node.right = node.right + node.left.parent = replacement_node + node.right.parent = replacement_node + if first_move_right or is_root: + self.right_depth = self._reassess_depths(self.root.right) + + else: + self.left_depth = self._reassess_depths(self.root.left) + + # if node.parent: + # self._rebalance(node.parent) + return + + def _reassess_depths(self, starting_node): + """Fix the depth of nodes below the starting_node an return the max_depth.""" + self.max_depth = 0 + + if starting_node: + self.visited = [] + queue = [starting_node] + while queue: + current = queue[0] + current.depth = current.parent.depth + 1 + if current.depth > self.max_depth: + self.max_depth = current.depth + queue = queue[1:] + + if current not in self.visited: + self.visited.append(current) + + if current.left: + if current.left not in self.visited: + queue.append(current.left) + + if current.right: + if current.right not in self.visited: + queue.append(current.right) + + return self.max_depth - starting_node.parent.depth + + def _locate_replacement_node(self, starting_node): + """Return the lowest-valued node on the right side of the sub-tree.""" + current = starting_node.right + while current.left: + current = current.left + return current + + def _rebalance(self, node): + """Rebalance a subtree, and if its root has a parent, recur on it.""" + node_balance = self.balance(node) + + if node_balance == 2: + child_balance = self.balance(node.right) + + if child_balance == 1: + self._rotate_left(node) + + if child_balance == -1: + self._rotate_right(node.right) + self._rotate_left(node) + + if node_balance == -2: + child_balance = self.balance(node.left) + + if child_balance == 1: + self._rotate_left(node.left) + self._rotate_right(node) + + if child_balance == -1: + self._rotate_right(node) + + # if node.parent: + # self._rebalance(node.parent) + + def _rotate_left(self, node): + """Rotate a node leftwards around its right child.""" + pivot_node = node.right + + if node.parent: + node.parent.left = pivot_node + + else: + self.root = pivot_node + + pivot_node.parent = node.parent + node.parent = pivot_node + + if pivot_node.left: + pivot_node.left.parent = node + + node.right = pivot_node.left + pivot_node.left = node + + def _rotate_right(self, node): + """Rotate a node rightwards around its left child.""" + pivot_node = node.left + + if node.parent: + node.parent.right = pivot_node + + else: + self.root = pivot_node + + pivot_node.parent = node.parent + node.parent = pivot_node + + if pivot_node.right: + pivot_node.right.parent = node + + node.left = pivot_node.right + pivot_node.right = node + + +if __name__ == '__main__': # pragma: no cover + import timeit as time + + l_imba = BST([6, 5, 4, 3, 2, 1]) + r_imba = BST([1, 2, 3, 4, 5, 6]) + sample_tree = BST([20, 12, 10, 1, 11, 16, 30, 42, 28, 27]) + + l_imba = time.timeit("l_imba.search(5)", setup="from __main__ import l_imba") + r_imba = time.timeit("r_imba.search(5)", setup="from __main__ import r_imba") + sample_tree = time.timeit("sample_tree.search(8)", setup="from __main__ import sample_tree") + + print('Left-Skewed Search Time: ', l_imba) + print('Right-Skewed Search Time: ', r_imba) + print('Balanced Search Time: ', sample_tree) diff --git a/src/test_bst.py b/src/test_bst.py new file mode 100644 index 0000000..eff0a0c --- /dev/null +++ b/src/test_bst.py @@ -0,0 +1,406 @@ +"""Tests for the Binary Search Tree.""" + +import pytest +from bst import BST +from bst import Node + + +@pytest.fixture +def sample_bst(): + """Make a sample_bst for testing.""" + from bst import BST + return BST() + + +def test_bst_exists(sample_bst): + """Test that the BST class makes something.""" + assert sample_bst + + +def test_bst_can_take_list_at_initialization(): + """Test that the BST can take a list.""" + from bst import BST + b = BST([1, 2, 3]) + assert b.size() == 3 + assert b.depth() == 2 + assert b.left_depth == 0 + assert b.right_depth == 2 + + +def test_bst_can_take_tuple_at_initialization(): + """Test that the BST can take a tuple.""" + from bst import BST + b = BST((1, 2, 3)) + assert b.size() == 3 + assert b.depth() == 2 + assert b.left_depth == 0 + assert b.right_depth == 2 + + +def test_bst_can_take_string_at_initialization(): + """Test that the BST can take a string.""" + from bst import BST + b = BST('abc') + assert b.size() == 3 + assert b.depth() == 2 + assert b.left_depth == 0 + assert b.right_depth == 2 + + +def test_insert_increases_depth(sample_bst): + """Test that the insert method increases the output of the depth method.""" + assert sample_bst.depth() == 0 + sample_bst.insert(1) + assert sample_bst.depth() == 0 + sample_bst.insert(2) + assert sample_bst.depth() == 1 + + +def test_insert_increases_size(sample_bst): + """Test that the insert method increases the output of the size method.""" + assert sample_bst.size() == 0 + sample_bst.insert(1) + assert sample_bst.size() == 1 + sample_bst.insert(2) + assert sample_bst.size() == 2 + + +def test_insert_increases_tree_size(sample_bst): + """Test that the insert method increases the tree_size attribute.""" + assert sample_bst.tree_size == 0 + sample_bst.insert(1) + assert sample_bst.tree_size == 1 + sample_bst.insert(2) + assert sample_bst.tree_size == 2 + + +def test_search_right(sample_bst): + """Assert that the search method returns something and that it's a Node.""" + from bst import Node + sample_bst.insert(1) + sample_bst.insert(2) + sample_bst.insert(3) + found = sample_bst.search(3) + assert found.val == 3 + assert isinstance(found, Node) + + +def test_search_left(sample_bst): + """Assert that the search method returns something and that it's a Node.""" + from bst import Node + sample_bst.insert(3) + sample_bst.insert(2) + sample_bst.insert(1) + found = sample_bst.search(3) + assert found.val == 3 + assert isinstance(found, Node) + + +def test_search_not_found_returns_none(sample_bst): + """Assert that the search method returns None when value isn't found.""" + sample_bst.insert(1) + sample_bst.insert(2) + sample_bst.insert(3) + found = sample_bst.search(4) + assert found is None + + +def test_contains_right(sample_bst): + """Assert that the contains method returns True.""" + sample_bst.insert(1) + sample_bst.insert(2) + sample_bst.insert(3) + found = sample_bst.contains(3) + assert found is True + + +def test_contains_left(sample_bst): + """Assert that the contains method returns True.""" + sample_bst.insert(3) + sample_bst.insert(2) + sample_bst.insert(1) + found = sample_bst.contains(3) + assert found is True + + +def test_contains_not_found_returns_none(sample_bst): + """Assert that the contains method returns False when value isn't found.""" + sample_bst.insert(1) + sample_bst.insert(2) + sample_bst.insert(3) + found = sample_bst.contains(4) + assert found is False + + +def test_that_bst_doesnt_work_with_non_iterable(): + """Test that BST only takes iterable inputs.""" + with pytest.raises(TypeError): + BST({0: 0, 1: 1, 2: 2}) + + +def test_that_negative_numbers_work_with_insert(sample_bst): + """Test that negative numbers are covered in insert.""" + sample_bst.insert(-500) + assert sample_bst + + +def test_node_attributes_exist(): + """Test that node attributes are in Node class.""" + n = Node(1) + assert n.val == 1 + assert n.left is None + assert n.right is None + assert n.depth == 0 + + +def test_node_attribute_depth_changes(sample_bst): + """Test that node attribute depth increases.""" + sample_bst.insert(4) + sample_bst.insert(3) + sample_bst.insert(2.5) + assert sample_bst.search(4).depth == 0 + assert sample_bst.search(3).depth == 1 + assert sample_bst.search(2.5).depth == 2 + + +def test_node_left_and_right_attributes_change(): + """Test that left and right node attributes are added with insert.""" + b = BST([5]) + b.insert(4) + b.insert(6) + assert b.root.left.val == 4 + assert b.root.right.val == 6 + + +def test_root_val_with_no_val_at_initialization(sample_bst): + """Test that root is None.""" + assert sample_bst.root is None + + +def test_in_order_indexerrors_with_empty_tree(sample_bst): + """Test that in_order raises an IndexError if the tree is empty.""" + with pytest.raises(IndexError): + sample_bst.in_order() + + +def test_pre_order_indexerrors_with_empty_tree(sample_bst): + """Test that pre_order raises an IndexError if the tree is empty.""" + with pytest.raises(IndexError): + sample_bst.pre_order() + + +def test_post_order_indexerrors_with_empty_tree(sample_bst): + """Test that post_order raises an IndexError if the tree is empty.""" + with pytest.raises(IndexError): + sample_bst.post_order() + + +def test_breadth_first_indexerrors_with_empty_tree(sample_bst): + """Test that breadth_first raises an IndexError if the tree is empty.""" + with pytest.raises(IndexError): + sample_bst.breadth_first() + + +def test_in_order_size_one(sample_bst): + """Check for the correct output of in_order on a tree of size 1.""" + sample_bst.insert(1) + gen = sample_bst.in_order() + assert next(gen) == 1 + + +def test_pre_order_size_one(sample_bst): + """Check for the correct output of pre_order on a tree of size 1.""" + sample_bst.insert(1) + gen = sample_bst.pre_order() + assert next(gen) == 1 + + +def test_post_order_size_one(sample_bst): + """Check for the correct output of post_order on a tree of size 1.""" + sample_bst.insert(1) + gen = sample_bst.post_order() + assert next(gen) == 1 + + +def test_breadth_first_size_one(sample_bst): + """Check for the correct output of breadth_first on a tree of size 1.""" + sample_bst.insert(1) + gen = sample_bst.breadth_first() + assert next(gen) == 1 + + +LEFT_IMBALANCED = [6, 5, 4, 3, 2, 1] +RIGHT_IMBALANCED = [1, 2, 3, 4, 5, 6] +SAMPLE_TREE = [20, 12, 10, 1, 11, 16, 30, 42, 28, 27] + + +def test_in_order_left_imba(): + """Check for the correct output of iot on a left-imbalanced tree.""" + tree = BST(LEFT_IMBALANCED) + gen = tree.in_order() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [1, 2, 3, 4, 5, 6] + + +def test_pre_order_left_imba(): + """Check for the correct output of preo-t on a left-imbalanced tree.""" + tree = BST(LEFT_IMBALANCED) + gen = tree.pre_order() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [6, 5, 4, 3, 2, 1] + + +def test_post_order_left_imba(): + """Check for the correct output of posto-t on a left-imbalanced tree.""" + tree = BST(LEFT_IMBALANCED) + gen = tree.post_order() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [1, 2, 3, 4, 5, 6] + + +def test_breadth_first_left_imba(): + """Check for the correct output of bft on a left-imbalanced tree.""" + tree = BST(LEFT_IMBALANCED) + gen = tree.breadth_first() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [6, 5, 4, 3, 2, 1] + + +def test_in_order_right_imba(): + """Check for the correct output of iot on a right-imbalanced tree.""" + tree = BST(RIGHT_IMBALANCED) + gen = tree.in_order() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [1, 2, 3, 4, 5, 6] + + +def test_pre_order_right_imba(): + """Check for the correct output of preo-t on a right-imbalanced tree.""" + tree = BST(RIGHT_IMBALANCED) + gen = tree.pre_order() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [1, 2, 3, 4, 5, 6] + + +def test_post_order_right_imba(): + """Check for the correct output of posto-t on a right-imbalanced tree.""" + tree = BST(RIGHT_IMBALANCED) + gen = tree.post_order() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [6, 5, 4, 3, 2, 1] + + +def test_breadth_first_right_imba(): + """Check for the correct output of bft on a right-imbalanced tree.""" + tree = BST(RIGHT_IMBALANCED) + gen = tree.breadth_first() + output = [] + for i in range(6): + output.append(next(gen)) + assert output == [1, 2, 3, 4, 5, 6] + + +def test_in_order_sample_tree(): + """Check for the correct output of iot on a sample tree.""" + tree = BST(SAMPLE_TREE) + gen = tree.in_order() + output = [] + for i in range(10): + output.append(next(gen)) + assert output == [1, 10, 11, 12, 16, 20, 27, 28, 30, 42] + + +def test_pre_order_sample_tree(): + """Check for the correct output of preo-t on a sample tree.""" + tree = BST(SAMPLE_TREE) + gen = tree.pre_order() + output = [] + for i in range(10): + output.append(next(gen)) + assert output == [20, 12, 10, 1, 11, 16, 30, 28, 27, 42] + + +def test_post_order_sample_tree(): + """Check for the correct output of posto-t on a sample tree.""" + tree = BST(SAMPLE_TREE) + gen = tree.post_order() + output = [] + for i in range(10): + output.append(next(gen)) + assert output == [1, 11, 10, 16, 12, 27, 28, 42, 30, 20] + + +def test_breadth_first_sample_tree(): + """Check for the correct output of bft on a right-imbalanced tree.""" + tree = BST(SAMPLE_TREE) + gen = tree.breadth_first() + output = [] + for i in range(10): + output.append(next(gen)) + assert output == [20, 12, 30, 10, 16, 28, 42, 1, 11, 27] + + +def test_delete_node_that_doesnt_exist(): + """Test deleting a nonexistant node from BST.""" + tree = BST([5, 3, 2, 15, 44, 100]) + assert tree.delete(20) is None + + +def test_delete_node_that_does_exist(sample_bst): + """Test deleting node, basic functionality.""" + sample_bst.insert(20) + sample_bst.insert(300000) + sample_bst.insert(300) + assert sample_bst.contains(300) + sample_bst.delete(300) + assert sample_bst.contains(300) is False + + +def test_deleting_node_reduces_tree_size(sample_bst): + """Test size and delete together.""" + sample_bst.insert(2) + sample_bst.insert(3) + assert sample_bst.size() == 2 + sample_bst.delete(3) + assert sample_bst.size() == 1 + + +def test_delete_left_imba(): + """Test that delete works on a left-imbalanced tree.""" + tree = BST(LEFT_IMBALANCED) + tree.delete(6) + assert tree.size() == 5 and tree.root.val == 5 + tree.delete(1) + assert tree.size() == 4 and tree.root.val == 5 + + +def test_delete_right_imba(): + """Test that delete works on a right-imbalanced tree.""" + tree = BST(RIGHT_IMBALANCED) + tree.delete(6) + assert tree.size() == 5 and tree.root.val == 1 + tree.delete(1) + assert tree.size() == 4 and tree.root.val == 2 + + +def test_delete_on_sample_tree(): + """Test that delete works on a right-imbalanced tree.""" + tree = BST(SAMPLE_TREE) + tree.delete(16) + assert tree.size() == 9 and tree.root.val == 20 + tree.delete(28) + assert tree.size() == 8 and tree.root.val == 20 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..082b2bc --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py27, py36 + +[testenv] +commands = py.test --cov --cov-report term-missing +deps = + pytest + pytest-cov \ No newline at end of file