diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index 55d89f455a25..3edb5c91a951 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -3,7 +3,7 @@ name: directory_writer on: [push] jobs: - build: + directory_writer: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 288e3f591403..6c1879ab1ac6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.9 + rev: v0.11.11 hooks: - id: ruff - id: ruff-format @@ -29,7 +29,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.5.1" + rev: "v2.6.0" hooks: - id: pyproject-fmt diff --git a/DIRECTORY.md b/DIRECTORY.md index 4129f9c1a5e2..00f4bb4ef2b2 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -480,6 +480,7 @@ * [Bi Directional Dijkstra](graphs/bi_directional_dijkstra.py) * [Bidirectional A Star](graphs/bidirectional_a_star.py) * [Bidirectional Breadth First Search](graphs/bidirectional_breadth_first_search.py) + * [Bidirectional Search](graphs/bidirectional_search.py) * [Boruvka](graphs/boruvka.py) * [Breadth First Search](graphs/breadth_first_search.py) * [Breadth First Search 2](graphs/breadth_first_search_2.py) diff --git a/backtracking/sum_of_subsets.py b/backtracking/sum_of_subsets.py index f34d3ca34339..f26f179f8725 100644 --- a/backtracking/sum_of_subsets.py +++ b/backtracking/sum_of_subsets.py @@ -1,5 +1,5 @@ """ -The sum-of-subsetsproblem states that a set of non-negative integers, and a +The sum-of-subsets problem states that a set of non-negative integers, and a value M, determine all possible subsets of the given set whose summation sum equal to given M. @@ -7,10 +7,20 @@ can be used only once. """ -from __future__ import annotations +def generate_sum_of_subsets_solutions(nums: list[int], max_sum: int) -> list[list[int]]: + """ + The main function. For list of numbers 'nums' find the subsets with sum + equal to 'max_sum' + + >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=9) + [[3, 4, 2], [4, 5]] + >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=3) + [[3]] + >>> generate_sum_of_subsets_solutions(nums=[3, 34, 4, 12, 5, 2], max_sum=1) + [] + """ -def generate_sum_of_subsets_soln(nums: list[int], max_sum: int) -> list[list[int]]: result: list[list[int]] = [] path: list[int] = [] num_index = 0 @@ -34,7 +44,21 @@ def create_state_space_tree( This algorithm follows depth-fist-search and backtracks when the node is not branchable. + >>> path = [] + >>> result = [] + >>> create_state_space_tree( + ... nums=[1], + ... max_sum=1, + ... num_index=0, + ... path=path, + ... result=result, + ... remaining_nums_sum=1) + >>> path + [] + >>> result + [[1]] """ + if sum(path) > max_sum or (remaining_nums_sum + sum(path)) < max_sum: return if sum(path) == max_sum: @@ -51,16 +75,7 @@ def create_state_space_tree( ) -""" -remove the comment to take an input from the user - -print("Enter the elements") -nums = list(map(int, input().split())) -print("Enter max_sum sum") -max_sum = int(input()) +if __name__ == "__main__": + import doctest -""" -nums = [3, 34, 4, 12, 5, 2] -max_sum = 9 -result = generate_sum_of_subsets_soln(nums, max_sum) -print(*result) + doctest.testmod() diff --git a/data_structures/hashing/hash_map.py b/data_structures/hashing/hash_map.py index 9213d6930f67..8c56c327a492 100644 --- a/data_structures/hashing/hash_map.py +++ b/data_structures/hashing/hash_map.py @@ -16,7 +16,7 @@ VAL = TypeVar("VAL") -@dataclass(frozen=True, slots=True) +@dataclass(slots=True) class _Item(Generic[KEY, VAL]): key: KEY val: VAL @@ -72,16 +72,17 @@ def _try_set(self, ind: int, key: KEY, val: VAL) -> bool: If bucket is empty or key is the same, does insert and return True. - If bucket has another key or deleted placeholder, - that means that we need to check next bucket. + If bucket has another key that means that we need to check next bucket. """ stored = self._buckets[ind] if not stored: + # A falsy item means that bucket was never used (None) + # or was deleted (_deleted). self._buckets[ind] = _Item(key, val) self._len += 1 return True elif stored.key == key: - self._buckets[ind] = _Item(key, val) + stored.val = val return True else: return False @@ -228,6 +229,27 @@ def __delitem__(self, key: KEY) -> None: Traceback (most recent call last): ... KeyError: 4 + + # Test resize down when sparse + ## Setup: resize up + >>> hm = HashMap(initial_block_size=100, capacity_factor=0.75) + >>> len(hm._buckets) + 100 + >>> for i in range(75): + ... hm[i] = i + >>> len(hm._buckets) + 100 + >>> hm[75] = 75 + >>> len(hm._buckets) + 200 + + ## Resize down + >>> del hm[75] + >>> len(hm._buckets) + 200 + >>> del hm[74] + >>> len(hm._buckets) + 100 """ for ind in self._iterate_buckets(key): item = self._buckets[ind] diff --git a/dynamic_programming/matrix_chain_order.py b/dynamic_programming/matrix_chain_order.py index d612aea7b99d..6df43e84be28 100644 --- a/dynamic_programming/matrix_chain_order.py +++ b/dynamic_programming/matrix_chain_order.py @@ -5,13 +5,19 @@ Implementation of Matrix Chain Multiplication Time Complexity: O(n^3) Space Complexity: O(n^2) + +Reference: https://en.wikipedia.org/wiki/Matrix_chain_multiplication """ -def matrix_chain_order(array): +def matrix_chain_order(array: list[int]) -> tuple[list[list[int]], list[list[int]]]: + """ + >>> matrix_chain_order([10, 30, 5]) + ([[0, 0, 0], [0, 0, 1500], [0, 0, 0]], [[0, 0, 0], [0, 0, 1], [0, 0, 0]]) + """ n = len(array) - matrix = [[0 for x in range(n)] for x in range(n)] - sol = [[0 for x in range(n)] for x in range(n)] + matrix = [[0 for _ in range(n)] for _ in range(n)] + sol = [[0 for _ in range(n)] for _ in range(n)] for chain_length in range(2, n): for a in range(1, n - chain_length + 1): @@ -28,26 +34,33 @@ def matrix_chain_order(array): return matrix, sol -# Print order of matrix with Ai as Matrix -def print_optiomal_solution(optimal_solution, i, j): +def print_optimal_solution(optimal_solution: list[list[int]], i: int, j: int): + """ + Print order of matrix with Ai as Matrix. + """ + if i == j: print("A" + str(i), end=" ") else: print("(", end=" ") - print_optiomal_solution(optimal_solution, i, optimal_solution[i][j]) - print_optiomal_solution(optimal_solution, optimal_solution[i][j] + 1, j) + print_optimal_solution(optimal_solution, i, optimal_solution[i][j]) + print_optimal_solution(optimal_solution, optimal_solution[i][j] + 1, j) print(")", end=" ") def main(): + """ + Size of matrix created from array [30, 35, 15, 5, 10, 20, 25] will be: + 30*35 35*15 15*5 5*10 10*20 20*25 + """ + array = [30, 35, 15, 5, 10, 20, 25] n = len(array) - # Size of matrix created from above array will be - # 30*35 35*15 15*5 5*10 10*20 20*25 + matrix, optimal_solution = matrix_chain_order(array) print("No. of Operation required: " + str(matrix[1][n - 1])) - print_optiomal_solution(optimal_solution, 1, n - 1) + print_optimal_solution(optimal_solution, 1, n - 1) if __name__ == "__main__": diff --git a/financial/straight_line_depreciation.py b/financial/straight_line_depreciation.py new file mode 100644 index 000000000000..e11e1c1364ce --- /dev/null +++ b/financial/straight_line_depreciation.py @@ -0,0 +1,103 @@ +""" +In accounting, depreciation refers to the decreases in the value +of a fixed asset during the asset's useful life. +When an organization purchases a fixed asset, +the purchase expenditure is not recognized as an expense immediately. +Instead, the decreases in the asset's value are recognized as expenses +over the years during which the asset is used. + +The following methods are widely used +for depreciation calculation in accounting: +- Straight-line method +- Diminishing balance method +- Units-of-production method + +The straight-line method is the simplest and most widely used. +This method calculates depreciation by spreading the cost evenly +over the asset's useful life. + +The following formula shows how to calculate the yearly depreciation expense: + +- annual depreciation expense = + (purchase cost of asset - residual value) / useful life of asset(years) + +Further information on: +https://en.wikipedia.org/wiki/Depreciation + +The function, straight_line_depreciation, returns a list of +the depreciation expenses over the given period. +""" + + +def straight_line_depreciation( + useful_years: int, + purchase_value: float, + residual_value: float = 0.0, +) -> list[float]: + """ + Calculate the depreciation expenses over the given period + :param useful_years: Number of years the asset will be used + :param purchase_value: Purchase expenditure for the asset + :param residual_value: Residual value of the asset at the end of its useful life + :return: A list of annual depreciation expenses over the asset's useful life + >>> straight_line_depreciation(10, 1100.0, 100.0) + [100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0] + >>> straight_line_depreciation(6, 1250.0, 50.0) + [200.0, 200.0, 200.0, 200.0, 200.0, 200.0] + >>> straight_line_depreciation(4, 1001.0) + [250.25, 250.25, 250.25, 250.25] + >>> straight_line_depreciation(11, 380.0, 50.0) + [30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0, 30.0] + >>> straight_line_depreciation(1, 4985, 100) + [4885.0] + """ + + if not isinstance(useful_years, int): + raise TypeError("Useful years must be an integer") + + if useful_years < 1: + raise ValueError("Useful years cannot be less than 1") + + if not isinstance(purchase_value, (float, int)): + raise TypeError("Purchase value must be numeric") + + if not isinstance(residual_value, (float, int)): + raise TypeError("Residual value must be numeric") + + if purchase_value < 0.0: + raise ValueError("Purchase value cannot be less than zero") + + if purchase_value < residual_value: + raise ValueError("Purchase value cannot be less than residual value") + + # Calculate annual depreciation expense + depreciable_cost = purchase_value - residual_value + annual_depreciation_expense = depreciable_cost / useful_years + + # List of annual depreciation expenses + list_of_depreciation_expenses = [] + accumulated_depreciation_expense = 0.0 + for period in range(useful_years): + if period != useful_years - 1: + accumulated_depreciation_expense += annual_depreciation_expense + list_of_depreciation_expenses.append(annual_depreciation_expense) + else: + depreciation_expense_in_end_year = ( + depreciable_cost - accumulated_depreciation_expense + ) + list_of_depreciation_expenses.append(depreciation_expense_in_end_year) + + return list_of_depreciation_expenses + + +if __name__ == "__main__": + user_input_useful_years = int(input("Please Enter Useful Years:\n > ")) + user_input_purchase_value = float(input("Please Enter Purchase Value:\n > ")) + user_input_residual_value = float(input("Please Enter Residual Value:\n > ")) + print( + straight_line_depreciation( + user_input_useful_years, + user_input_purchase_value, + user_input_residual_value, + ) + ) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py new file mode 100644 index 000000000000..b3ff9f75fd44 --- /dev/null +++ b/graphs/bidirectional_search.py @@ -0,0 +1,201 @@ +""" +Bidirectional Search Algorithm. + +This algorithm searches from both the source and target nodes simultaneously, +meeting somewhere in the middle. This approach can significantly reduce the +search space compared to a traditional one-directional search. + +Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth +Space Complexity: O(b^(d/2)) + +https://en.wikipedia.org/wiki/Bidirectional_search +""" + +from collections import deque + + +def expand_search( + graph: dict[int, list[int]], + queue: deque[int], + parents: dict[int, int | None], + opposite_direction_parents: dict[int, int | None], +) -> int | None: + if not queue: + return None + + current = queue.popleft() + for neighbor in graph[current]: + if neighbor in parents: + continue + + parents[neighbor] = current + queue.append(neighbor) + + # Check if this creates an intersection + if neighbor in opposite_direction_parents: + return neighbor + + return None + + +def construct_path(current: int | None, parents: dict[int, int | None]) -> list[int]: + path: list[int] = [] + while current is not None: + path.append(current) + current = parents[current] + return path + + +def bidirectional_search( + graph: dict[int, list[int]], start: int, goal: int +) -> list[int] | None: + """ + Perform bidirectional search on a graph to find the shortest path. + + Args: + graph: A dictionary where keys are nodes and values are lists of adjacent nodes + start: The starting node + goal: The target node + + Returns: + A list representing the path from start to goal, or None if no path exists + + Examples: + >>> graph = { + ... 0: [1, 2], + ... 1: [0, 3, 4], + ... 2: [0, 5, 6], + ... 3: [1, 7], + ... 4: [1, 8], + ... 5: [2, 9], + ... 6: [2, 10], + ... 7: [3, 11], + ... 8: [4, 11], + ... 9: [5, 11], + ... 10: [6, 11], + ... 11: [7, 8, 9, 10], + ... } + >>> bidirectional_search(graph=graph, start=0, goal=11) + [0, 1, 3, 7, 11] + >>> bidirectional_search(graph=graph, start=5, goal=5) + [5] + >>> disconnected_graph = { + ... 0: [1, 2], + ... 1: [0], + ... 2: [0], + ... 3: [4], + ... 4: [3], + ... } + >>> bidirectional_search(graph=disconnected_graph, start=0, goal=3) is None + True + """ + if start == goal: + return [start] + + # Check if start and goal are in the graph + if start not in graph or goal not in graph: + return None + + # Initialize forward and backward search dictionaries + # Each maps a node to its parent in the search + forward_parents: dict[int, int | None] = {start: None} + backward_parents: dict[int, int | None] = {goal: None} + + # Initialize forward and backward search queues + forward_queue = deque([start]) + backward_queue = deque([goal]) + + # Intersection node (where the two searches meet) + intersection = None + + # Continue until both queues are empty or an intersection is found + while forward_queue and backward_queue and intersection is None: + # Expand forward search + intersection = expand_search( + graph=graph, + queue=forward_queue, + parents=forward_parents, + opposite_direction_parents=backward_parents, + ) + + # If no intersection found, expand backward search + if intersection is not None: + break + + intersection = expand_search( + graph=graph, + queue=backward_queue, + parents=backward_parents, + opposite_direction_parents=forward_parents, + ) + + # If no intersection found, there's no path + if intersection is None: + return None + + # Construct path from start to intersection + forward_path: list[int] = construct_path( + current=intersection, parents=forward_parents + ) + forward_path.reverse() + + # Construct path from intersection to goal + backward_path: list[int] = construct_path( + current=backward_parents[intersection], parents=backward_parents + ) + + # Return the complete path + return forward_path + backward_path + + +def main() -> None: + """ + Run example of bidirectional search algorithm. + + Examples: + >>> main() # doctest: +NORMALIZE_WHITESPACE + Path from 0 to 11: [0, 1, 3, 7, 11] + Path from 5 to 5: [5] + Path from 0 to 3: None + """ + # Example graph represented as an adjacency list + example_graph = { + 0: [1, 2], + 1: [0, 3, 4], + 2: [0, 5, 6], + 3: [1, 7], + 4: [1, 8], + 5: [2, 9], + 6: [2, 10], + 7: [3, 11], + 8: [4, 11], + 9: [5, 11], + 10: [6, 11], + 11: [7, 8, 9, 10], + } + + # Test case 1: Path exists + start, goal = 0, 11 + path = bidirectional_search(graph=example_graph, start=start, goal=goal) + print(f"Path from {start} to {goal}: {path}") + + # Test case 2: Start and goal are the same + start, goal = 5, 5 + path = bidirectional_search(graph=example_graph, start=start, goal=goal) + print(f"Path from {start} to {goal}: {path}") + + # Test case 3: No path exists (disconnected graph) + disconnected_graph = { + 0: [1, 2], + 1: [0], + 2: [0], + 3: [4], + 4: [3], + } + start, goal = 0, 3 + path = bidirectional_search(graph=disconnected_graph, start=start, goal=goal) + print(f"Path from {start} to {goal}: {path}") + + +if __name__ == "__main__": + main() diff --git a/maths/radix2_fft.py b/maths/radix2_fft.py index d41dc82d5588..ccd5cdcc0160 100644 --- a/maths/radix2_fft.py +++ b/maths/radix2_fft.py @@ -40,13 +40,13 @@ class FFT: Print product >>> x.product # 2x + 3x^2 + 8x^3 + 4x^4 + 6x^5 - [(-0+0j), (2+0j), (3+0j), (8+0j), (6+0j), (8+0j)] + [(-0-0j), (2+0j), (3-0j), (8-0j), (6+0j), (8+0j)] __str__ test >>> print(x) A = 0*x^0 + 1*x^1 + 2*x^0 + 3*x^2 B = 0*x^2 + 1*x^3 + 2*x^4 - A*B = 0*x^(-0+0j) + 1*x^(2+0j) + 2*x^(3+0j) + 3*x^(8+0j) + 4*x^(6+0j) + 5*x^(8+0j) + A*B = 0*x^(-0-0j) + 1*x^(2+0j) + 2*x^(3-0j) + 3*x^(8-0j) + 4*x^(6+0j) + 5*x^(8+0j) """ def __init__(self, poly_a=None, poly_b=None): @@ -147,7 +147,9 @@ def __multiply(self): inverce_c = new_inverse_c next_ncol *= 2 # Unpack - inverce_c = [round(x[0].real, 8) + round(x[0].imag, 8) * 1j for x in inverce_c] + inverce_c = [ + complex(round(x[0].real, 8), round(x[0].imag, 8)) for x in inverce_c + ] # Remove leading 0's while inverce_c[-1] == 0: diff --git a/searches/quick_select.py b/searches/quick_select.py index c8282e1fa5fc..f67f939c88c3 100644 --- a/searches/quick_select.py +++ b/searches/quick_select.py @@ -60,3 +60,25 @@ def quick_select(items: list, index: int): # must be in larger else: return quick_select(larger, index - (m + count)) + + +def median(items: list): + """ + One common application of Quickselect is finding the median, which is + the middle element (or average of the two middle elements) in a sorted dataset. + It works efficiently on unsorted lists by partially sorting the data without + fully sorting the entire list. + + >>> median([3, 2, 2, 9, 9]) + 3 + + >>> median([2, 2, 9, 9, 9, 3]) + 6.0 + """ + mid, rem = divmod(len(items), 2) + if rem != 0: + return quick_select(items=items, index=mid) + else: + low_mid = quick_select(items=items, index=mid - 1) + high_mid = quick_select(items=items, index=mid) + return (low_mid + high_mid) / 2 diff --git a/strings/boyer_moore_search.py b/strings/boyer_moore_search.py index 9615d2fd659b..ad14a504f792 100644 --- a/strings/boyer_moore_search.py +++ b/strings/boyer_moore_search.py @@ -11,23 +11,31 @@ a shift is proposed that moves the entirety of Pattern past the point of mismatch in the text. -If there no mismatch then the pattern matches with text block. +If there is no mismatch then the pattern matches with text block. Time Complexity : O(n/m) n=length of main string m=length of pattern string """ -from __future__ import annotations - class BoyerMooreSearch: + """ + Example usage: + + bms = BoyerMooreSearch(text="ABAABA", pattern="AB") + positions = bms.bad_character_heuristic() + + where 'positions' contain the locations where the pattern was matched. + """ + def __init__(self, text: str, pattern: str): self.text, self.pattern = text, pattern self.textLen, self.patLen = len(text), len(pattern) def match_in_pattern(self, char: str) -> int: - """finds the index of char in pattern in reverse order + """ + Finds the index of char in pattern in reverse order. Parameters : char (chr): character to be searched @@ -35,6 +43,10 @@ def match_in_pattern(self, char: str) -> int: Returns : i (int): index of char from last in pattern -1 (int): if char is not found in pattern + + >>> bms = BoyerMooreSearch(text="ABAABA", pattern="AB") + >>> bms.match_in_pattern("B") + 1 """ for i in range(self.patLen - 1, -1, -1): @@ -44,8 +56,8 @@ def match_in_pattern(self, char: str) -> int: def mismatch_in_text(self, current_pos: int) -> int: """ - find the index of mis-matched character in text when compared with pattern - from last + Find the index of mis-matched character in text when compared with pattern + from last. Parameters : current_pos (int): current index position of text @@ -53,6 +65,10 @@ def mismatch_in_text(self, current_pos: int) -> int: Returns : i (int): index of mismatched char from last in text -1 (int): if there is no mismatch between pattern and text block + + >>> bms = BoyerMooreSearch(text="ABAABA", pattern="AB") + >>> bms.mismatch_in_text(2) + 3 """ for i in range(self.patLen - 1, -1, -1): @@ -61,7 +77,14 @@ def mismatch_in_text(self, current_pos: int) -> int: return -1 def bad_character_heuristic(self) -> list[int]: - # searches pattern in text and returns index positions + """ + Finds the positions of the pattern location. + + >>> bms = BoyerMooreSearch(text="ABAABA", pattern="AB") + >>> bms.bad_character_heuristic() + [0, 3] + """ + positions = [] for i in range(self.textLen - self.patLen + 1): mismatch_index = self.mismatch_in_text(i) @@ -75,13 +98,7 @@ def bad_character_heuristic(self) -> list[int]: return positions -text = "ABAABA" -pattern = "AB" -bms = BoyerMooreSearch(text, pattern) -positions = bms.bad_character_heuristic() +if __name__ == "__main__": + import doctest -if len(positions) == 0: - print("No match found") -else: - print("Pattern found in following positions: ") - print(positions) + doctest.testmod()