From 82f3a67ac3de7593a7ff5f2139b70dad772413e1 Mon Sep 17 00:00:00 2001 From: EH225 Date: Tue, 25 Mar 2025 12:14:53 -0400 Subject: [PATCH 1/7] major update to documentation and methods --- .../binary_index_tree.cpython-311.pyc | Bin 6770 -> 6968 bytes .../binary_search_tree.cpython-311.pyc | Bin 29310 -> 30540 bytes ds/__pycache__/disjoint_sets.cpython-311.pyc | Bin 4281 -> 4406 bytes ds/binary_index_tree.py | 32 ++-- ds/binary_search_tree.py | 78 ++++---- ds/cache.py | 169 ++++++++++++------ ds/deque.py | 79 ++++---- ds/disjoint_sets.py | 5 +- ds/heaps.py | 22 ++- ds/linked_list.py | 129 ++++++------- tests/README.txt | 2 +- tests/test_data_structures.py | 145 ++++++++------- 12 files changed, 398 insertions(+), 263 deletions(-) diff --git a/ds/__pycache__/binary_index_tree.cpython-311.pyc b/ds/__pycache__/binary_index_tree.cpython-311.pyc index fd5a95b8b9729031bb511b05ec8d46f6de1ec2d3..bb8dcd06ec3928cfec9a163439ec56ab27040e5c 100644 GIT binary patch delta 685 zcmexlvcrsTIWI340}xp4eU!dld?H_PKp>Y>W?o`Zr9x(2N@|5dNl|L5LP}yuqC#;= zQE75XX;G>|ZhlH>PO6SVF;FTaHK#xWqQeuUAq1#iQz0$CNFgP)Br!9mcw@5$BV+aC zHH=!DZ!xMdN|qJ?O-n5X8n2M3P*9YbmRX@&T$-!LHCc%H#O8a<+Zh>OPM*g4U~?VY zaVEwOlifJ)>gaJPKtVui5zy@1Vg)2){55j&(~UIpG&L1UGIJ+iXEoX!%muVmW^)a< z79(TyF zVkKVd7YR0P))8`K4xbFkmM?g2lh!kS!7h`Ue1?SlFuo delta 504 zcmdmC_Q`~AIWI340}z~WzLEY+Y$9K7v# z85uuLKFs=n10(?gn-{YkXJR}wIh*tDWJf-O&C9reW=d^7&#lGC=rs8kPZOi(4hJ7H@tms>;Y%Ihj*@31j`_b>jCKTPOEPJTektRh^;yfdNP@a9yFi zqI8AU6%|u3e}@W~LML=4FOt+Yl42EJApC&=NUbmiQyWZoh#p|xA$7&r1_a~KLF4}l&k;% diff --git a/ds/__pycache__/binary_search_tree.cpython-311.pyc b/ds/__pycache__/binary_search_tree.cpython-311.pyc index 2a81525faa413f35b6622a8c66ce7313c3c35261..8fc9cfb3d95b6a6d96383cc0d99215f229c9c89f 100644 GIT binary patch delta 3790 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 delta 2930 zcmZ`*drX_x75DWUf_VjljqS*WU}Fk4<`GC>kQPJ;Ddu5Zzy!?0#)dKs*A6A5X;4D7 zEZv%x{n5f?nodA7sx+J7A#AhL=NlHgYbK)IlGj(F7N&& zrBEQN-nS1UvG4N_WZ;#(MwCz>Z)O;hH!u|Bqu^A8nB-IztMXUmzJh=ZuBj|AqA-I} zspBK1a7S6Fo>T^mD+-_CLC3^8M(luARR%*UdyY~s#$p&K395rixT$*4wp5;OX5SN2 zvt#tMIT{?J6Jc|VEyNrci;Y!8WAxNS>=cE+rd=d@;avJHLJV*?qmh_kA)}PZb2)n0 z$hbk?fUJHid_&Vr%)+~xapE$wKr|B}i@|jn%|_XPpkWY|F<_Bt4rJ%vcQMhO5zi;@^2`J6>b|2=a!>~I%VPxP#gQ?sOI#ddr&xIY$l`{ zerEa|`31b!WrWDMLJ$rVNg=yd4u3GW)SIB+ zUQ0d^zG?p@ku6Q9L;tu5qK~y|krzKIWkK8&GepbaYPq(i3B@l>?Qzud4Z(POrq%>q zgZYpw*JwV&!PgipOTJJG=@lwMo)dB%nFQi>PH;PW#A#U|Qfq`aokh~WON39IC4{u` zB67KA;c}fG{^qdnIJCl*sx&tGovK1sayP3iOdd@yfV)*Hjg+upr+KhsHf+?pgzD-; z(uWb6(u~Z4wlfQu%`w1xwV#nr9_Qmjl*M_I^?K@&cc$LVWX78}IW-&bhk{n5UfiJ; zHtKCMX7y1+tw`PrhDN>8%Z8VV!1DBd=xof=97UIv2eY|z+#swp_Q=Fa?qh1&3?DTc z@}6Sip7b@dZPS76Xr{)49IvKw3?Ds#p|7`?N;wIhC_0FMM~OayZ3ynRlxn7t>7`Vr zrq;N;gBOdMskaq$Y&I06tthMJB6Bmr$yf^UZUImXYfn!p&|apXCt=`tk@5(N{jhwz zmn;jvJ`T7En0ZJ^GUV@;@;1$4z0V6T!&8gFgYUp(9Jep$BxbS zckP8|y3doINesDjCU}T2#+hQUCv#}Hq!q6)55K|iCU*E++dMx>NkE>^(c43m&B$Va zYaRnR0=GT2Bmtj$&MSFUo8U~Ji+lh#`exWH3%xFK2cGnP%OLuoqY1+^QpwJFoM-i- zQ_?(yb$6DdX)-?Fhz=(!lLkjWE=>YxU!CZcp@B}_M z+9DujRE|C|YYy4KmgM?xW>WM#-MYuQ7FZ~yRjzFKX1t(|~UJa-T zDS#WnQHGh5R=ucagLXnku#wU8XgUF%lSZ-vS3@<-@_Uo-*b7jNuw$|RNHU_{u-=krj2JCEx z6JP0q+w(OhV^Wr{Xnx;fU0qyPCv1l|>NndH4V`gYXTstJ9UZo56uPy(q=Yr=;f=+_ zp>{6sg#%C#Jsj34oG%Zoh7%@dQo`i*3EpgkH&1PO;=TP_zQnPCMAIPG?u8n)qGDCI zRx*~$5*hr#N=S~yYStK*%mgS?6RMtHjy9C_%( zU?;y^-fHDd{NyNa_d+>biB-aHVs*!iiZSxCZnZR_e=I4%&U)!a_3IU)yt&A-^43|g z6CWHFg-2i`jGlTs4U5BV-$!2o=Rza@c}NRjejyf;k@BSE%Ww~M_a6>qw4{-;q~y_f NkL&OMpHYf+`d{c+;7R}h diff --git a/ds/__pycache__/disjoint_sets.cpython-311.pyc b/ds/__pycache__/disjoint_sets.cpython-311.pyc index e6fb10b33b2cd7f5896d5f5ff3f31b7eaf32f794..1dda8e6fdb6b2909fdbe1bb980d1eb3f8a3ebf50 100644 GIT binary patch delta 301 zcmdm~xJ`+7IWI340}$k#e3ZUvB5w?1+Qcje6&;1*)KrCx)SLnhm(1d<{LH+P;M9_0 zO@*}lB88OHlEloM;)$ycGoGHD$5<_flJjEoVJ{W-EW@8U>cWK`QM&Gna&(RQ;W u4=)p=^JI0t-(1YB93L1!1lMLAem_QgC!n$q4A{w!VDT?FWQzoV;R681?_DAQ delta 179 zcmdm{v{R9HIWI340}y<2xsh%%kvE3XX=0Yc#B+xk3nx!vEZiK+)WygsH2DSdJVxQk z?JUiVe4DvhUokR@Z~n%1jgc{O@>z~7MupAhoC%DKs+(tU{bgjd**uMhmx None: """ Updates the value of an element in the original array located at a particular index (idx) and also the - binary index tree accordingly. An update can also be accomplished with: binary_idx_tree[idx] = val + binary index tree accordingly. An update can also be accomplished with: binary_idx_tree[idx] = val. + Performs updates in O(log2(n)) time. Parameters ---------- @@ -63,22 +64,22 @@ def update(self, idx: int, val: Union[int, float]) -> None: def _range_query(self, end: int) -> Union[float, int]: """ Internal helper function for performing range queries. Computes the sum of all elements up through - index end. Is used by range_query to compute the sum of elements start:end by taking the sum through - end and subtracting off the sum through (start-1). - + index end. This method is used by range_query to compute the sum of elements start:end by taking the + sum through index=end and subtracting off the sum through index=(start-1). Runs in O(log2(n)) time. + Parameters ---------- end : int The ending index of the range query. - + Returns ------- Union[float, int] The evaluation of the range sum query from the first element through the end index element. - + """ end += 1 # Convert to 1-indexing - sum_total = 0 # Aggregate the sum total across all entries from the start up through idnex end + sum_total = 0 # Aggregate the sum total across all entries from the start, up through index end while end > 0: sum_total += self.binary_idx_tree[end - 1] end -= (end & -end) # Flip the last set bit @@ -87,7 +88,8 @@ def _range_query(self, end: int) -> Union[float, int]: def range_query(self, start: int, end: int) -> Union[float, int]: """ Performs a sum range query using the binary index tree and returns the aggregate answer. Computes the - sum [start, end] of the original array. + sum of the array elements falling within the inclusive index interval [start, end] of the original + array. Runs in O(log2(n)) time. Parameters ---------- @@ -95,7 +97,7 @@ def range_query(self, start: int, end: int) -> Union[float, int]: The starting index of the range query. end : int The ending index of the range query. - + Returns ------- Union[float, int] diff --git a/ds/binary_search_tree.py b/ds/binary_search_tree.py index b4f3e2b..6b66413 100644 --- a/ds/binary_search_tree.py +++ b/ds/binary_search_tree.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Binary search tree. +Binary search tree data structure module, see help(BinarySearchTree) for details. """ from typing import Union, Optional, List, Tuple, Iterable @@ -12,6 +12,10 @@ ########################## class TreeNode: + """ + Binary tree node object. + """ + def __init__(self, val, left=None, right=None): self.val = val self.left, self.right = left, right @@ -19,7 +23,12 @@ def __init__(self, val, left=None, right=None): class BinarySearchTree: """ - Binary search tree data-structure. + Binary search tree (BST) data-structure. + + Binary search trees are good for quickly locating elements in a collection in O(log2(n)) time and being + able to also add and remove elements from the collection in O(log2(n)) time as well. They are also able + to quickly locate the min and max of a collection in O(log2(n)) time and also find the first element + greather than or less than a given value in O(log2(n)) time. """ def __init__(self): @@ -38,7 +47,7 @@ def search(self, val: Union[int, float]) -> Optional[TreeNode]: def _search(self, root: Optional[TreeNode], val: Union[int, float]) -> Optional[TreeNode]: """ - Recursive helper function for locating a node with the value of val in the BST. Returns a + Recursive helper function for locating a node with the value of val in the BST. Returns a pointer to the node with this value if it exists, otherwise None is returned. :param root: The root node of a BST through which to search for the node containing val. @@ -69,7 +78,7 @@ def insert(self, val: Union[float, int]) -> None: self.root = self._insert(self.root, val) self.n += 1 - def _insert(self, root: Optional[TreeNode], val: int) -> TreeNode: + def _insert(self, root: Optional[TreeNode], val: Union[int, float]) -> TreeNode: """ Recursive helper function to insert a new value into the BST. Returns a TreeNode object i.e. the root of the new BST after insertion. @@ -98,7 +107,7 @@ def delete(self, val: Union[float, int]) -> None: self.n -= 1 self.root = self._delete(self.root, val) - def _delete(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]: + def _delete(self, root: Optional[TreeNode], key: Union[int, float]) -> Optional[TreeNode]: """ Recursive helper function for deleting a node from the BST. @@ -160,7 +169,7 @@ def _delete(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]: return root def _treeSearch(self, root: Optional[TreeNode], prior_node: Optional[TreeNode], - key: int) -> Tuple[Optional[TreeNode], Optional[TreeNode]]: + key: Union[int, float]) -> Tuple[Optional[TreeNode], Optional[TreeNode]]: """ Helper function that returns a pointer to the node that has a value equal to key and a pointer to the that node's parent. Returns None for either or both if they do not @@ -170,8 +179,8 @@ def _treeSearch(self, root: Optional[TreeNode], prior_node: Optional[TreeNode], :param root: The root node of an existing sub-tree or None. :param prior_node: The node prior to root, i.e. the parent of root or None if it has no parent. :param key: The key to locate in the BST. - :returns: Returns the node and its prior node as a tuple (in that order) if they exist or None - values instead within the tuple. + :returns: Returns pointers to a node and its prior node as a tuple (in that order) if they exist or + None values instead within the tuple. """ if root is None or root.val == key: # Base-case, when we either reach the node we # are looking for or a None ending indicating that it does not exist @@ -185,7 +194,7 @@ def _treeSearch(self, root: Optional[TreeNode], prior_node: Optional[TreeNode], def inorderSuccessor(self, root: TreeNode, p: TreeNode) -> Optional[TreeNode]: """ - To return the in-order successor of p, we will need to handle things in a few + To return the in-order successor of some node p, we will need to handle things in a few different cases: 1). If node p has a right child, then we want to get the left-most child of that right successor. A right child means that there is a value larger @@ -230,9 +239,12 @@ def rebalance(self) -> None: """ self.root = self._balanced_BST(self.inOrderTraversal()) - def _balanced_BST(self, inOrderNodeList: list) -> Optional[TreeNode]: + def _balanced_BST(self, inOrderNodeList: List[Union[int, float]]) -> Optional[TreeNode]: """ Helper function that returns a height-balanced BST built off an in-order node traversal. + + :param inOrderNodeList: A list of node values from an in-order traversal of the tree. + :returns: The root of a newly balanced BST using the same nodes that were provided as inputs. """ if len(inOrderNodeList) == 0: # Recursion base-case return None @@ -250,16 +262,17 @@ def find_first_le(self, val: Union[int, float]) -> Optional[Union[int, float]]: Finds the first value in the BST that is less than or equal to a given input value. Returns None if there is no value that is less than or equal to the value provided. - :param val: The value that is a ceiling for the largest element to returns from the tree. + :param val: The value that is a ceiling for the largest element to return from the tree. :returns: Returns the largest value in the BST that is less than or equal to val. """ return self._find_le(self.root, val) - def _find_le(self, root: Optional[TreeNode], val: int) -> Optional[Union[int, float]]: + def _find_le(self, root: Optional[TreeNode], val: Union[int, float]) -> Optional[Union[int, float]]: """ Recursive helper function for finding the largest value in the BST that is less than or equal to the input val. + :param root: The root node of an existing sub-tree. :param val: The value that is a ceiling for the largest element to returns from the tree. :returns: Returns the largest value in the BST that is less than or equal to val. """ @@ -281,7 +294,7 @@ def _find_le(self, root: Optional[TreeNode], val: int) -> Optional[Union[int, fl # the lower bound i.e. the largest value in the tree <= val return max(root.val, right_le) - def find_first_ge(self, val: Optional[Union[int, float]]) -> Optional[Union[int, float]]: + def find_first_ge(self, val: Union[int, float]) -> Optional[Union[int, float]]: """ Finds the first value in the BST that is greater than or equal to a given input value. Returns None if there is no value that is greater than or equal to the value provided. @@ -291,11 +304,12 @@ def find_first_ge(self, val: Optional[Union[int, float]]) -> Optional[Union[int, """ return self._find_ge(self.root, val) - def _find_ge(self, root: Optional[TreeNode], val: int) -> Optional[Union[int, float]]: + def _find_ge(self, root: Optional[TreeNode], val: Union[int, float]) -> Optional[Union[int, float]]: """ Recursive helper function for finding the smallest value in the BST that is greater than or equal to the input val. + :param root: The root node of an existing sub-tree. :param val: The value that is a floor for the smallest element to returns from the tree. :returns: Returns the smallest value in the BST that is greater than or equal to val. """ @@ -317,7 +331,8 @@ def _find_ge(self, root: Optional[TreeNode], val: int) -> Optional[Union[int, fl # the upper bound i.e. the smallest value in the tree >= val return min(root.val, left_ge) - def preOrderTraversal(self, root: Optional[TreeNode] = 0, return_vals: bool = True) -> list: + def preOrderTraversal(self, root: Optional[TreeNode] = 0, + return_vals: bool = True) -> List[Union[int, float]]: """ Returns the pre-order traversal of the BST nodes: [root, left, right] @@ -336,7 +351,8 @@ def preOrderTraversal(self, root: Optional[TreeNode] = 0, return_vals: bool = Tr nodes.extend(self.preOrderTraversal(root.right, return_vals)) return nodes - def inOrderTraversal(self, root: Optional[TreeNode] = 0, return_vals: bool = True) -> list: + def inOrderTraversal(self, root: Optional[TreeNode] = 0, + return_vals: bool = True) -> List[Union[int, float]]: """ Returns the in-order traversal of the BST nodes: [left, root, right] @@ -359,7 +375,8 @@ def inOrderTraversal(self, root: Optional[TreeNode] = 0, return_vals: bool = Tru nodes.extend(self.inOrderTraversal(root.right, return_vals)) return nodes - def postOrderTraversal(self, root: Optional[TreeNode] = 0, return_vals: bool = True) -> list: + def postOrderTraversal(self, root: Optional[TreeNode] = 0, + return_vals: bool = True) -> List[Union[int, float]]: """ Returns the post-order traversal of the BST nodes: [left, right, root] @@ -382,7 +399,7 @@ def postOrderTraversal(self, root: Optional[TreeNode] = 0, return_vals: bool = T return nodes def levelOrderTraversal(self, root: Optional[TreeNode] = 0, return_vals: bool = True, - return_levels: bool = False) -> list: + return_levels: bool = False) -> List[Union[int, float]]: """ Returns the level-order traversal of the BST nodes as a list or list of lists. @@ -429,17 +446,18 @@ def isValidBST(self) -> bool: Operates by recursively calling a DFS helper function on the left and right child nodes where root.val is provided to the left-subtree as the max and root.val is provided to the right-subtree as the min value. - + :returns: A bool indicating if the BST rooted at root is a valid BST. """ return self._DFS(self.root.left, None, self.root.val) and self._DFS(self.root.right, self.root.val, None) - def _DFS(self, node: Optional[TreeNode], low: Optional[int], high: Optional[int]) -> bool: + def _DFS(self, node: Optional[TreeNode], low: Optional[Union[int, float]], + high: Optional[Union[int, float]]) -> bool: """ Helper function for isValidBST that checks if the tree with root as the root node is a valid BST based on the input low and high values provided. - + :param node: The root of this subtree to be evaluated if it is a valid BST. :param low: The min value we should find in this subtree i.e. all values must be >= low. :param high: The max value we should find in this subtree i.e. all values must be <= high. @@ -487,7 +505,7 @@ def get_max_depth(self, root: Optional[TreeNode] = 0) -> int: Recursive helper function that returns the max depth of the tree rooted at root. Runs on the self.root internal BST if root is left as the default value of 0. A tree with only a root is defined to have a depth of 1. A tree with no nodes is defined to have a depth of 0. - + :param root: The root node of a BST. If set to 0, this method operates on the entire BST. :returns: An integer value denoting the max depth of the tree along all of its branches. """ @@ -510,7 +528,7 @@ def contains_digit(input_list: list) -> bool: """ Helper function that checks if any entry in input_list is a numerical value. Returns True if any element of input_list is either an int or float type value. - + :param input_list: An input list of elements, could be of mixed type. :returns: A bool indicating if there is at least 1 element that is an int or float date type. """ @@ -526,15 +544,15 @@ def contains_digit(input_list: list) -> bool: print(f"Tree depth is too large ({max_depth} > 10) to print, try running .rebalance() first to " "reduce the depth.") return None - # Create a list of lists where each inner list is a row in the output print string. We will be - # including additional characters to draw the branches between values so we'll need more than just - # width number of place holders. Each column of vals is separated from others internally by a column - # to hold the branches so we need max_depth cols for the vals + (max_depth - 1) cols for the branching + # Create a list of lists where each inner list is a row in the output print string. We will be + # including additional characters to draw the branches between values so we'll need more than just + # width number of place holders. Each column of vals is separated from others internally by a column + # to hold the branches so we need max_depth cols for the vals + (max_depth - 1) cols for the branching # between. Similarly, for the rows, between 2 child nodes, we will want to have at least 1 space so # we'll need width*2 - 1 places print_str = [[" " for j in range(max_depth * 2 - 1)] for i in range(width)] node_stack = [(self.root, (width // 2, 0), width)] # (node, (x, y), width), root begins at mid-x, y=0 - # The width of the tree where this node is at the center is important for computing the dist of the + # The width of the tree where this node is at the center is important for computing the dist of the # child nodes up and down from the current one. It is 1 + width // 2 = row diff to child nodes # Traverse the tree using DFS and fill in values and branch characters as we go while node_stack: # Iterate until we've visited all nodes @@ -553,8 +571,8 @@ def contains_digit(input_list: list) -> bool: # No branch characters added if this node has no children offset = w // 2 // 2 + 1 # Find the row offset size from this current node's x to the child nodes - # we take the width of the tree that the current node sits in the middle of, split it in half to - # get the width of each left and right size, then take half of that to the midpoint within each + # we take the width of the tree that the current node sits in the middle of, split it in half to + # get the width of each left and right size, then take half of that to the midpoint within each # half and add 1 since we need to move from the mid row of this width to one of the halfs and that # takes 1 step diff --git a/ds/cache.py b/ds/cache.py index d4b63c2..55c0f33 100644 --- a/ds/cache.py +++ b/ds/cache.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- """ -LRU cache and LFU cache data structures. +LRU cache and LFU cache data structures module, see help(LRUCache) and help(LFUCache) for details. """ +from typing import Optional from collections import defaultdict, OrderedDict @@ -11,93 +12,122 @@ ################# class ListNode: - def __init__(self, val, prev=None, next=None): + def __init__(self, val, prev_=None, next_=None): self.val = val - self.next = next - self.prev = prev + self.next_ = next_ + self.prev_ = prev_ class LRUCache: """ - Least recently used cache (LFU) data structure. Caches elements and - drops the one that was least recently used when out of space and adding - a new element. - + Least recently used cache (LFU) data structure. Caches elements and drops the one that was least recently + used when at capacity and adding a new element. This data structure helps cache values that may be useful + to have in memory later to prevent duplicative computations, while also limiting how much total memory + is used to retain prior results. Operates much like a dictionary but with a limited number of keys + retained. + Great explination: https://www.romaglushko.com/blog/design-lru-cache/ """ def __init__(self, capacity: int): self.capacity = capacity # How many keys in total may be stored - self.dict = {} # A hashmap to quickly find data associated with - # each key - # Also store the head and tail of a double linked list that will - # be able to track our usage of different keys. When a key is used - # or added, it will be added to the tail and removed from the other + self.dict = {} # A hashmap to quickly find data associated with each key + # Also store the head and tail of a double linked list that will be able to track our usage of + # different keys. When a key is used or added, it will be added to the tail and removed from the other # part of the list if it exists already self.head, self.tail = None, None - # When we want to add a new element and there are too many to add, we - # will drop the most element most distantly used, i.e. the head element - # and we will know what that is by keeping a pointer to the head of - # this linked list in memory + # When we want to add a new element and there are too many to add, we will drop the most element most + # distantly used, i.e. the head element and we will know what that is by keeping a pointer to the head + # of this linked list in memory - def get(self, key: int) -> int: + def get(self, key: int) -> Optional[int]: """ - Returns the value associated with a key if it exists, and -1 otherwise. + Returns the value associated with a key if it exists, and None otherwise. + + :param key: The lookup key of an element in the LRU cache. + :returns: The value associated with the key if found, otherwise returns None. """ - val, node = self.dict.get(key, (-1, None)) # Attempt to retrieve val + val, node = self.dict.get(key, (None, None)) # Attempt to retrieve val if node is not None: # Then this key does exist already, update the # internal linked list to reflect that it has been recently used self._update_usage(key) return val def _update_usage(self, key: int) -> None: + """ + Internal helper method for updating the frequency of usage of a given key in the cache, incriments + up its usage by 1 so that the overall usage can be tracked for when it times comes to drop a least + recently used key. + + :param key: The lookup key of an element in the LRU cache. + :returns: None, updates the internal usage recency usage associated with this key. + """ node_pointer = self.dict[key][1] # Get the node pointer of this key if node_pointer == self.tail: # If this node is already the tail node return None # then return None, nothing to update else: # Move this existing node from where it currently is to the tail if node_pointer == self.head: # If the head node, handle special case # self.head != self.tail since the above condition was not met - self.head = self.head.next # Move to the next node - self.head.prev = None # Disconnect to make this the new head + self.head = self.head.next_ # Move to the next node + self.head.prev_ = None # Disconnect to make this the new head else: # If not the first node, nor the last, update pointers # Link the prior node to the next node to skip over this node - node_pointer.prev.next = node_pointer.next - node_pointer.next.prev = node_pointer.prev + node_pointer.prev_.next_ = node_pointer.next_ + node_pointer.next_.prev_ = node_pointer.prev_ # Now add this removed node to the end as a new node - self.tail.next = node_pointer # Move to the tail of the LL - node_pointer.prev = self.tail # Link backwards - self.tail = self.tail.next # Update the tail reference - self.tail.next = None # Break the old next connection, the tail - # node should always have a .next pointing to None - - def _add_new_key(self, key: int) -> None: - if self.tail is None: # If no linked list currently - self.head = ListNode(val=key) - self.tail = self.head - else: # If there is already a tail node, add the new node at the end - self.tail.next = ListNode(val=key, prev=self.tail) # Add node - self.tail = self.tail.next # Update tail pointer + self.tail.next_ = node_pointer # Move to the tail of the LL + node_pointer.prev_ = self.tail # Link backwards + self.tail = self.tail.next_ # Update the tail reference + self.tail.next_ = None # Break the old next connection, the tail + # node should always have a .next_ pointing to None def put(self, key: int, value: int) -> None: + """ + Adds a new key:value pair to the LRU cache or updates the value associated with key if key is + already an existing key:value pair in the cache, + + :param key: aasda + :param value: adasda + :returns: None, adds the input data to the internal data structures. + """ # If the key already exists, update the value already there if key in self.dict: self.dict[key][0] = value # Edit the value of the key self._update_usage(key) # Reflect that this key was recently used else: # Otherwise if not already in the dict, add it - self._add_new_key(key) # Add the new key to the recency linked list + # Add the new key to the recency linked list + if self.tail is None: # If no linked list currently + self.head = ListNode(val=key) + self.tail = self.head + else: # If there is already a tail node, add the new node at the end + self.tail.next_ = ListNode(val=key, prev_=self.tail) # Add node + self.tail = self.tail.next_ # Update tail pointer + self.dict[key] = [value, self.tail] # Store both the value and node pointer # keys added are always added at the end and become the new tail node if len(self.dict) > self.capacity: # Check if adding this new element # puts us over the limit, ifso, drop the most distantly used element drop_key = self.head.val # The element used least recently - self.head = self.head.next # Move the head to the next element, which + self.head = self.head.next_ # Move the head to the next element, which # is guarenteed to exist since we just added a new node which put us # over the limit which is >= 1 - self.head.prev = None # Drop the prior ref link + self.head.prev_ = None # Drop the prior ref link del self.dict[drop_key] # Delete from the dictionary as well + def __setitem__(self, key: int, val: int) -> None: + """ + Support for obj[idx] = val changes to the key:value pair. + """ + self.put(key, val) + + def __len__(self): + """ + Returns the size of the cache i.e. how many element are currently stored. + """ + return len(self.dict) + ################# ### LFU Cache ### @@ -107,20 +137,40 @@ def put(self, key: int, value: int) -> None: class LFUCache: def __init__(self, capacity: int): """ - Least frequently used cache (LFU) data structure. Caches elements and - drops the one that was least frequently used when out of space and adding - a new element. + Least frequently used cache (LFU) data structure. Caches elements and drops the one that was least + frequently used when out of space and adding a new element.This data structure helps cache values that + may be useful to have in memory later to prevent duplicative computations, while also limiting how + much total memory is used to retain prior results. Operates much like a dictionary but with a limited + number of keys retained. """ self.capacity = capacity # The max number of elements allowed self.dict = {} # Create a dict to hold the (key:(val, usage_count)) pairs self.freq_dict = defaultdict(OrderedDict) # Create another dict to hold - # (usage_count:OrderedDict(key:None)) to organize the frequency of uasge + # (usage_count:OrderedDict(key:None)) to organize the frequency of uasge # within each usage count bucket self.min_freq = None # Keep track of the min usage frequency + def get(self, key: int) -> Optional[int]: + """ + Returns the value associated with a key if it exists, and None otherwise. + + :param key: The lookup key of an element in the LFU cache. + :returns: The value associated with the key if found, otherwise returns None. + """ + val, usage_count = self.dict.get(key, (None, None)) + if usage_count is not None: + # If the key does exist, update the usage_count + self._incriment_usage_count(key) + return val + def _incriment_usage_count(self, key: int) -> None: """ - Increments the usage_count of a particular key by 1. + Internal helper method that increments the usage_count of a particular lookup key by 1. Helpful for + tracking which key:value pairs are most often used so that when adding a new key:value pair when + already at capacity, it is known which one has been used least frequently and should be dropped. + + :param key: The lookup key of an element in the LFU cache. + :returns: None, updates the internal usage frequency usage associated with this key. """ val, usage_count = self.dict[key] del self.freq_dict[usage_count][key] # Remove from the old dict @@ -138,14 +188,15 @@ def _incriment_usage_count(self, key: int) -> None: # usage count dict to reflect the get action self.dict[key][1] = new_usage_count # Update the usage count - def get(self, key: int) -> int: - val, usage_count = self.dict.get(key, (-1, None)) - if usage_count is not None: - # If the key does exist, update the usage_count - self._incriment_usage_count(key) - return val - def put(self, key: int, value: int) -> None: + """ + Adds a new key:value pair to the LFU cache or updates the value associated with key if key is + already an existing key:value pair in the cache, + + :param key: A new lookup key value. + :param value: A value associated with this lookup key to store. + :returns: None, adds the input data to the internal data structures. + """ if key in self.dict: # If this key already exists self.dict[key][0] = value # Update the value associated with the key self._incriment_usage_count(key) # Record a new usage instance for this key @@ -162,7 +213,7 @@ def put(self, key: int, value: int) -> None: break # is also the least frequently used key in this bin del self.freq_dict[self.min_freq][key] # Remove key from bin # Drop the least frequently used key among all keys that have been - # used min_freq number of times + # used min_freq number of times if len(self.freq_dict[self.min_freq]) == 0: # If this freq dict is now empty del self.freq_dict[self.min_freq] # then drop it from the data structure @@ -170,3 +221,15 @@ def put(self, key: int, value: int) -> None: self.min_freq = 1 # The smallest min_freq can ever be is 1 which is now the # new min since we've added a new element with a usage count of 1 + + def __setitem__(self, key: int, val: int) -> None: + """ + Support for obj[idx] = val changes to the key:value pair. + """ + self.put(key, val) + + def __len__(self): + """ + Returns the size of the cache i.e. how many element are currently stored. + """ + return len(self.dict) diff --git a/ds/deque.py b/ds/deque.py index fac737c..c283859 100644 --- a/ds/deque.py +++ b/ds/deque.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Deque data structure. +Deque data structure module, see help(Deque) for details. """ from typing import Optional @@ -11,53 +11,62 @@ ############# class ListNode: - def __init__(self, val, prev=None, next=None): - # Use a doubly linked list + def __init__(self, val, prev_=None, next_=None): + """ + Doubly linked list node data structure. + """ self.val = val - self.prev = prev - self.next = next + self.prev_ = prev_ + self.next_ = next_ class Deque: """ - A data structure that supports insertion and deletion at the front and back in O(n) time. + A data structure that supports insertion and deletion at the front and back in O(n) time. Uses a doubly + linked list internal data structure. """ def __init__(self, k: int = 100): - self.k = k # The max capacity of the queue - self.n = 0 # The number of elements in the queue + self.k = k # The max capacity of the deque + self.n = 0 # The number of elements in the deque self.head = None # The root of the linked list self.tail = None # The end of the linked list - def appendLeft(self, value: int) -> None: + def append_left(self, value: int) -> None: """ - Append a new element to the front of the dequeue if possible. + Appends a new element to the front of the deque if possible. + + :param value: The value to be added to the front of the deque. + :returns: None, adds this new value to the data structure. """ if self.n < self.k: # Can only add if space available if self.n == 0: # No nodes exist yet self.head = ListNode(val=value) self.tail = self.head # Only 1 node, both are the same else: # Otherwise add to the existing linked list - self.head.prev = ListNode(val=value, next=self.head) - self.head = self.head.prev + self.head.prev_ = ListNode(val=value, next_=self.head) + self.head = self.head.prev_ self.n += 1 def append(self, value: int) -> None: """ - Append a new element to the end of the dequeue if possible. + Appends a new element to the end of the deque if possible. + + :param value: The value to be added to the end of the deque. + :returns: None, adds this new value to the data structure. """ if self.n < self.k: # Can only add if space available if self.n == 0: # No nodes exist yet self.head = ListNode(val=value) self.tail = self.head # Only 1 node, both are the same else: # Otherwise add to the existing linked list - self.tail.next = ListNode(val=value, prev=self.tail) - self.tail = self.tail.next + self.tail.next_ = ListNode(val=value, prev_=self.tail) + self.tail = self.tail.next_ self.n += 1 - def popLeft(self) -> Optional[int]: + def pop_left(self) -> Optional[int]: """ - Remove the first element in the deque if possible and returns its value. + Removes the first element in the deque if possible and returns its value. """ if self.n == 0: # Nothing to delete return None @@ -66,14 +75,14 @@ def popLeft(self) -> Optional[int]: if self.n == 1: # Only 1 node self.head, self.tail = None, None else: # At least 2 nodes - self.head = self.head.next - self.head.prev = None + self.head = self.head.next_ + self.head.prev_ = None self.n -= 1 return return_val def pop(self) -> Optional[int]: """ - Remove the last element in the deque if possible and returns its value. + Removes the last element in the deque if possible and returns its value. """ if self.n == 0: # Nothing to delete return None @@ -82,50 +91,54 @@ def pop(self) -> Optional[int]: if self.n == 1: # Only 1 node self.head, self.tail = None, None else: # At least 2 nodes - self.tail = self.tail.prev - self.tail.next = None + self.tail = self.tail.prev_ + self.tail.next_ = None self.n -= 1 return return_val - def getFront(self) -> int: + def get_front(self) -> int: """ Return the element from the front of the deque or -1 if empty. """ return self.head.val if self.n > 0 else -1 - def getRear(self) -> int: + def get_rear(self) -> int: """ Return the element from the rear of the deque or -1 if empty. """ return self.tail.val if self.n > 0 else -1 - def isEmpty(self) -> bool: + def is_empty(self) -> bool: """ Return True if the deque is empty and False otherwise """ return self.n == 0 - def isFull(self) -> bool: + def is_full(self) -> bool: """ Return True if the deque is full and False otherwise. """ return self.n == self.k def __str__(self) -> str: + """ + Returns a string representation of the deque. + """ return self.__repr__() def __len__(self) -> int: - n = 0 - node = self.head - while node is not None: - n += 1 - node = node.next - return n + """ + Returns the number of element currently in the deque. + """ + return self.n def __repr__(self) -> str: + """ + Returns a string representation of the deque. + """ output = [] node = self.head while node is not None: output.append(str(node.val)) - node = node.next + node = node.next_ return "[" + ", ".join(output) + "]" diff --git a/ds/disjoint_sets.py b/ds/disjoint_sets.py index 51a83b4..6737f14 100644 --- a/ds/disjoint_sets.py +++ b/ds/disjoint_sets.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Disjoint sets data structure utilizing the union find algorithm. +Disjoint sets data structure utilizing the union find algorithm, see help(DisjointSets) for details. """ from typing import List from collections import defaultdict @@ -9,6 +9,7 @@ class DisjointSets(): """ Data structure for handling n objects labeled 0 to (n-1) that are arranged in a series of disjoint set. + Handles finding the root represenative node and joining sets quickly and efficiently. """ def __init__(self, n: int): @@ -36,7 +37,7 @@ def find_root(self, x: int) -> int: def join_sets(self, x: int, y: int) -> None: """ - Joins the 2 disjoint sets together to which x and y belong. If x and y belong to the same set, + Joins the 2 disjoint sets together to which x and y belong. If x and y belong to the same set, no action occurs. If they belong to different sets i.e. have different root nodes, then they are joined. The node belonging to the set that has a root with a lower rank is linked to the larger set root node. diff --git a/ds/heaps.py b/ds/heaps.py index c390368..66ea290 100644 --- a/ds/heaps.py +++ b/ds/heaps.py @@ -1,15 +1,19 @@ # -*- coding: utf-8 -*- """ -Min and max heap data structures. +Min and max heap data structures module, see help(MinHeap) and help(MaxHeap) for details. """ class MinHeap: """ - An implementation of a min-heap, code based on leetcode's template. Generally we would just use a list + An implementation of a min-heap, code based on leetcode's template. Generally we would just use a list and python's heapq package instead. This data structure implements the methods of heap operations manually i.e. push and pop. For simplicity, one can push each element of a collection separately to heapify them in their entirety and add them to the data structure. + + Min heaps are able to push and pop elements in O(log2(n)) time and give access to the min value in O(1) + time. This makes them useful when tracking the min element of a collection as elements are added and/or + removed. """ def __init__(self): @@ -33,7 +37,7 @@ def push(self, x) -> None: # before using these relationships. # Heapify by swapping elements until the parent nodes are all smaller than the child nodes - # This operation assumes that the rest of the nodes are already in an ordering that satisfies the + # This operation assumes that the rest of the nodes are already in an ordering that satisfies the # heap property. If this newly added node is smaller than its parent node, then swap it with the # parent node and continue swimming the new x up the tree until that is no longer the case. while (self.heap[idx_x - 1] < self.heap[idx_parent - 1] and idx_x > 1): @@ -104,7 +108,7 @@ def size(self) -> int: def top(self): """ - Returns the element at the top of the heap without removing it. + Returns the element at the top of the heap without removing it i.e. the min. """ return self.heap[0] @@ -120,10 +124,14 @@ def __len__(self): class MaxHeap: """ - An implementation of a max-heap, code based on leetcode's template. Generally we would just use a list + An implementation of a max-heap, code based on leetcode's template. Generally we would just use a list and python's heapq package instead. This data structure implements the methods of heap operations manually i.e. push and pop. For simplicity, one can push each element of a collection separately to heapify them in their entirety and add them to the data structure. + + Max heaps are able to push and pop elements in O(log2(n)) time and give access to the max value in O(1) + time. This makes them useful when tracking the max element of a collection as elements are added and/or + removed. """ def __init__(self): @@ -147,7 +155,7 @@ def push(self, x) -> None: # before using these relationships. # Heapify by swapping elements until the parent nodes are all larger than the child nodes - # This operation assumes that the rest of the nodes are already in an ordering that satisfies the + # This operation assumes that the rest of the nodes are already in an ordering that satisfies the # heap property. If this newly added node is larger than its parent node, then swap it with the # parent node and continue swimming the new x up the tree until that is no longer the case. while (self.heap[idx_x - 1] > self.heap[idx_parent - 1] and idx_x > 1): @@ -218,7 +226,7 @@ def size(self) -> int: def top(self): """ - Returns the element at the top of the heap without removing it. + Returns the element at the top of the heap without removing it i.e. the max. """ return self.heap[0] diff --git a/ds/linked_list.py b/ds/linked_list.py index d275bff..90ce108 100644 --- a/ds/linked_list.py +++ b/ds/linked_list.py @@ -15,14 +15,15 @@ class ListNode: A singly-linked list node. """ - def __init__(self, val, next_=None): + def __init__(self, val: int, next_=None): self.val = val self.next_ = next_ class LinkedList: """ - Singly-linked list data-structure. + Singly-linked list data-structure. Supports append and deletion operations at the head and tail in O(1) + time. Supports append and deletion operations at an arbitrary index in O(n) time. """ def __init__(self): @@ -36,7 +37,7 @@ def _get(self, index: int, return_prev: bool = False) -> Union[Optional[ListNode list or None if the index is out of range. Returns the node prior to the one at the given index if return_prev is set to True. If there is no node prior, None will be returned. The return order is (prior_node, node) if return_prev is True and just node alone if set to False. - + :param index: An integer index value denoting the element in the list to access. :param return_prev: Whether to also return the node prior to the one located at index as well. The default is False. @@ -51,10 +52,10 @@ def _get(self, index: int, return_prev: bool = False) -> Union[Optional[ListNode prev_node, node = node, node.next_ return (prev_node, node) if return_prev is True else node - def get(self, index: int, return_value: bool = True): + def get(self, index: int, return_value: bool = True) -> Optional[Union[ListNode, int]]: """ Retrieves the node or value of the node at the input index. - + :param index: An integer index value denoting the element in the list to access. :param return_value: Whether to return the value of the node or the node itself. The default is True. :returns: The value associated with a node or a pointer to the node itself at the index. @@ -65,52 +66,41 @@ def get(self, index: int, return_value: bool = True): else: return node.val if return_value is True else node - def addAtHead(self, val) -> None: + def insert(self, index: int = None, val: int = None) -> None: """ - In-place method for adding a new node with val as the associated value to the linked list at - the head which becomes the new head node. - - :param val: A value to be added to the linked list. - :returns: None - """ - if self.head is None: # No elements currently in the list - new_node = ListNode(val=val) # Create a new node - self.head, self.tail = new_node, new_node - else: # If we already have a head node, insert prior - self.head = ListNode(val=val, next_=self.head) - self.n += 1 # Update length of list counter + In-place method for adding a new node with a value of val at a given index in the linked list. - def addAtTail(self, val) -> None: - """ - In-place method for adding a new node with val as the associated value to the linked list at the - tail which becomes the new tail node. - - :param val: A value to be added to the linked list. - :returns: None. - """ - if self.tail is None: # No elements currently in the list - new_node = ListNode(val=val) # Create a new node - self.head, self.tail = new_node, new_node - else: # If we already have a tail node, insert at the end - self.tail.next_ = ListNode(val=val) # Add a new tail node - self.tail = self.tail.next_ # This new node is now the last node - self.n += 1 # Update length of list counter + If index == 0, then the new node will be added at the head. If index == n or None, then the new node + will be added at the end of the list. The new node will become the node at the index provided. - def addAtIndex(self, index: int, val) -> None: + :param val: The value to be added to the linked list. + :param index: The index where the new value should be inserted, the default is None, which will result + in the new node being appended to the end. + :returns: None, adds a new node to the data structure. """ - In-place method for adding a new node with val as the associated value to the linked list at the - location described by index. The new node will become the node at the index provided. - - :param index: An integer index value denoting the location in the linked list to add a new element - An index of 0 is equlivalent to calling addAtHead. - :param val: A value to be added to the linked list. - :returns: None. - """ - if index == self.n: # If index is 1 beyond the valid range of indices - self.addAtTail(val) # Append the new node to the tail - elif index == 0: # Append at the head if index is 0 - self.addAtHead(val) - else: # Otherwise, insert before the node located at index + if val is None: + raise ValueError("val must not be None") + + index = self.n if index is None else index + if index < 0 or index > self.n: + raise IndexError(f"Index={index} out of range") + + if index == 0: # Then insert before the head node, create a new head node with this value + if self.head is None: # No elements currently in the list + new_node = ListNode(val=val) # Create a new node + self.head, self.tail = new_node, new_node + else: # If we already have a head node, insert prior + self.head = ListNode(val=val, next_=self.head) + self.n += 1 # Update length of list counter + + elif index == self.n: # Insert at the end, append a new node to the tail + # If the length of the list is 0, then insertion will be at index 0 and handled above, otherwise + # the length will be >= 1 so there must already be a tail node present + self.tail.next_ = ListNode(val=val) # Add a new tail node + self.tail = self.tail.next_ # This new node is now the last node + self.n += 1 # Update length of list counter + + else: # Otherwise insert the new node at an index somewhere internally, between the head and tail # Attempt to get this node from the list and it's predessor prev_node, node = self._get(index, return_prev=True) if node is not None: # If index was valid, operate on the returned node @@ -118,16 +108,17 @@ def addAtIndex(self, index: int, val) -> None: prev_node.next_ = new_node self.n += 1 # Update length of list counter - def deleteAtIndex(self, index: int) -> None: + def pop(self, index: int) -> Optional[int]: """ - In-place method for deleting a node located at a particular index in the linked list. Performs the - operation if possible, does nothing if not the index is not valid. + In-place method for deleting a node located at a particular index in the linked list and returning + the value associated. Performs the operation if possible, does nothing if not the index is not valid. - :param index: An integer denoting the location of the node to be deleted. - :returns: None. + :param index: An integer denoting the location of the node to be deleted. Must be [0, n-1]. + :returns: The associated value if possiable for this node to be removed. """ prev_node, node = self._get(index, return_prev=True) if node is not None: # If index was valid, operate on the returned node + ans = node.val # Make note of what value this is before removing the node if self.n == 1: # Remove the only node in the linked list self.head, self.tail = None, None else: # Then there are at least 2 nodes in the linked list, we will @@ -141,6 +132,7 @@ def deleteAtIndex(self, index: int) -> None: else: # Delete some middle element in the linked list prev_node.next_ = next_node self.n -= 1 # Update length of list counter + return ans def __len__(self) -> int: return self.n @@ -157,7 +149,17 @@ def __getitem__(self, index: int) -> ListNode: if node is not None: return node else: - raise KeyError(f"Index {index} is out of range") + raise IndexError(f"Index={index} is out of range") + + def __setitem__(self, index: int, val: int) -> None: + """ + Supports obj[index] = val updates to existing nodes in the linked list. + """ + node = self.get(index, return_value=False) + if node is None: + raise IndexError(f"Index={index} out of range") + else: # Update the value associated with this node + node.val = val def __iter__(self) -> Optional[ListNode]: """ @@ -176,6 +178,9 @@ def __iter__(self) -> Optional[ListNode]: yield yield_node def __repr__(self) -> str: + """ + Returns a string representation of the linked list. + """ if self.head is None: return "[]" else: @@ -187,6 +192,9 @@ def __repr__(self) -> str: return "[" + ", ".join(node_vals) + "]" def __str__(self) -> str: + """ + Returns a string representation of the linked list. + """ return self.__repr__() @@ -207,7 +215,8 @@ def __init__(self, val, prev_=None, next_=None): class DoublyLinkedList: """ - A doubly-linked list data-structure. + A doubly-linked list data-structure.Supports append and deletion operations at the head and tail in O(1) + time. Supports append and deletion operations at an arbitrary index in O(n) time. """ def __init__(self): @@ -219,7 +228,7 @@ def _get(self, index: int) -> Optional[DoublyListNode]: """ Internal helper function for the get method. Returns the node located at index within the linked list or None if the index is out of range. - + :param index: An integer index value denoting the element in the list to access. :returns: Either the node located at index in the linked list if the index is in range or None. """ @@ -241,7 +250,7 @@ def _get(self, index: int) -> Optional[DoublyListNode]: def get(self, index: int, return_value: bool = True): """ Retrieves the node or value of the node at the input index. - + :param index: An integer index value denoting the element in the list to access. :param return_value: Whether to return the value of the node or the node itself. The default is True. :returns: The value associated with a node or a pointer to the node itself at the index. @@ -256,7 +265,7 @@ def addAtHead(self, val) -> None: """ In-place method for adding a new node with val as the associated value to the linked list at the head which becomes the new head node. - + :param val: A value to be added to the linked list. :returns: None. """ @@ -273,7 +282,7 @@ def addAtTail(self, val) -> None: """ In-place method for adding a new node with val as the associated value to the linked list at the tail which becomes the new tail node. - + :param val: A value to be added to the linked list. :returns: None. """ @@ -290,7 +299,7 @@ def addAtIndex(self, index: int, val) -> None: """ In-place method for adding a new node with val as the associated value to the linked list at the location described by index. The new node will become the node at the index provided. - + :param index: An integer index value denoting the location in the linked list to add a new element. An index of 0 is equlivalent to calling addAtHead. :param val: A value to be added to the linked list. @@ -312,7 +321,7 @@ def deleteAtIndex(self, index: int) -> None: """ In-place method for deleting a node located at a particular index in the linked list. Performs the operation if possible, does nothing if not the index is not valid. - + :param index: An integer denoting the location of the node to be deleted. :returns: None. """ diff --git a/tests/README.txt b/tests/README.txt index 4c61fbe..6d5e7ef 100644 --- a/tests/README.txt +++ b/tests/README.txt @@ -1 +1 @@ -Run tests from the command line using: python -m pytest --cov=ds/ --cov-report=html:/tests/code_coverage \ No newline at end of file +Run tests from the command line using: python -m pytest --cov=ds/ --cov-report=html:tests/code_coverage \ No newline at end of file diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index 32aa6fc..002dc2b 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -10,54 +10,65 @@ def test_LinkedList(): test_data = [1, 5, 8, 7] obj = LinkedList() - with pytest.raises(KeyError): # Test indexing out of range + with pytest.raises(IndexError): # Test indexing out of range obj[7] - obj.addAtHead(5) - obj.deleteAtIndex(0) - assert len(obj) == 0, "delete from size 1 list test failed" + with pytest.raises(IndexError): # Test inserting out of range + obj.insert(5, 20) - assert str(obj) == "[]" + with pytest.raises(ValueError): # Test inserting without providing a value + obj.insert(5) + + with pytest.raises(IndexError): # Test updating a node at a non-existant index + obj[10] = 5 + + obj.insert(0, 5) + assert obj.pop(0) == 5, "Test for pop failed" + assert len(obj) == 0, "Pop from size 1 list test failed" + + assert str(obj) == "[]", "Test for str representation of linked list failed" for x in test_data: - obj.addAtTail(x) + obj.insert(len(obj), x) assert len(obj) == len(test_data), "len comparison test failed" assert str(test_data) == str(obj), "str representation comparison test failed" for a, b in zip(test_data, obj): assert a == b.val - # assert test_data == [x.val for x in obj], "values iteration test failed - - obj.addAtTail(10) - assert obj[-1].val == 10, "Test for addAtTail failed" + obj.insert(len(obj), 10) + assert obj[-1].val == 10, "Test for insert failed" - obj.addAtHead(-15) - assert obj[0].val == -15, "Test for addAtHead failed" + obj.insert(0, -15) + assert obj[0].val == -15, "Test for insert failed" - obj.addAtIndex(3, 845315) - assert obj[3].val == 845315, "Test for addAtIndex failed" + obj.insert(3, 845315) + assert obj[3].val == 845315, "Test for insert failed" val = obj[4].val - obj.deleteAtIndex(3) - assert obj[3].val == val, "Test for deleteAtIndex failed" + assert obj[3].val == obj.pop(3), "Test for pop failed" + assert obj[3].val == val, "Test for pop at index failed" obj = LinkedList() - obj.addAtHead(15) - assert obj.head.val == 15, "Test for addAtHead empty list failed" + obj.insert(None, 15) + assert obj.head.val == 15, "Test for insert in empty list failed" - obj.addAtIndex(len(obj), 6) - assert obj[-1].val == 6, "Test for addAtIndex at index n failed" + obj.insert(len(obj), 6) + assert obj[-1].val == 6, "Test for insert at index n failed" - obj.addAtIndex(0, 14) - assert obj[0].val == 14, "Test for addAtIndex at 0 failed" + obj.insert(0, 14) + assert obj[0].val == 14, "Test for insert at 0 failed" val = obj[-2].val - obj.deleteAtIndex(len(obj) - 1) - assert obj.tail.val == val, "Test for deleteAtIndex at index n failed" + obj.pop(len(obj) - 1) + assert obj.tail.val == val, "Test for pop at index n-1 failed" val = obj[1].val - obj.deleteAtIndex(0) - assert obj.head.val == val, "Test fordeleteAtIndex for index 0 failed " + assert obj[0].val == obj.pop(0), "Test pop for index 0 failed " + assert obj.head.val == val, "Test pop for index 0 failed " + + obj.insert(1, 50) + obj[1] = 5 + assert obj[1].val == 5, "Test for set item failed" def test_DoublyLinkedList(): @@ -200,7 +211,7 @@ def test_BinarySearchTree(): for x in list(range(30, 40)) + list(range(25)): obj.insert(x) - assert len(obj) == 35 + assert len(obj) == 35, "Search for __len__ value check failed" assert obj.print_tree() is None # Should give a print statement because the tree is too deep assert obj.search(10).val == 10, "Search for valid value check failed" @@ -218,19 +229,19 @@ def test_BinarySearchTree(): 20, 19, 18, 21, 24, 23, 30, 36, 34, 33, 32, 35, 38, 37, 39] inOrderTraversal = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39] - assert obj.inOrderTraversal() == inOrderTraversal - assert [x for x in obj] == inOrderTraversal - assert str(obj) == str(inOrderTraversal) + assert obj.inOrderTraversal() == inOrderTraversal, "Test for inOrderTraversal failed" + assert [x for x in obj] == inOrderTraversal, "Test for inOrderTraversal failed" + assert str(obj) == str(inOrderTraversal), "Test for inOrderTraversal failed" assert [x.val for x in obj.inOrderTraversal(return_vals=False)] == inOrderTraversal postOrderTraversal = [0, 1, 3, 2, 5, 7, 6, 4, 9, 10, 12, 11, 14, 16, 15, 13, 8, 18, 19, 21, 20, 23, 30, 24, 22, 32, 33, 35, 34, 37, 39, 38, 36, 31, 17] - assert obj.postOrderTraversal() == postOrderTraversal + assert obj.postOrderTraversal() == postOrderTraversal, "Test for postOrderTraversal failed" assert [x.val for x in obj.postOrderTraversal(return_vals=False)] == postOrderTraversal levelOrderTraversal = [17, 8, 31, 4, 13, 22, 36, 2, 6, 11, 15, 20, 24, 34, 38, 1, 3, 5, 7, 10, 12, 14, 16, 19, 21, 23, 30, 33, 35, 37, 39, 0, 9, 18, 32] - assert obj.levelOrderTraversal() == levelOrderTraversal + assert obj.levelOrderTraversal() == levelOrderTraversal, "Test for levelOrderTraversal failed" assert [x.val for x in obj.levelOrderTraversal(return_vals=False)] == levelOrderTraversal levels = [[17], [8, 31], @@ -238,10 +249,10 @@ def test_BinarySearchTree(): [2, 6, 11, 15, 20, 24, 34, 38], [1, 3, 5, 7, 10, 12, 14, 16, 19, 21, 23, 30, 33, 35, 37, 39], [0, 9, 18, 32]] - assert obj.levelOrderTraversal(return_levels=True) == levels + assert obj.levelOrderTraversal(return_levels=True) == levels, "Test for levelOrderTraversal failed" - assert obj.isValidBST() is True - assert obj.get_max_depth() == 6 + assert obj.isValidBST() is True, "Test for isValidBST failed" + assert obj.get_max_depth() == 6, "Test for get_max_depth failed" for val in [30, 32, 18, 1, 17]: obj.delete(val) @@ -260,29 +271,29 @@ def test_Deque(): Runs basic tests for the Deque data structure, tests methods and functionality. """ obj = Deque(10) - assert obj.isEmpty() is True + assert obj.is_empty() is True, "Test for isEmpty failed" obj.append(5) - obj.appendLeft(1) + obj.append_left(1) - assert obj.getFront() == 1 - assert obj.getRear() == 5 + assert obj.get_front() == 1, "Test for getFront failed" + assert obj.get_rear() == 5, "Test for getRear failed" for i in range(10): obj.append(i) - assert obj.isFull() is True - assert obj.pop() == 7 - assert obj.popLeft() == 1 - assert obj.isFull() is False - assert len(obj) == 8 - assert str(obj) == "[5, 0, 1, 2, 3, 4, 5, 6]" + assert obj.is_full() is True, "Test for isFull failed" + assert obj.pop() == 7, "Test for pop failed" + assert obj.pop_left() == 1, "Test for popLeft failed" + assert obj.is_full() is False, "Test for isFull failed" + assert len(obj) == 8, "Test for __len__ failed" + assert str(obj) == "[5, 0, 1, 2, 3, 4, 5, 6]", "Test for __str__ failed" obj = Deque(10) - assert obj.pop() is None - assert obj.popLeft() is None - obj.appendLeft(5) - assert obj.pop() == 5 + assert obj.pop() is None, "Test for pop failed" + assert obj.pop_left() is None, "Test for popLeft failed" + obj.append_left(5) + assert obj.pop() == 5, "Test for pop failed" obj.append(8) - assert obj.popLeft() == 8 + assert obj.pop_left() == 8, "Test for popLeft failed" def test_MinHeap(): @@ -296,18 +307,18 @@ def test_MinHeap(): obj.pop() obj.push(5) - assert obj.pop() == 5 + assert obj.pop() == 5, "Test for pop failed" for x in test_data: obj.push(x) - assert len(obj) == len(test_data) - assert str(obj) == '[-9, 0, 1, 2, 12, 1, 8, 6]' - assert obj.top() == min(test_data) + assert len(obj) == len(test_data), "Test for __len__ failed" + assert str(obj) == '[-9, 0, 1, 2, 12, 1, 8, 6]', "Test for __str__ failed" + assert obj.top() == min(test_data), "Test for top failed" test_data.sort(reverse=True) while test_data: - assert test_data.pop() == obj.pop() + assert test_data.pop() == obj.pop(), "Test for pop failed" def test_MaxHeap(): @@ -321,18 +332,18 @@ def test_MaxHeap(): obj.pop() obj.push(5) - assert obj.pop() == 5 + assert obj.pop() == 5, "Test for pop failed" for x in test_data: obj.push(x) - assert len(obj) == len(test_data) - assert str(obj) == '[12, 6, 8, 2, 1, -9, 1, 0]' - assert obj.top() == max(test_data) + assert len(obj) == len(test_data), "Test for __len__ failed" + assert str(obj) == '[12, 6, 8, 2, 1, -9, 1, 0]', "Test for __str__ failed" + assert obj.top() == max(test_data), "Test for top failed" test_data.sort() while test_data: - assert test_data.pop() == obj.pop() + assert test_data.pop() == obj.pop(), "Test for pop failed" def test_Trie(): @@ -423,7 +434,12 @@ def test_LFUCache(): assert obj.dict == {1: [5, 5], 2: [10, 3], 7: [35, 1]}, "Test for put failed" obj.put(7, 7 * 4) - assert obj.get(7) == 7 * 4, "Test for get failed" + assert obj.get(7) == 7 * 4, "Test for put failed" + + obj[7] = 7 * 3 + assert obj.get(7) == 7 * 3, "Test for obj[key]=val put method failed" + + assert len(obj) == 3, "Test for len(obj) failed" def test_LRUCache(): @@ -441,4 +457,9 @@ def test_LRUCache(): assert set(obj.dict.keys()) == set([1, 3, 7]), "Test for put failed" obj.put(1, 1 * 4) - assert obj.get(1) == 1 * 4, "Test for get failed" + assert obj.get(1) == 1 * 4, "Test for put failed" + + obj[7] = 7 * 3 + assert obj.get(7) == 7 * 3, "Test for obj[key]=val put method failed" + + assert len(obj) == 3, "Test for len(obj) failed" From ce4faf7161cf2b4df43c7f81323a65cf41c9b781 Mon Sep 17 00:00:00 2001 From: EH225 Date: Tue, 25 Mar 2025 12:15:23 -0400 Subject: [PATCH 2/7] remove pycache files from git --- .../binary_index_tree.cpython-311.pyc | Bin 6968 -> 0 bytes .../binary_search_tree.cpython-311.pyc | Bin 30540 -> 0 bytes ds/__pycache__/disjoint_sets.cpython-311.pyc | Bin 4406 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ds/__pycache__/binary_index_tree.cpython-311.pyc delete mode 100644 ds/__pycache__/binary_search_tree.cpython-311.pyc delete mode 100644 ds/__pycache__/disjoint_sets.cpython-311.pyc diff --git a/ds/__pycache__/binary_index_tree.cpython-311.pyc b/ds/__pycache__/binary_index_tree.cpython-311.pyc deleted file mode 100644 index bb8dcd06ec3928cfec9a163439ec56ab27040e5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6968 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 PhhMSpzgZ@hb<_U?fv&d| diff --git a/ds/__pycache__/disjoint_sets.cpython-311.pyc b/ds/__pycache__/disjoint_sets.cpython-311.pyc deleted file mode 100644 index 1dda8e6fdb6b2909fdbe1bb980d1eb3f8a3ebf50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4406 zcmb_f-ER}w6~8kcJ5Fp!LQEjQhG7#RUb2R;3EEbG)ROMfLM>6~s*PA#gJ+yfGMO!5jZ_J>>chUdO4|pX`a5^*v7H8~ zD|I|R_xs#)&&Thc`Fziw9)`#G<6mcgx0kWMQKR`tjmq|URMwfwW|%6dq9H7bGonEC zkP%vxW+Z`$?0u$6511N$CNlOMfB%^YtC8z0*|n1Enbw_oOE+EF(OgGX6<3iR*Dhq; zg00B~S2y$(-JF%(IkZgOGUc3Zsbvd%O%Bj9|ZD$ImbQIm3_8hRi> zdP$SiFxrwDK|8|RUDT&Wa}l*0eO;|Sjrw}f*Nrtj_(?;-o~;^q$1o?RMxmLw7jHk# z%Aa~T}vJ_-=0W9dx|S7P^eoPIMAgQ})0Nb)$+1B#5=4htk>UT8Rv{Z0~MaYOWkDKv! zLC-E2%X~n~<@BtsnXa+ybM^!?8+ZZ~LH-oNfBP{A{7HDgTmjX_Qxu+|^*rdAX3sG4 zygAH@EE9~W;@4gqNKzEQiR9o9Z#6v~J`S6N(%E1m%%Q0R_;2q6S!c5lT+5#XC2@bNTGqshiW9v*23!n^!f>&5|;& zYKqM@`F%?*zydcZCNe=xppCa-t_*2Am0y0bPKV(pq=1GW1wBN_I>+^M}t9xiUWo`mLt7cseZ>@J!kdXl3VIo@a& z$%{7iv$B6zwUNlqRgn9v9%To{p_fRbWz-RRDLlIfUkIU@|OJ2m*fOD@2hx^!doslEXqvk(Fp#>NV+;wfS zC|rl{mD_!@IUW7Guk8$}dty=SoJr7L6yfy3$}c$_5d8dr3+r$!=1{#hsJMQ-LDQ2~J+q)L@@N9>B3I=L-A|u=1J> ztg%esI7vZIQnblAylz6L49SuV*$2>LlX|=u*e_ZpA8;s6vBdYlBWSD)c33)KL7wJJ zgTT~1EWT$=dK4>t>(SoL*l;B_yfu(myy5G1>4#TJ!cWsL2Zk&0;f9Hq)7b!0z*)kd+S@9u1>3SR8vp(5guK_z6L;8Ax)&;j ziqZn#)BXu;8hAx9)bgB;{~IJmcSy|gzj^?bu~35-+&lA8hv5; zh20QNbFmE9|D58;ip*KwMdpOMVco@f2`ptDX@hPqATjXI6l`KB_ADmhbpBG>q!ouY z-F@k#WK&$(`-xCo+Jhj;a4RD1VRQiP5U3eKJt~)-Fgg*Ljo9?M=`b9#cSLB=e*z1f zKY$=Y_p(F7pA0`9-VmxoZ`Fq0+L-=r`Zwv{PgKV*)W$EY0cE#h{c9g=#tv6vhqrnM z9xj&#YrS%L^657=KB$dMZuU-AdMCF|jPo#mui7_K=^J_3zyHxt>0GV<&GL<>Ki;@g zJAQ7n|6Ha2+>Uv`fs=0s3t#pn9(9*aRQitLt@Rx%U#s>tTGhT|FXMfyR{#wY)%X#r ztm0Q6V(}x8i=)Ku2r+|W{{IoezcV!0+k zA7y=spMUhTk4j3lS0=Mgf1dtz`o%-4FC`e$>9hzw*L*X-jQXx z0o*aR15^c<)-@hLQ~*1j^QPH{XK<0?x-N4VZLVmR#T{VXg0^xS|K@DEemnUK?BRqP z?2U1u-27T;Ekdm`JMQGc4gbL|hNqN}z5~dh?Y86>xn-;V7lS(ooap>SYcqD2P3QI` z;$>56$+Io)0bWGO=QUIHBHXi_hDUf^>O2p4FNFS1uZRMUeK6=u%$nVouz@oNa-aPx z$_9pirB?Ti)b@?6cKv1GU@7rr@Z$@QF4UROJ;llDrLFkD+Vpz*7wPgl)x^mf-uT#` z~J^djzZ9$>?@ zLOGma01_zi_fnwdc4N?yYf>4)4L8>z%5hfh0umkPy78|K_ XzV*nr1pxx@PXEFC|M{9xewzOPHQg2J From 2ca87b99e7d846bbe740509419c64b37f359be2f Mon Sep 17 00:00:00 2001 From: EH225 Date: Tue, 25 Mar 2025 14:20:56 -0400 Subject: [PATCH 3/7] large updates to the linked list module and corresponding tests --- ds/linked_list.py | 321 +++++++++++++++++++++++----------- tests/test_data_structures.py | 115 ++++++++---- 2 files changed, 299 insertions(+), 137 deletions(-) diff --git a/ds/linked_list.py b/ds/linked_list.py index 90ce108..1adfe97 100644 --- a/ds/linked_list.py +++ b/ds/linked_list.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """ -Singly and doubly linked-lists. +Singly and doubly linked-lists data structures module, see help(LinkedList) and help(DoublyLinkedList) for +details. """ from typing import Union, Optional, List, Tuple, Iterable @@ -31,7 +32,7 @@ def __init__(self): self.tail = None # Maintain a reference to the last node self.n = 0 # Record the total number of elements in the list - def _get(self, index: int, return_prev: bool = False) -> Union[Optional[ListNode], Tuple[ListNode]]: + def _get(self, index: int) -> Tuple[Optional[ListNode], Optional[ListNode]]: """ Internal helper function for the get method. Returns the node located at index within the linked list or None if the index is out of range. Returns the node prior to the one at the given index if @@ -45,12 +46,12 @@ def _get(self, index: int, return_prev: bool = False) -> Union[Optional[ListNode None. If return_prev is True, then a tuple of None or ListNode1 objects are returned. """ if index < 0 or index >= self.n: # Check for invalid indices - return (None, None) if return_prev is True else None + return None, None else: # Locate the element requested by traversing the list prev_node, node = None, self.head for i in range(index): prev_node, node = node, node.next_ - return (prev_node, node) if return_prev is True else node + return prev_node, node def get(self, index: int, return_value: bool = True) -> Optional[Union[ListNode, int]]: """ @@ -60,7 +61,7 @@ def get(self, index: int, return_value: bool = True) -> Optional[Union[ListNode, :param return_value: Whether to return the value of the node or the node itself. The default is True. :returns: The value associated with a node or a pointer to the node itself at the index. """ - node = self._get(index, return_prev=False) + prev_node, node = self._get(index) if node is None: return None else: @@ -91,50 +92,107 @@ def insert(self, index: int = None, val: int = None) -> None: self.head, self.tail = new_node, new_node else: # If we already have a head node, insert prior self.head = ListNode(val=val, next_=self.head) - self.n += 1 # Update length of list counter elif index == self.n: # Insert at the end, append a new node to the tail - # If the length of the list is 0, then insertion will be at index 0 and handled above, otherwise - # the length will be >= 1 so there must already be a tail node present - self.tail.next_ = ListNode(val=val) # Add a new tail node - self.tail = self.tail.next_ # This new node is now the last node - self.n += 1 # Update length of list counter + # If the length of the list is 0, then insertion will be at index 0 and handled above, otherwise + # the length will be >= 1 so there must already be a tail node present + self.tail.next_ = ListNode(val=val) # Add a new tail node + self.tail = self.tail.next_ # This new node is now the last node else: # Otherwise insert the new node at an index somewhere internally, between the head and tail - # Attempt to get this node from the list and it's predessor - prev_node, node = self._get(index, return_prev=True) - if node is not None: # If index was valid, operate on the returned node - new_node = ListNode(val=val, next_=node) - prev_node.next_ = new_node - self.n += 1 # Update length of list counter + # Attempt to get this node from the list and its predecessor + prev_node, node = self._get(index) + new_node = ListNode(val=val, next_=node) + prev_node.next_ = new_node - def pop(self, index: int) -> Optional[int]: + self.n += 1 # Update length of list counter + + def append(self, val: int) -> None: + """ + In-place method for appending a new value to the end of the linked list. This method is the same as + using obj.insert(len(obj), val). + + :param val: The value to be added to the end of the linked list. + :returns: None, adds a new node to the data structure. + """ + self.insert(self.n, val) + + def pop(self, index: int = None) -> int: """ In-place method for deleting a node located at a particular index in the linked list and returning - the value associated. Performs the operation if possible, does nothing if not the index is not valid. + the associated value. If index is left as None (not provided), the default behavior will be to pop + from the end of the list. If the index provided is not valid, an IndexError is raised. :param index: An integer denoting the location of the node to be deleted. Must be [0, n-1]. - :returns: The associated value if possiable for this node to be removed. - """ - prev_node, node = self._get(index, return_prev=True) - if node is not None: # If index was valid, operate on the returned node - ans = node.val # Make note of what value this is before removing the node - if self.n == 1: # Remove the only node in the linked list - self.head, self.tail = None, None - else: # Then there are at least 2 nodes in the linked list, we will - # have either a prev or next node or both - next_node = node.next_ - if prev_node is None: # Delete the first element in the list - self.head = node.next_ # Move head ref to next element - elif next_node is None: # Delete the last element in the list - self.tail = prev_node # Move the tail ref back 1 element - self.tail.next_ = None # Remove forward ref at new tail - else: # Delete some middle element in the linked list - prev_node.next_ = next_node - self.n -= 1 # Update length of list counter - return ans + :returns: The associated value if possible for this node to be removed. + """ + if self.n == 0: + raise IndexError("Cannot pop from an empty list") + + index = self.n - 1 if index is None else index + prev_node, node = self._get(index) + if node is None: + raise IndexError(f"Index={index} is out of range") + + ans = node.val # Make note of what value this is before removing the node + if self.n == 1: # Remove the only node in the linked list + self.head, self.tail = None, None + + else: # Then there are at least 2 nodes in the linked list, we will + # have either a prev or next node or both + next_node = node.next_ + if prev_node is None: # Delete the first element in the list + self.head = node.next_ # Move head ref to next element + elif next_node is None: # Delete the last element in the list + self.tail = prev_node # Move the tail ref back 1 element + self.tail.next_ = None # Remove forward ref at new tail + else: # Delete some middle element in the linked list + prev_node.next_ = next_node + + self.n -= 1 # Update length of list counter + return ans + + def index(self, val: int) -> int: + """ + Returns the first index where a given input value occurs in the linked list. If the provided value + cannot be found, an index error is raised. + """ + idx = 0 # Track the index of the node as the linked list is traversed + node = self.head # Begin with the head node + while node is not None: # Iterate until we reach the tail + if node.val == val: # Check if the target value is matched, if so return the index of occurence + return idx + else: # Otherwise move to the next node and keep searching + node = node.next_ + idx += 1 + raise IndexError(f"Could not locate {val} in linked list") + + def reverse(self) -> None: + """ + In-place method that reverses the order of elements stored in the linked list. + """ + if self.n > 1: # Only need to take action if there is more than 1 node in the linked list + stack = [] # Use a stack to perform a LIFO traversal of the nodes + node = self.head + + while node is not None: + stack.append(node) + node = node.next_ + + self.head = stack.pop() # The old tail becomes the new head + last_node = self.head # Keep track of the prior node so that we can add the next_ linkage + while stack: + node = stack.pop() + last_node.next_ = node + last_node = node # Update ref for next iteration + + node.next_ = None # The new tail node is the old head node, set the next_ pointer to None + self.tail = node # Update the pointer to the tail node def __len__(self) -> int: + """ + Returns the length of the linked list. + """ return self.n def __getitem__(self, index: int) -> ListNode: @@ -235,9 +293,9 @@ def _get(self, index: int) -> Optional[DoublyListNode]: if index < 0 or index >= self.n: # Check for invalid indices return None else: # Locate the element requested by traversing the list - # We can find it quickest by iterating from the side that - # the index is closest to i.e. either the head or tail - if index + 1 < self.n / 2: # The index is in the first half + # We can find it faster by iterating from the side that the index is closest to + # # i.e. either the head or tail and moving inwards + if index + 1 <= self.n // 2: # The index is in the first half node = self.head # Start from the head and move right for i in range(index): node = node.next_ @@ -261,88 +319,127 @@ def get(self, index: int, return_value: bool = True): else: return node.val if return_value is True else node - def addAtHead(self, val) -> None: + def insert(self, index: int = None, val: int = None) -> None: """ - In-place method for adding a new node with val as the associated value to the linked list at the head - which becomes the new head node. + In-place method for adding a new node with a value of val at a given index in the linked list. - :param val: A value to be added to the linked list. - :returns: None. - """ - if self.head is None: # No elements currently in the list - new_node = DoublyListNode(val=val) # Create a new node - self.head, self.tail = new_node, new_node - else: # If we already have a head node, insert prior - new_node = DoublyListNode(val=val, next_=self.head) - self.head.prev_ = new_node # Link back to the new node - self.head = new_node # This new node is now the first node - self.n += 1 # Update length of list counter + If index == 0, then the new node will be added at the head. If index == n or None, then the new node + will be added at the end of the list. The new node will become the node at the index provided. - def addAtTail(self, val) -> None: + :param val: The value to be added to the linked list. + :param index: The index where the new value should be inserted, the default is None, which will result + in the new node being appended to the end. + :returns: None, adds a new node to the data structure. """ - In-place method for adding a new node with val as the associated value to the linked list at the - tail which becomes the new tail node. + if val is None: + raise ValueError("val must not be None") - :param val: A value to be added to the linked list. - :returns: None. - """ - if self.tail is None: # No elements currently in the list - new_node = DoublyListNode(val=val) # Create a new node - self.head, self.tail = new_node, new_node - else: # If we already have a tail node, insert at the end + index = self.n if index is None else index + if index < 0 or index > self.n: + raise IndexError(f"Index={index} out of range") + + if index == 0: # Then insert before the head node, create a new head node with this value + if self.head is None: # No elements currently in the list + new_node = DoublyListNode(val=val) # Create a new node + self.head, self.tail = new_node, new_node + else: # If we already have a head node, insert prior + new_node = DoublyListNode(val=val, next_=self.head) + self.head.prev_ = new_node # Link back to the new node + self.head = new_node # This new node is now the first node + + elif index == self.n: # Insert at the end, append a new node to the tail + # If the length of the list is 0, then insertion will be at index 0 and handled above, otherwise + # the length will be >= 1 so there must already be a tail node present new_node = DoublyListNode(val=val, prev_=self.tail) self.tail.next_ = new_node # Link ahead to the new node self.tail = new_node # This new node is now the last node + + else: # Otherwise insert the new node at an index somewhere internally, between the head and tail + # Attempt to get this node from the list and its predecessor + node = self._get(index) # Get a pointer to the node currently at this index in the list + prev_node = node.prev_ + new_node = DoublyListNode(val=val, next_=node, prev_=prev_node) + prev_node.next_, node.prev_ = new_node, new_node + self.n += 1 # Update length of list counter - def addAtIndex(self, index: int, val) -> None: + def append(self, val: int) -> None: """ - In-place method for adding a new node with val as the associated value to the linked list at the - location described by index. The new node will become the node at the index provided. + In-place method for appending a new value to the end of the linked list. This method is the same as + using obj.insert(len(obj), val). - :param index: An integer index value denoting the location in the linked list to add a new - element. An index of 0 is equlivalent to calling addAtHead. - :param val: A value to be added to the linked list. - :returns: None. + :param val: The value to be added to the end of the linked list. + :returns: None, adds a new node to the data structure. """ - if index == self.n: # If index is 1 beyond the valid range of indices - self.addAtTail(val) # Append the new node to the tail - elif index == 0: # Append at the head if index is 0 - self.addAtHead(val) - else: # Otherwise, insert before the node located at index - node = self._get(index) # Attempt to get this node from the list - if node is not None: # If index was valid, operate on the returned node - prev_node = node.prev_ - new_node = DoublyListNode(val=val, next_=node, prev_=prev_node) - prev_node.next_, node.prev_ = new_node, new_node - self.n += 1 # Update length of list counter + self.insert(self.n, val) - def deleteAtIndex(self, index: int) -> None: + def pop(self, index: int = None) -> int: """ - In-place method for deleting a node located at a particular index in the linked list. Performs the - operation if possible, does nothing if not the index is not valid. + In-place method for deleting a node located at a particular index in the linked list and returning + the associated value. If index is left as None (not provided), the default behavior will be to pop + from the end of the list. If the index provided is not valid, an IndexError is raised. - :param index: An integer denoting the location of the node to be deleted. - :returns: None. + :param index: An integer denoting the location of the node to be deleted. Must be [0, n-1]. + :returns: The associated value if possible for this node to be removed. """ + if self.n == 0: + raise IndexError("Cannot pop from an empty list") + + index = self.n - 1 if index is None else index + node = self._get(index) # Attempt to get this node from the list - if node is not None: # If index was valid, operate on the returned node - if self.n == 1: # Remove the only node in the linked list - self.head, self.tail = None, None - else: # Then there are at least 2 nodes in the linked list, we will - # have either a prev or next node or both - prev_node, next_node = node.prev_, node.next_ - if prev_node is None: # Delete the first element in the list - self.head = node.next_ # Move head ref to next element - self.head.prev_ = None # Remove backward ref at new head - elif next_node is None: # Delete the last element in the list - self.tail = node.prev_ # Move the tail ref back 1 element - self.tail.next_ = None # Remove forward ref at new tail - else: # Delete some middle element in the linked list - prev_node.next_, next_node.prev_ = next_node, prev_node - self.n -= 1 # Update length of list counter + if node is None: + raise IndexError(f"Index={index} is out of range") + + ans = node.val # Make note of what value this is before removing the node + if self.n == 1: # Remove the only node in the linked list + self.head, self.tail = None, None + + else: # Then there are at least 2 nodes in the linked list, we will + # have either a prev or next node or both + prev_node, next_node = node.prev_, node.next_ + if prev_node is None: # Delete the first element in the list + self.head = node.next_ # Move head ref to next element + self.head.prev_ = None # Remove backward ref at new head + elif next_node is None: # Delete the last element in the list + self.tail = node.prev_ # Move the tail ref back 1 element + self.tail.next_ = None # Remove forward ref at new tail + else: # Delete some middle element in the linked list + prev_node.next_, next_node.prev_ = next_node, prev_node + + self.n -= 1 # Update length of list counter + return ans + + def index(self, val: int) -> int: + """ + Returns the first index where a given input value occurs in the linked list. If the provided value + cannot be found, an index error is raised. + """ + idx = 0 # Track the index of the node as the linked list is traversed + node = self.head # Begin with the head node + while node is not None: # Iterate until we reach the tail + if node.val == val: # Check if the target value is matched, if so return the index of occurence + return idx + else: # Otherwise move to the next node and keep searching + node = node.next_ + idx += 1 + raise IndexError(f"Could not locate {val} in linked list") + + def reverse(self) -> None: + """ + In-place method that reverses the order of elements stored in the linked list. + """ + if self.n > 1: # Only need to take action if there is more than 1 node in the linked list + node = self.head # The old head becomes the new tail + self.head, self.tail = self.tail, self.head # Update pointers + while node is not None: # Iterate through and reverse the pointers + node.next_, node.prev_ = node.prev_, node.next_ + node = node.prev_ # The next node to visit is now the prev node def __len__(self) -> int: + """ + Returns the length of the linked list. + """ return self.n def __getitem__(self, index: int) -> Optional[DoublyListNode]: @@ -357,7 +454,17 @@ def __getitem__(self, index: int) -> Optional[DoublyListNode]: if node is not None: return node else: - raise KeyError(f"Index {index} is out of range") + raise IndexError(f"Index {index} is out of range") + + def __setitem__(self, index: int, val: int) -> None: + """ + Supports obj[index] = val updates to existing nodes in the linked list. + """ + node = self.get(index, return_value=False) + if node is None: + raise IndexError(f"Index={index} out of range") + else: # Update the value associated with this node + node.val = val def __iter__(self) -> Optional[DoublyListNode]: """ @@ -376,6 +483,9 @@ def __iter__(self) -> Optional[DoublyListNode]: yield yield_node def __repr__(self) -> str: + """ + Returns a string representation of the linked list. + """ if self.head is None: return "[]" else: @@ -387,4 +497,7 @@ def __repr__(self) -> str: return "[" + ", ".join(node_vals) + "]" def __str__(self) -> str: + """ + Returns a string representation of the linked list. + """ return self.__repr__() diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index 002dc2b..d335b05 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -22,18 +22,39 @@ def test_LinkedList(): with pytest.raises(IndexError): # Test updating a node at a non-existant index obj[10] = 5 + with pytest.raises(IndexError): # Test popping from an empty list + obj.pop() + obj.insert(0, 5) + with pytest.raises(IndexError): # Test popping from out of range + obj.pop(5) + assert obj.pop(0) == 5, "Test for pop failed" assert len(obj) == 0, "Pop from size 1 list test failed" assert str(obj) == "[]", "Test for str representation of linked list failed" for x in test_data: - obj.insert(len(obj), x) + obj.append(x) + + with pytest.raises(IndexError): # Test the index method on a value that doesn't exist + obj.index(20) assert len(obj) == len(test_data), "len comparison test failed" assert str(test_data) == str(obj), "str representation comparison test failed" - for a, b in zip(test_data, obj): + assert obj.head.val == test_data[0], "Test for obj.head failed" + assert obj.tail.val == test_data[-1], "Test for obj.tail failed" + for idx, (a, b) in enumerate(zip(test_data, obj)): assert a == b.val + assert obj.index(a) == idx + + obj.reverse() # Reverse the order of the elements and run tests + assert obj.head.val == test_data[-1], "Test for obj.head failed" + assert obj.tail.val == test_data[0], "Test for obj.tail failed" + for idx, (a, b) in enumerate(zip(test_data[::-1], obj)): + assert a == b.val + assert obj.index(a) == idx + + obj.reverse() obj.insert(len(obj), 10) assert obj[-1].val == 10, "Test for insert failed" @@ -78,60 +99,88 @@ def test_DoublyLinkedList(): test_data = [1, 5, 8, 7] obj = DoublyLinkedList() - with pytest.raises(KeyError): # Test indexing out of range + with pytest.raises(IndexError): # Test indexing out of range obj[7] - obj.addAtHead(5) - obj.deleteAtIndex(0) - assert len(obj) == 0, "delete from size 1 list test failed" + with pytest.raises(IndexError): # Test inserting out of range + obj.insert(5, 20) + + with pytest.raises(ValueError): # Test inserting without providing a value + obj.insert(5) - assert str(obj) == "[]" + with pytest.raises(IndexError): # Test updating a node at a non-existant index + obj[10] = 5 + + with pytest.raises(IndexError): # Test popping from an empty list + obj.pop() + + obj.insert(0, 5) + with pytest.raises(IndexError): # Test popping from out of range + obj.pop(5) + + assert obj.pop(0) == 5, "Test for pop failed" + assert len(obj) == 0, "Pop from size 1 list test failed" + + assert str(obj) == "[]", "Test for str representation of linked list failed" for x in test_data: - obj.addAtTail(x) + obj.append(x) + + with pytest.raises(IndexError): # Test the index method on a value that doesn't exist + obj.index(20) assert len(obj) == len(test_data), "len comparison test failed" assert str(test_data) == str(obj), "str representation comparison test failed" - for a, b in zip(test_data, obj): + assert obj.head.val == test_data[0], "Test for obj.head failed" + assert obj.tail.val == test_data[-1], "Test for obj.tail failed" + for idx, (a, b) in enumerate(zip(test_data, obj)): assert a == b.val + assert obj.index(a) == idx - # assert test_data == [x.val for x in obj], "values iteration test failed" - - assert obj.head.val == 1, "head test failed" - assert obj.tail.val == 7, "tail test failed" + obj.reverse() # Reverse the order of the elements and run tests + assert obj.head.val == test_data[-1], "Test for obj.head failed" + assert obj.tail.val == test_data[0], "Test for obj.tail failed" + for idx, (a, b) in enumerate(zip(test_data[::-1], obj)): + assert a == b.val + assert obj.index(a) == idx - for i in range(1, len(obj)): - assert obj[i].prev_.val == test_data[i - 1] + obj.reverse() - obj.addAtTail(10) - assert obj[-1].val == 10, "Test for addAtTail failed" + obj.insert(len(obj), 10) + assert obj[-1].val == 10, "Test for insert failed" - obj.addAtHead(-15) - assert obj[0].val == -15, "Test for addAtHead failed" + obj.insert(0, -15) + assert obj[0].val == -15, "Test for insert failed" - obj.addAtIndex(3, 845315) - assert obj[3].val == 845315, "Test for addAtIndex failed" + obj.insert(3, 845315) + assert obj[3].val == 845315, "Test for insert failed" val = obj[4].val - obj.deleteAtIndex(3) - assert obj[3].val == val, "Test for deleteAtIndex failed" + assert obj[3].val == obj.pop(3), "Test for pop failed" + assert obj[3].val == val, "Test for pop at index failed" obj = DoublyLinkedList() - obj.addAtHead(15) - assert obj.head.val == 15, "Test for addAtHead empty list failed" + obj.insert(None, 15) + assert obj.head.val == 15, "Test for insert in empty list failed" - obj.addAtIndex(len(obj), 6) - assert obj[-1].val == 6, "Test for addAtIndex at index n failed" + obj.insert(len(obj), 6) + assert obj[-1].val == 6, "Test for insert at index n failed" - obj.addAtIndex(0, 14) - assert obj[0].val == 14, "Test for addAtIndex at 0 failed" + obj.insert(0, 14) + assert obj[0].val == 14, "Test for insert at 0 failed" val = obj[-2].val - obj.deleteAtIndex(len(obj) - 1) - assert obj.tail.val == val, "Test for deleteAtIndex at index n failed" + obj.pop(len(obj) - 1) + assert obj.tail.val == val, "Test for pop at index n-1 failed" val = obj[1].val - obj.deleteAtIndex(0) - assert obj.head.val == val, "Test for deleteAtIndex for index 0 failed " + assert obj[0].val == obj.pop(0), "Test pop for index 0 failed " + assert obj.head.val == val, "Test pop for index 0 failed " + + obj.insert(1, 50) + obj.insert(2, 70) + obj.insert(3, 80) + obj[1] = 5 + assert obj[1].val == 5, "Test for set item failed" def test_BinaryIndexTree(): From 0bcfe6883cd0c6b9e21742594f9140c29b992290 Mon Sep 17 00:00:00 2001 From: EH225 Date: Tue, 25 Mar 2025 15:13:59 -0400 Subject: [PATCH 4/7] fixed isValidBST method and updated tests for BST --- ds/__pycache__/segment_tree.cpython-311.pyc | Bin 15921 -> 0 bytes ds/__pycache__/trie.cpython-311.pyc | Bin 9635 -> 0 bytes ds/binary_search_tree.py | 20 ++++++---- ds/segment_tree.py | 37 +++++++++++------ ds/trie.py | 10 ++--- tests/test_data_structures.py | 42 ++++++++++++++++++++ 6 files changed, 85 insertions(+), 24 deletions(-) delete mode 100644 ds/__pycache__/segment_tree.cpython-311.pyc delete mode 100644 ds/__pycache__/trie.cpython-311.pyc diff --git a/ds/__pycache__/segment_tree.cpython-311.pyc b/ds/__pycache__/segment_tree.cpython-311.pyc deleted file mode 100644 index be29a3d54e21fd388b4b9ffa062e39ba1c34cdc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15921 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 Y(Yn#`fyad*RZsuG%Kx~=T}J)?06fLmtpET3 diff --git a/ds/__pycache__/trie.cpython-311.pyc b/ds/__pycache__/trie.cpython-311.pyc deleted file mode 100644 index c0e1c78d4ef99ab69e4898737b856705f33c599b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9635 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 C$^)hV diff --git a/ds/binary_search_tree.py b/ds/binary_search_tree.py index 6b66413..0b07820 100644 --- a/ds/binary_search_tree.py +++ b/ds/binary_search_tree.py @@ -449,6 +449,9 @@ def isValidBST(self) -> bool: :returns: A bool indicating if the BST rooted at root is a valid BST. """ + if self.root is None: + return True + # Recursively evaluate both branches to check that this BST is valid return self._DFS(self.root.left, None, self.root.val) and self._DFS(self.root.right, self.root.val, None) @@ -465,18 +468,21 @@ def _DFS(self, node: Optional[TreeNode], low: Optional[Union[int, float]], """ if node is None: # If given a blank node, always return True, this is a base case return True - if high != None and node.val >= high: # If there is an upper limit established for this tree - # check to make sure that this root node's value is not as large or greater, that would violate - # the properties of a BST + + if high is not None and node.val > high: # If there is an upper limit established for this tree + # check to make sure that this root node's value is not greater, that would violate the + # properties of a BST return False - if low != None and node.val <= low: # If there is a lower bound limit established for this tree - # check to make sure that this root node's value is not as small or smaller, that would violate - # the properties of a BST + + if low is not None and node.val < low: # If there is a lower bound limit established for this tree + # check to make sure that this root node's value is not smaller, that would violate the properties + # of a BST return False if (node.right is None and node.left is None): # Another base-case, a node with no children is always # a valid BST, this comes second since we need to use the above ifs to check that this is a valid - # child node of the prior parent node + # child node of the prior parent node first regardless of the decendents of this node. + # If this node is valid and there are no child nodes, then no further recursive calls needed return True # If both of those conditions are met, then this node is valid for the parent node above it diff --git a/ds/segment_tree.py b/ds/segment_tree.py index 29857c3..8b1f475 100644 --- a/ds/segment_tree.py +++ b/ds/segment_tree.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Segment tree data structure. +Segment tree data structure module, see help(SegmentTree) for details. """ import math @@ -32,6 +32,18 @@ def __repr__(self) -> str: class SegmentTree: """ Segment tree data-structure. + + Segment trees allow for range queries to be executed on an array in O(log2(n)) time which is much faster + than O(n) which is required to traverse the array element by element to evaluate. A range query is some + function e.g. sum, max, min etc. applied over a given range of elements of an array e.g. sum(arr[4:8]). + + Segment trees also support making updates to elements in the array in O(log2(n)) time. This gives them + an advantage over prefix sums which require O(n) time to make updates to an arbitrary element in the + array. Segment trees also support the usage of monotonic, non-linear operators such as max and min which + prefix sumes do not. + + Segment trees are well suited to handle a stream of range queries and array updates. They are less well + suited to handle insertions or deletions to the array, which requires a rebuild of the segment tree. """ def __init__(self, arr: List[Union[int, float]], eval_func: str): @@ -46,13 +58,14 @@ def __init__(self, arr: List[Union[int, float]], eval_func: str): The function that will be evaluated over various ranges of arr in the segment tree. Must be one of the following: ["min", "max", "sum", "gcd", "lcm"]. Note, gcd and lcm are only appropriate if all values in arr are non-negative integers. - + """ # Record a dictionary of possible evaluation functions that can be applied over the elements of arr # Each expects 2 numbers as inputs and returns a number as an output after combining self.func_dict = {"min": min, "max": max, "sum": lambda x, y: x + y, "gcd": math.gcd, "lcm": math.lcm} self.eval_func, self.arr, self.seg_tree = None, None, None # Initialize as None self.build_tree(arr, eval_func) # Construct the segmentation tree using the input array and function + self.n = len(arr) # Record the length of the internal array def build_tree(self, arr: List[Union[int, float]], eval_func: str) -> None: """ @@ -60,10 +73,10 @@ def build_tree(self, arr: List[Union[int, float]], eval_func: str) -> None: which will be computed over various segments of arr. This method is called when the SegmentTree object is instantiated and also again after certain other methods that make changes to the internal array (e.g. insert, pop). - + This method can also be used to re-build the internal segment tree for a new input array or choice of evaluation function if desired by the user without creating a new instance of the SegmentTree class. - + When this method is called, a copy of the input array and the evaluation function are saved as properties to this object in self.arr and self.eval_func respectively. @@ -75,7 +88,7 @@ def build_tree(self, arr: List[Union[int, float]], eval_func: str) -> None: The function that will be evaluated over various ranges of arr in the segment tree. Must be one of the following: ["min", "max", "sum", "gcd", "lcm"]. Note, gcd and lcm are only appropriate if all values in arr are non-negative integers. - + """ assert len(arr) > 0 and isinstance(arr, list), "arr must be a non-empty python list" self.arr = arr.copy() # Make a copy of the array to store internally @@ -89,7 +102,7 @@ def build_tree(self, arr: List[Union[int, float]], eval_func: str) -> None: def _build_tree(self, start: int, end: int) -> Optional[SegmentTreeNode]: """ Internal helper method that creates and returns a SegmentTreeNode root node. - + Creates a segment tree for the elements of self.arr and a specified evaluation function (self.eval_func e.g. sum, max etc.). The segment tree is constructed by splitting arr into a left and right half and recursively calling this _build_tree helper method on the segments. At the bottom of @@ -133,7 +146,7 @@ def append(self, val: Union[int, float]) -> None: ---------- val : Union[int, float] The new value to be appended to the end of the internal array. - + """ self.insert(self.n, val) @@ -152,14 +165,14 @@ def insert(self, idx: int, val: Union[int, float]) -> None: The new value to be inserted into the internal array. """ - assert idx >= 0 and idx <= self.n, f"idx must be [0, {self.n}], got {idx}" + assert 0 <= idx <= self.n, f"idx must be [0, {self.n}], got {idx}" self.arr.insert(idx, val) # Mirror the same change to the copy of arr stored internally self.build_tree(self.arr, self.eval_func) # Rebuild the segment tree after the addition has been made def pop(self, idx: int = None) -> Union[int, float]: """ Removes an element from the internal array at a specified index location and updates the segment tree - accordingly. Valid idx values are 0 through (self.n - 1). If idx=None (the default), then the + accordingly. Valid idx values are 0 through (self.n - 1). If idx=None (the default), then the last element of the internal array will be popped and returned. Parameters @@ -175,7 +188,7 @@ def pop(self, idx: int = None) -> Union[int, float]: """ idx = self.n - 1 if idx is None else idx # Default to the last element if unspecified - assert idx >= 0 and idx <= self.n - 1, f"idx must be [0, {self.n - 1}], got {idx}" + assert 0 <= idx <= self.n - 1, f"idx must be [0, {self.n - 1}], got {idx}" ans = self.arr.pop(idx) # Mirror the same change to the copy of arr stored internally self.build_tree(self.arr, self.eval_func) # Rebuild the segment tree after the element was removed return ans # Return the popped value from the internal array @@ -238,9 +251,9 @@ def range_query(self, start: int, end: int, root: Optional[SegmentTreeNode] = No root = self.seg_tree if root is None else root # Default to the tree-root if not specified # Perform data validation on the interval endpoints requested msg = f"start must be an integer in the range [{root.start}, {root.end}], got {start}" - assert start >= root.start and start <= root.end, msg + assert root.start <= start <= root.end, msg msg = f"end must be an integer in the range [{root.start}, {root.end}], got {end}" - assert end >= root.start and end <= root.end, msg + assert root.start <= end <= root.end, msg assert start <= end, f"start must be <= end, got {start} and {end}" if start == root.start and end == root.end: # Check if requested interval matches this node's bounds diff --git a/ds/trie.py b/ds/trie.py index be59253..d03ea3d 100644 --- a/ds/trie.py +++ b/ds/trie.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Trie data structure. +Trie data structure module, see help(Trie) for details. """ from typing import Optional, List @@ -10,7 +10,7 @@ class TrieNode: def __init__(self, n: int, n_prefix: int): self.n = n # Record the number of words ending at this character (could be 0) self.n_prefix = n_prefix # Record the number of words with this prefix (always >= 1 if not root) - self.children = {} + self.children = {} # Maintain a dictionary of linkages to other TrieNodes organized by letter class Trie: @@ -46,7 +46,7 @@ def insert(self, word: str) -> None: node.children[letter].n_prefix += 1 # Increment the prefix counter node = node.children[letter] # Update for next iter, continue until we've covered all chr in word - def get_word_count(self, word: int) -> int: + def get_word_count(self, word: str) -> int: """ Returns the number of instances of the input word that have been added to the Trie. The input word must be a non-empty string. @@ -113,12 +113,12 @@ def remove_word(self, word: str, remove_all: bool = False) -> None: node = next_node # Update ref for next iteration node.n -= n_remove # Decrement the terminal node word count by the number to remove - def first_prefix_word(self, word: str) -> str: + def first_prefix_word(self, word: str) -> Optional[str]: """ Returns the first word found in the Trie along the node path leading to the input word provided. Note, the input word itself does not necessarily need to be a word in the Trie. The input word must be a non-empty string. - + E.g. let word="apple". If "app" is a word in the Trie while "a" and "ap" are not, then "app" will be returned. If none of the proper prefixes of "apple" are in the Trie, but "apple" is, then "apple" will be returned. If "apple" and none of its prefixes are in the Trie, then None will be returned. diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index d335b05..ece1cc3 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -218,6 +218,8 @@ def test_SegmentTree(): """ test_data = [1, 2, 3, 5, 8, -10, 12] obj = SegmentTree(test_data, "sum") + assert obj._build_tree(4, 3) is None, "Empty segment returns no root test failed" + assert str(obj.seg_tree) == "[0, 6] 21", "Failed str representation test" assert len(obj) == len(test_data), "Failed len(obj) test" assert isinstance(str(obj), str), "Failed string representation test" @@ -259,6 +261,7 @@ def test_BinarySearchTree(): obj = BinarySearchTree() for x in list(range(30, 40)) + list(range(25)): obj.insert(x) + assert obj.isValidBST() is True assert len(obj) == 35, "Search for __len__ value check failed" @@ -315,6 +318,45 @@ def test_BinarySearchTree(): obj.print_tree() # Should run without crashing + + obj = BinarySearchTree() + assert obj.preOrderTraversal() == [], "Failed empty traversal test" + assert obj.inOrderTraversal() == [], "Failed empty traversal test" + assert obj.postOrderTraversal() == [], "Failed empty traversal test" + assert obj.levelOrderTraversal() == [], "Failed empty traversal test" + assert obj.isValidBST() is True, "Failed isValidBST empty tree test" + obj.delete(5) # Delete from a blank tree + assert obj.n == 0 + + obj.insert(5) # Add 1 element, then delete it + obj.delete(5) # Delete the root node + assert obj.n == 0 # Check that this ran and the size is recorded as 0 + + obj.insert(5) # Add the same element multiple times and make sure the valid BST check still passes + obj.insert(5) + obj.insert(4) + obj.insert(4) + obj.insert(0) + assert obj.isValidBST() is True, "Insertion of duplicate elements validation check failed" + obj.root.val = 50 # This will make the BST no longer valid + assert obj.isValidBST() is False, "Validation check failed to catch invalid BST" + obj.root.val = -50 # This will make the BST no longer valid + assert obj.isValidBST() is False, "Validation check failed to catch invalid BST" + + obj = BinarySearchTree() + obj.insert(5) + obj.insert(10) + obj.delete(5) + assert obj.root.val == 10, "Root node deletion test failed" + + obj = BinarySearchTree() + obj.insert(10) + obj.insert(15) + obj.insert(11) + obj.delete(15) + assert str(obj) == "[10, 11]", "Node deletion test failed" + + def test_Deque(): """ Runs basic tests for the Deque data structure, tests methods and functionality. From 8a75d8167b71d3ad608f030fd7af1cbf41937734 Mon Sep 17 00:00:00 2001 From: EH225 Date: Tue, 25 Mar 2025 16:24:51 -0400 Subject: [PATCH 5/7] rename binary index tree to binary indexed tree --- README.txt | 2 ++ all_ds.py | 2 +- ds/.gitignore | 3 +++ ...y_index_tree.py => binary_indexed_tree.py} | 22 +++++++++---------- tests/.gitignore | 3 +++ tests/test_data_structures.py | 9 ++++++-- 6 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 README.txt create mode 100644 ds/.gitignore rename ds/{binary_index_tree.py => binary_indexed_tree.py} (86%) create mode 100644 tests/.gitignore diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..c44c176 --- /dev/null +++ b/README.txt @@ -0,0 +1,2 @@ +# Data Structures +This project contains a variety of data structures implemented in python with a set of tests that achieve a near 100% code coverage. \ No newline at end of file diff --git a/all_ds.py b/all_ds.py index 6b53037..1775f19 100644 --- a/all_ds.py +++ b/all_ds.py @@ -7,7 +7,7 @@ from ds.heaps import MinHeap, MaxHeap from ds.deque import Deque from ds.binary_search_tree import BinarySearchTree -from ds.binary_index_tree import BinaryIndexTree +from ds.binary_indexed_tree import BinaryIndexedTree from ds.segment_tree import SegmentTree from ds.disjoint_sets import DisjointSets from ds.trie import Trie diff --git a/ds/.gitignore b/ds/.gitignore new file mode 100644 index 0000000..8799a44 --- /dev/null +++ b/ds/.gitignore @@ -0,0 +1,3 @@ +ds/__pycache__/* +*__pycache__* +*.pyc \ No newline at end of file diff --git a/ds/binary_index_tree.py b/ds/binary_indexed_tree.py similarity index 86% rename from ds/binary_index_tree.py rename to ds/binary_indexed_tree.py index 2475508..a1faefd 100644 --- a/ds/binary_index_tree.py +++ b/ds/binary_indexed_tree.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- """ -Binary index tree data structure module, see help(BinaryIndexTree) for details. +Binary indexed tree data structure module, see help(BinaryIndexedTree) for details. """ from typing import Union, List -class BinaryIndexTree: +class BinaryIndexedTree: """ - Binary Index Tree data-structure. + Binary Indexed Tree data-structure. - Also known as a Fenwick Tree, Binary Index Trees allow for evaluation of the sum function over a given + Also known as a Fenwick Tree, Binary Indexed Trees allow for evaluation of the sum function over a given range in an array in O(log2(n)) time. They also support updates to the values of the array in O(log2(n)) time which is substantial improvement over O(n) update time required for updates in a prefix-sum. The binary representation of the array element's index is used for various purposes in constructing the - tree, making query evaluations, and updating values, hence the name Binary Index Tree. + tree, making query evaluations, and updating values, hence the name Binary Indexed Tree. See: https://www.youtube.com/watch?v=uSFzHCZ4E-8&t=12s for details. """ @@ -27,10 +27,10 @@ def __init__(self, arr: List[Union[int, float]]): Parameters ---------- arr : List[Union[int, float]] - An input array of values for which the binary index tree is built. + An input array of values for which the binary indexed tree is built. """ - # Construct the binary index tree + # Construct the binary indexed tree self.arr = arr.copy() # Store a copy of the original array internally self.binary_idx_tree = arr.copy() # Start off with a copy of the original input array for idx in range(1, len(self.binary_idx_tree) + 1): # Use 1-indexing throughout @@ -41,7 +41,7 @@ def __init__(self, arr: List[Union[int, float]]): def update(self, idx: int, val: Union[int, float]) -> None: """ Updates the value of an element in the original array located at a particular index (idx) and also the - binary index tree accordingly. An update can also be accomplished with: binary_idx_tree[idx] = val. + binary indexed tree accordingly. An update can also be accomplished with: binary_idx_tree[idx] = val. Performs updates in O(log2(n)) time. Parameters @@ -87,8 +87,8 @@ def _range_query(self, end: int) -> Union[float, int]: def range_query(self, start: int, end: int) -> Union[float, int]: """ - Performs a sum range query using the binary index tree and returns the aggregate answer. Computes the - sum of the array elements falling within the inclusive index interval [start, end] of the original + Performs a sum range query using the binary indexed tree and returns the aggregate answer. Computes + the sum of the array elements falling within the inclusive index interval [start, end] of the original array. Runs in O(log2(n)) time. Parameters @@ -134,6 +134,6 @@ def __str__(self) -> str: def __len__(self): """ - Returns the length of binary index tree. + Returns the length of binary indexed tree. """ return len(self.binary_idx_tree) diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..8799a44 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,3 @@ +ds/__pycache__/* +*__pycache__* +*.pyc \ No newline at end of file diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index ece1cc3..b5b18f2 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -1,4 +1,9 @@ -from all_ds import BinarySearchTree, BinaryIndexTree, Deque, DisjointSets, MinHeap, MaxHeap, LinkedList +# -*- coding: utf-8 -*- +""" +Tests for all data structures contains in this repo. +""" + +from all_ds import BinarySearchTree, BinaryIndexedTree, Deque, DisjointSets, MinHeap, MaxHeap, LinkedList from all_ds import DoublyLinkedList, SegmentTree, Trie, LRUCache, LFUCache import pytest @@ -188,7 +193,7 @@ def test_BinaryIndexTree(): Runs basic tests for the BinaryIndexTree data structure, tests methods and functionality. """ test_data = [1, 2, 3, 5, 8, -10, 12] - obj = BinaryIndexTree(test_data) + obj = BinaryIndexedTree(test_data) assert len(obj) == len(test_data), "Failed len(obj) test" assert isinstance(str(obj), str), "Failed string representation test" From f744564192c53498986803c9cab2c91e204d64be Mon Sep 17 00:00:00 2001 From: EH225 Date: Tue, 25 Mar 2025 16:27:25 -0400 Subject: [PATCH 6/7] update workflow to include dev pushes in automated testing --- .github/workflows/ci_tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index de5e7f9..ce4587d 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -5,9 +5,9 @@ name: Continuous Integration Tests on: push: - branches: [ "main" ] + branches: [ "main", "dev" ] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] permissions: contents: read From d3055e7d58c14e302e926296067f41534671fb6b Mon Sep 17 00:00:00 2001 From: EH225 Date: Tue, 25 Mar 2025 16:28:32 -0400 Subject: [PATCH 7/7] rename README extension to md --- README.txt => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.txt => README.md (100%) diff --git a/README.txt b/README.md similarity index 100% rename from README.txt rename to README.md