From 0876a87186516f93c8d53ee69a600630905984a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:33:56 +0200 Subject: [PATCH 01/76] [pre-commit.ci] pre-commit autoupdate (#12944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.10 → v0.12.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.10...v0.12.11) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6eb0906fb23a..f60913a743ad 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.12.10 + rev: v0.12.11 hooks: - id: ruff-check - id: ruff-format From 544f48ff34492eba4cf452fca3eba7bdff1cda5c Mon Sep 17 00:00:00 2001 From: hema_ameh <152301559+PYDIMARRI-HEMA-HARSHINI-23-586@users.noreply.github.com> Date: Thu, 4 Sep 2025 06:19:59 +0530 Subject: [PATCH 02/76] Fix is_palindrome_recursive logic in strings/palindrome.py (#12946) * Fix: is_palindrome_recursive logic for 2-char strings * Update palindrome.py * Update palindrome.py * Update palindrome.py --------- Co-authored-by: Maxim Smolskiy --- strings/palindrome.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/strings/palindrome.py b/strings/palindrome.py index bfdb3ddcf396..e765207e5942 100644 --- a/strings/palindrome.py +++ b/strings/palindrome.py @@ -11,6 +11,8 @@ "BB": True, "ABC": False, "amanaplanacanalpanama": True, # "a man a plan a canal panama" + "abcdba": False, + "AB": False, } # Ensure our test data is valid assert all((key == key[::-1]) is value for key, value in test_data.items()) @@ -61,7 +63,7 @@ def is_palindrome_recursive(s: str) -> bool: >>> all(is_palindrome_recursive(key) is value for key, value in test_data.items()) True """ - if len(s) <= 2: + if len(s) <= 1: return True if s[0] == s[len(s) - 1]: return is_palindrome_recursive(s[1:-1]) From 4ce1185f9e92ecea6805c8ee77404a56d8f70ea9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:23:36 +0200 Subject: [PATCH 03/76] Bump actions/setup-python from 5 to 6 (#12952) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/directory_writer.yml | 2 +- .github/workflows/project_euler.yml | 4 ++-- .github/workflows/sphinx.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69192db0c4c6..731e3fad3b85 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: with: enable-cache: true cache-dependency-glob: uv.lock - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x allow-prereleases: true diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index f5167f8d1a58..9a4682677c00 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Write DIRECTORY.md diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index 8b8cb2a1e68f..f52ff280b29a 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - run: uv sync --group=euler-validate --group=test @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - run: uv sync --group=euler-validate --group=test diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index e28fa04f3ab4..bd253dc3de65 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -27,7 +27,7 @@ jobs: steps: - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.13 allow-prereleases: true From 8f1a6b0ca0cbe563ea3f48c43695a07959b923a1 Mon Sep 17 00:00:00 2001 From: Juan Dupierris Date: Mon, 8 Sep 2025 01:41:07 +0200 Subject: [PATCH 04/76] Adding the function is_proth_number (#12399) * Adding the function isProthNumber(n : int) which returns true if n is a Proth number * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixing the issues of the isprothnumber function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * New fixes on isprothnumber() * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixes on isprothnumber() * Fixes on isprothnumber * Fixes on isprothnumber() * Fixes on isprothnumber * Update proth_number.py * Update proth_number.py * Update proth_number.py * Update proth_number.py * Update proth_number.py * Update proth_number.py * Update proth_number.py * Update proth_number.py --------- Co-authored-by: Juanitoupipou Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- maths/special_numbers/proth_number.py | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/maths/special_numbers/proth_number.py b/maths/special_numbers/proth_number.py index 47747ed260f7..b9b827b6a5a2 100644 --- a/maths/special_numbers/proth_number.py +++ b/maths/special_numbers/proth_number.py @@ -59,6 +59,50 @@ def proth(number: int) -> int: return proth_list[number - 1] +def is_proth_number(number: int) -> bool: + """ + :param number: positive integer number + :return: true if number is a Proth number, false otherwise + >>> is_proth_number(1) + False + >>> is_proth_number(2) + False + >>> is_proth_number(3) + True + >>> is_proth_number(4) + False + >>> is_proth_number(5) + True + >>> is_proth_number(34) + False + >>> is_proth_number(-1) + Traceback (most recent call last): + ... + ValueError: Input value of [number=-1] must be > 0 + >>> is_proth_number(6.0) + Traceback (most recent call last): + ... + TypeError: Input value of [number=6.0] must be an integer + """ + if not isinstance(number, int): + message = f"Input value of [{number=}] must be an integer" + raise TypeError(message) + + if number <= 0: + message = f"Input value of [{number=}] must be > 0" + raise ValueError(message) + + if number == 1: + return False + + number -= 1 + n = 0 + while number % 2 == 0: + n += 1 + number //= 2 + return number < 2**n + + if __name__ == "__main__": import doctest @@ -73,3 +117,9 @@ def proth(number: int) -> int: continue print(f"The {number}th Proth number: {value}") + + for number in [1, 2, 3, 4, 5, 9, 13, 49, 57, 193, 241, 163, 201]: + if is_proth_number(number): + print(f"{number} is a Proth number") + else: + print(f"{number} is not a Proth number") From 18c853d301eb03ca2ba829250c3d415485e49d8b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:46:04 +0200 Subject: [PATCH 05/76] [pre-commit.ci] pre-commit autoupdate (#12961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.12.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.12.12) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f60913a743ad..c30442a2a6f6 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.12.11 + rev: v0.12.12 hooks: - id: ruff-check - id: ruff-format From 63180d7e243a95ce28c2b52abcd7c81c452f623f Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 13 Sep 2025 00:56:14 +0200 Subject: [PATCH 06/76] pre-commit autoupdate 2025-09-11 (#12963) * pre-commit autoupdate 2025-09-11 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- data_structures/arrays/sudoku_solver.py | 2 +- data_structures/trie/radix_tree.py | 4 ++-- graphs/graph_adjacency_list.py | 10 +++++----- graphs/graph_adjacency_matrix.py | 10 +++++----- knapsack/tests/test_greedy_knapsack.py | 12 +++++++----- linear_algebra/gaussian_elimination.py | 2 +- linear_algebra/jacobi_iteration_method.py | 4 ++-- machine_learning/polynomial_regression.py | 2 +- machine_learning/principle_component_analysis.py | 2 +- maths/chinese_remainder_theorem.py | 2 +- maths/modular_division.py | 4 ++-- neural_network/convolution_neural_network.py | 4 ++-- project_euler/problem_551/sol1.py | 2 +- pyproject.toml | 1 - scheduling/multi_level_feedback_queue.py | 2 +- 16 files changed, 33 insertions(+), 32 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c30442a2a6f6..4af51c08d8a4 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.12.12 + rev: v0.13.0 hooks: - id: ruff-check - id: ruff-format diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py index 4c722f12fd6e..07269e2a69cc 100644 --- a/data_structures/arrays/sudoku_solver.py +++ b/data_structures/arrays/sudoku_solver.py @@ -149,7 +149,7 @@ def search(values): if all(len(values[s]) == 1 for s in squares): return values ## Solved! ## Chose the unfilled square s with the fewest possibilities - n, s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) + _n, s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) return some(search(assign(values.copy(), s, d)) for d in values[s]) diff --git a/data_structures/trie/radix_tree.py b/data_structures/trie/radix_tree.py index caf566a6ce30..bd2306befa79 100644 --- a/data_structures/trie/radix_tree.py +++ b/data_structures/trie/radix_tree.py @@ -115,7 +115,7 @@ def find(self, word: str) -> bool: if not incoming_node: return False else: - matching_string, remaining_prefix, remaining_word = incoming_node.match( + _matching_string, remaining_prefix, remaining_word = incoming_node.match( word ) # If there is remaining prefix, the word can't be on the tree @@ -144,7 +144,7 @@ def delete(self, word: str) -> bool: if not incoming_node: return False else: - matching_string, remaining_prefix, remaining_word = incoming_node.match( + _matching_string, remaining_prefix, remaining_word = incoming_node.match( word ) # If there is remaining prefix, the word can't be on the tree diff --git a/graphs/graph_adjacency_list.py b/graphs/graph_adjacency_list.py index 244e59e0e1bf..c901e2cf3dac 100644 --- a/graphs/graph_adjacency_list.py +++ b/graphs/graph_adjacency_list.py @@ -448,7 +448,7 @@ def test_remove_edge(self) -> None: ( undirected_graph, directed_graph, - random_vertices, + _random_vertices, random_edges, ) = self.__generate_graphs(20, 0, 100, 4) @@ -502,7 +502,7 @@ def test_add_vertex_exception_check(self) -> None: undirected_graph, directed_graph, random_vertices, - random_edges, + _random_edges, ) = self.__generate_graphs(20, 0, 100, 4) for vertex in random_vertices: @@ -516,7 +516,7 @@ def test_remove_vertex_exception_check(self) -> None: undirected_graph, directed_graph, random_vertices, - random_edges, + _random_edges, ) = self.__generate_graphs(20, 0, 100, 4) for i in range(101): @@ -530,7 +530,7 @@ def test_add_edge_exception_check(self) -> None: ( undirected_graph, directed_graph, - random_vertices, + _random_vertices, random_edges, ) = self.__generate_graphs(20, 0, 100, 4) @@ -569,7 +569,7 @@ def test_contains_edge_exception_check(self) -> None: undirected_graph, directed_graph, random_vertices, - random_edges, + _random_edges, ) = self.__generate_graphs(20, 0, 100, 4) for vertex in random_vertices: diff --git a/graphs/graph_adjacency_matrix.py b/graphs/graph_adjacency_matrix.py index 8eeeae786513..6dca0fbbcf05 100644 --- a/graphs/graph_adjacency_matrix.py +++ b/graphs/graph_adjacency_matrix.py @@ -469,7 +469,7 @@ def test_remove_edge(self) -> None: ( undirected_graph, directed_graph, - random_vertices, + _random_vertices, random_edges, ) = self.__generate_graphs(20, 0, 100, 4) @@ -523,7 +523,7 @@ def test_add_vertex_exception_check(self) -> None: undirected_graph, directed_graph, random_vertices, - random_edges, + _random_edges, ) = self.__generate_graphs(20, 0, 100, 4) for vertex in random_vertices: @@ -537,7 +537,7 @@ def test_remove_vertex_exception_check(self) -> None: undirected_graph, directed_graph, random_vertices, - random_edges, + _random_edges, ) = self.__generate_graphs(20, 0, 100, 4) for i in range(101): @@ -551,7 +551,7 @@ def test_add_edge_exception_check(self) -> None: ( undirected_graph, directed_graph, - random_vertices, + _random_vertices, random_edges, ) = self.__generate_graphs(20, 0, 100, 4) @@ -590,7 +590,7 @@ def test_contains_edge_exception_check(self) -> None: undirected_graph, directed_graph, random_vertices, - random_edges, + _random_edges, ) = self.__generate_graphs(20, 0, 100, 4) for vertex in random_vertices: diff --git a/knapsack/tests/test_greedy_knapsack.py b/knapsack/tests/test_greedy_knapsack.py index e6a40084109e..7ebaddd3c99e 100644 --- a/knapsack/tests/test_greedy_knapsack.py +++ b/knapsack/tests/test_greedy_knapsack.py @@ -28,7 +28,7 @@ def test_negative_max_weight(self): # profit = [10, 20, 30, 40, 50, 60] # weight = [2, 4, 6, 8, 10, 12] # max_weight = -15 - pytest.raises(ValueError, match="max_weight must greater than zero.") + pytest.raises(ValueError, match=r"max_weight must greater than zero.") def test_negative_profit_value(self): """ @@ -38,7 +38,7 @@ def test_negative_profit_value(self): # profit = [10, -20, 30, 40, 50, 60] # weight = [2, 4, 6, 8, 10, 12] # max_weight = 15 - pytest.raises(ValueError, match="Weight can not be negative.") + pytest.raises(ValueError, match=r"Weight can not be negative.") def test_negative_weight_value(self): """ @@ -48,7 +48,7 @@ def test_negative_weight_value(self): # profit = [10, 20, 30, 40, 50, 60] # weight = [2, -4, 6, -8, 10, 12] # max_weight = 15 - pytest.raises(ValueError, match="Profit can not be negative.") + pytest.raises(ValueError, match=r"Profit can not be negative.") def test_null_max_weight(self): """ @@ -58,7 +58,7 @@ def test_null_max_weight(self): # profit = [10, 20, 30, 40, 50, 60] # weight = [2, 4, 6, 8, 10, 12] # max_weight = null - pytest.raises(ValueError, match="max_weight must greater than zero.") + pytest.raises(ValueError, match=r"max_weight must greater than zero.") def test_unequal_list_length(self): """ @@ -68,7 +68,9 @@ def test_unequal_list_length(self): # profit = [10, 20, 30, 40, 50] # weight = [2, 4, 6, 8, 10, 12] # max_weight = 100 - pytest.raises(IndexError, match="The length of profit and weight must be same.") + pytest.raises( + IndexError, match=r"The length of profit and weight must be same." + ) if __name__ == "__main__": diff --git a/linear_algebra/gaussian_elimination.py b/linear_algebra/gaussian_elimination.py index 6f4075b710fd..cf816940b0d1 100644 --- a/linear_algebra/gaussian_elimination.py +++ b/linear_algebra/gaussian_elimination.py @@ -33,7 +33,7 @@ def retroactive_resolution( [ 0.5]]) """ - rows, columns = np.shape(coefficients) + rows, _columns = np.shape(coefficients) x: NDArray[float64] = np.zeros((rows, 1), dtype=float) for row in reversed(range(rows)): diff --git a/linear_algebra/jacobi_iteration_method.py b/linear_algebra/jacobi_iteration_method.py index 2cc9c103018b..0f9fcde7af6c 100644 --- a/linear_algebra/jacobi_iteration_method.py +++ b/linear_algebra/jacobi_iteration_method.py @@ -112,7 +112,7 @@ def jacobi_iteration_method( (coefficient_matrix, constant_matrix), axis=1 ) - rows, cols = table.shape + rows, _cols = table.shape strictly_diagonally_dominant(table) @@ -149,7 +149,7 @@ def jacobi_iteration_method( # Here we get 'i_col' - these are the column numbers, for each row # without diagonal elements, except for the last column. - i_row, i_col = np.where(masks) + _i_row, i_col = np.where(masks) ind = i_col.reshape(-1, rows - 1) #'i_col' is converted to a two-dimensional list 'ind', which will be diff --git a/machine_learning/polynomial_regression.py b/machine_learning/polynomial_regression.py index 212f40bea197..f52177df1292 100644 --- a/machine_learning/polynomial_regression.py +++ b/machine_learning/polynomial_regression.py @@ -93,7 +93,7 @@ def _design_matrix(data: np.ndarray, degree: int) -> np.ndarray: ... ValueError: Data must have dimensions N x 1 """ - rows, *remaining = data.shape + _rows, *remaining = data.shape if remaining: raise ValueError("Data must have dimensions N x 1") diff --git a/machine_learning/principle_component_analysis.py b/machine_learning/principle_component_analysis.py index 46ccdb968494..174500d89620 100644 --- a/machine_learning/principle_component_analysis.py +++ b/machine_learning/principle_component_analysis.py @@ -65,7 +65,7 @@ def main() -> None: """ Driver function to execute PCA and display results. """ - data_x, data_y = collect_dataset() + data_x, _data_y = collect_dataset() # Number of principal components to retain n_components = 2 diff --git a/maths/chinese_remainder_theorem.py b/maths/chinese_remainder_theorem.py index 18af63d106e8..b7a7712ae917 100644 --- a/maths/chinese_remainder_theorem.py +++ b/maths/chinese_remainder_theorem.py @@ -65,7 +65,7 @@ def invert_modulo(a: int, n: int) -> int: 1 """ - (b, x) = extended_euclid(a, n) + (b, _x) = extended_euclid(a, n) if b < 0: b = (b % n + n) % n return b diff --git a/maths/modular_division.py b/maths/modular_division.py index 2f8f4479b27d..94f12b3e096e 100644 --- a/maths/modular_division.py +++ b/maths/modular_division.py @@ -31,7 +31,7 @@ def modular_division(a: int, b: int, n: int) -> int: assert n > 1 assert a > 0 assert greatest_common_divisor(a, n) == 1 - (d, t, s) = extended_gcd(n, a) # Implemented below + (_d, _t, s) = extended_gcd(n, a) # Implemented below x = (b * s) % n return x @@ -47,7 +47,7 @@ def invert_modulo(a: int, n: int) -> int: 1 """ - (b, x) = extended_euclid(a, n) # Implemented below + (b, _x) = extended_euclid(a, n) # Implemented below if b < 0: b = (b % n + n) % n return b diff --git a/neural_network/convolution_neural_network.py b/neural_network/convolution_neural_network.py index d4ac360a98de..6b1aa50c7981 100644 --- a/neural_network/convolution_neural_network.py +++ b/neural_network/convolution_neural_network.py @@ -317,7 +317,7 @@ def predict(self, datas_test): print((" - - Shape: Test_Data ", np.shape(datas_test))) for p in range(len(datas_test)): data_test = np.asmatrix(datas_test[p]) - data_focus1, data_conved1 = self.convolute( + _data_focus1, data_conved1 = self.convolute( data_test, self.conv1, self.w_conv1, @@ -339,7 +339,7 @@ def predict(self, datas_test): def convolution(self, data): # return the data of image after convoluting process so we can check it out data_test = np.asmatrix(data) - data_focus1, data_conved1 = self.convolute( + _data_focus1, data_conved1 = self.convolute( data_test, self.conv1, self.w_conv1, diff --git a/project_euler/problem_551/sol1.py b/project_euler/problem_551/sol1.py index 100e9d41dd31..e13cf77a776d 100644 --- a/project_euler/problem_551/sol1.py +++ b/project_euler/problem_551/sol1.py @@ -185,7 +185,7 @@ def solution(n: int = 10**15) -> int: i = 1 dn = 0 while True: - diff, terms_jumped = next_term(digits, 20, i + dn, n) + _diff, terms_jumped = next_term(digits, 20, i + dn, n) dn += terms_jumped if dn == n - i: break diff --git a/pyproject.toml b/pyproject.toml index b680cc0d439e..71eb730f1329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,7 +124,6 @@ lint.ignore = [ "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME "SIM905", # Consider using a list literal instead of `str.split` -- DO NOT FIX "SLF001", # Private member accessed: `_Iterator` -- FIX ME - "UP038", # Use `X | Y` in `{}` call instead of `(X, Y)` -- DO NOT FIX ] lint.per-file-ignores."data_structures/hashing/tests/test_hash_map.py" = [ diff --git a/scheduling/multi_level_feedback_queue.py b/scheduling/multi_level_feedback_queue.py index abee3c85c5a5..58ba2afa0e67 100644 --- a/scheduling/multi_level_feedback_queue.py +++ b/scheduling/multi_level_feedback_queue.py @@ -255,7 +255,7 @@ def multi_level_feedback_queue(self) -> deque[Process]: # all queues except last one have round_robin algorithm for i in range(self.number_of_queues - 1): - finished, self.ready_queue = self.round_robin( + _finished, self.ready_queue = self.round_robin( self.ready_queue, self.time_slices[i] ) # the last queue has first_come_first_served algorithm From 0ee534edde76462eeace007afdba3c6a4d43dbf0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:17:29 +0200 Subject: [PATCH 07/76] [pre-commit.ci] pre-commit autoupdate (#12969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.17.1 → v1.18.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.17.1...v1.18.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4af51c08d8a4..9fbeb9a08682 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.1 hooks: - id: mypy args: From 4ec71a303b4794d5c702c50e1c64b9175ed72b71 Mon Sep 17 00:00:00 2001 From: lighting9999 Date: Thu, 18 Sep 2025 21:55:59 +0800 Subject: [PATCH 08/76] fix covid_stats_via_xpath.py (#12975) * fix covid_stats_via_xpath.py Improve error handling. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix covid_stats_via_xpath.py typo * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * upgrade covid_stats_via_xpath.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update and fix covid_stats_via_xpath.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- web_programming/covid_stats_via_xpath.py | 44 +++++++++++++++++------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/web_programming/covid_stats_via_xpath.py b/web_programming/covid_stats_via_xpath.py index 9c016ba414ea..88a248610441 100644 --- a/web_programming/covid_stats_via_xpath.py +++ b/web_programming/covid_stats_via_xpath.py @@ -1,7 +1,8 @@ """ -This is to show simple COVID19 info fetching from worldometers archive site using lxml -* The main motivation to use lxml in place of bs4 is that it is faster and therefore -more convenient to use in Python web projects (e.g. Django or Flask-based) +This script demonstrates fetching simple COVID-19 statistics from the +Worldometers archive site using lxml. lxml is chosen over BeautifulSoup +for its speed and convenience in Python web projects (such as Django or +Flask). """ # /// script @@ -25,15 +26,34 @@ class CovidData(NamedTuple): def covid_stats( - url: str = "https://web.archive.org/web/20250825095350/https://www.worldometers.info/coronavirus/", + url: str = ( + "https://web.archive.org/web/20250825095350/" + "https://www.worldometers.info/coronavirus/" + ), ) -> CovidData: xpath_str = '//div[@class = "maincounter-number"]/span/text()' - return CovidData( - *html.fromstring(httpx.get(url, timeout=10).content).xpath(xpath_str) + try: + response = httpx.get(url, timeout=10).raise_for_status() + except httpx.TimeoutException: + print( + "Request timed out. Please check your network connection " + "or try again later." + ) + return CovidData("N/A", "N/A", "N/A") + except httpx.HTTPStatusError as e: + print(f"HTTP error occurred: {e}") + return CovidData("N/A", "N/A", "N/A") + data = html.fromstring(response.content).xpath(xpath_str) + if len(data) != 3: + print("Unexpected data format. The page structure may have changed.") + data = "N/A", "N/A", "N/A" + return CovidData(*data) + + +if __name__ == "__main__": + fmt = ( + "Total COVID-19 cases in the world: {}\n" + "Total deaths due to COVID-19 in the world: {}\n" + "Total COVID-19 patients recovered in the world: {}" ) - - -fmt = """Total COVID-19 cases in the world: {} -Total deaths due to COVID-19 in the world: {} -Total COVID-19 patients recovered in the world: {}""" -print(fmt.format(*covid_stats())) + print(fmt.format(*covid_stats())) From e696e4dc9007b56d9e14d40d203c36ef861ab1b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 02:18:01 +0200 Subject: [PATCH 09/76] [pre-commit.ci] pre-commit autoupdate (#12988) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.0 → v0.13.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.0...v0.13.1) - [github.com/pre-commit/mirrors-mypy: v1.18.1 → v1.18.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.1...v1.18.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fbeb9a08682..2f7c43b06af2 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.13.0 + rev: v0.13.1 hooks: - id: ruff-check - id: ruff-format @@ -47,7 +47,7 @@ repos: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.1 + rev: v1.18.2 hooks: - id: mypy args: From 8d1fb262dab5d3f04c87f5433e8e2a6b9547ece4 Mon Sep 17 00:00:00 2001 From: kathrynpete <166650430+kathrynpete@users.noreply.github.com> Date: Tue, 23 Sep 2025 02:51:49 -0400 Subject: [PATCH 10/76] Added edit_distance test cases (#12984) * Added edit_distance test cases * Update edit_distance.py --------- Co-authored-by: Maxim Smolskiy --- strings/edit_distance.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/strings/edit_distance.py b/strings/edit_distance.py index e842c8555c8e..77ed23037937 100644 --- a/strings/edit_distance.py +++ b/strings/edit_distance.py @@ -14,6 +14,20 @@ def edit_distance(source: str, target: str) -> int: >>> edit_distance("GATTIC", "GALTIC") 1 + >>> edit_distance("NUM3", "HUM2") + 2 + >>> edit_distance("cap", "CAP") + 3 + >>> edit_distance("Cat", "") + 3 + >>> edit_distance("cat", "cat") + 0 + >>> edit_distance("", "123456789") + 9 + >>> edit_distance("Be@uty", "Beautyyyy!") + 5 + >>> edit_distance("lstring", "lsstring") + 1 """ if len(source) == 0: return len(target) From c0ad5bbde403b8db3097745e500e3f086607ee8f Mon Sep 17 00:00:00 2001 From: Dylanskyep <149001171+Dylanskyep@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:18:44 -0400 Subject: [PATCH 11/76] Add doctests for cross function. Contributes to #9943 (#12991) * Add doctests for cross function. Contributes to #9943 * Update sudoku_solver.py --------- Co-authored-by: Maxim Smolskiy --- data_structures/arrays/sudoku_solver.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py index 07269e2a69cc..d2fa43bbf298 100644 --- a/data_structures/arrays/sudoku_solver.py +++ b/data_structures/arrays/sudoku_solver.py @@ -11,6 +11,19 @@ def cross(items_a, items_b): """ Cross product of elements in A and elements in B. + + >>> cross('AB', '12') + ['A1', 'A2', 'B1', 'B2'] + >>> cross('ABC', '123') + ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'] + >>> cross('ABC', '1234') + ['A1', 'A2', 'A3', 'A4', 'B1', 'B2', 'B3', 'B4', 'C1', 'C2', 'C3', 'C4'] + >>> cross('', '12') + [] + >>> cross('A', '') + [] + >>> cross('', '') + [] """ return [a + b for a in items_a for b in items_b] From a71618f891da36ae6a8f9b58273f586ca6acf2a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:30:14 +0200 Subject: [PATCH 12/76] [pre-commit.ci] pre-commit autoupdate (#13006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.1 → v0.13.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.1...v0.13.2) * ci: autoupdate_schedule: monthly --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss --- .pre-commit-config.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f7c43b06af2..5c66d306b0e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +ci: + autoupdate_schedule: monthly + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -16,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.1 + rev: v0.13.2 hooks: - id: ruff-check - id: ruff-format From 7530a417e872040d35338d5e43ec176320dab462 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:49:00 +0200 Subject: [PATCH 13/76] [pre-commit.ci] pre-commit autoupdate (#13286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.2 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.2...v0.13.3) - [github.com/tox-dev/pyproject-fmt: v2.6.0 → v2.7.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.6.0...v2.7.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c66d306b0e0..82a669007945 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.2 + rev: v0.13.3 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.6.0 + rev: v2.7.0 hooks: - id: pyproject-fmt From 9372040da93cf7f77fc4ec2fd9ce5f2761b8800b Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 7 Oct 2025 18:23:37 +0200 Subject: [PATCH 14/76] Test on Python 3.14 (#12710) --- .github/workflows/build.yml | 9 ++++++++- .github/workflows/directory_writer.yml | 3 ++- .github/workflows/project_euler.yml | 20 ++++++++++++++++++-- .github/workflows/sphinx.yml | 9 ++++++++- ciphers/gronsfeld_cipher.py | 2 +- machine_learning/xgboost_classifier.py | 2 -- maths/largest_of_very_large_numbers.py | 2 +- pyproject.toml | 7 ++++--- 8 files changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 731e3fad3b85..666d45b13c1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,13 @@ jobs: build: runs-on: ubuntu-latest steps: + - run: + sudo apt-get update && sudo apt-get install -y libtiff5-dev libjpeg8-dev libopenjp2-7-dev + zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk + libharfbuzz-dev libfribidi-dev libxcb1-dev + libxml2-dev libxslt-dev + libhdf5-dev + libopenblas-dev - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 with: @@ -16,7 +23,7 @@ jobs: cache-dependency-glob: uv.lock - uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: 3.14 allow-prereleases: true - run: uv sync --group=test - name: Run tests diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index 9a4682677c00..866440a37b31 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -11,7 +11,8 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: 3.14 + allow-prereleases: true - name: Write DIRECTORY.md run: | scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index f52ff280b29a..dbea5aeeea02 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -14,21 +14,37 @@ jobs: project-euler: runs-on: ubuntu-latest steps: + - run: + sudo apt-get update && sudo apt-get install -y libtiff5-dev libjpeg8-dev libopenjp2-7-dev + zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk + libharfbuzz-dev libfribidi-dev libxcb1-dev + libxml2-dev libxslt-dev + libhdf5-dev + libopenblas-dev - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: 3.14 + allow-prereleases: true - run: uv sync --group=euler-validate --group=test - run: uv run pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=project_euler/ project_euler/ validate-solutions: runs-on: ubuntu-latest steps: + - run: + sudo apt-get update && sudo apt-get install -y libtiff5-dev libjpeg8-dev libopenjp2-7-dev + zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk + libharfbuzz-dev libfribidi-dev libxcb1-dev + libxml2-dev libxslt-dev + libhdf5-dev + libopenblas-dev - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - uses: actions/setup-python@v6 with: - python-version: 3.x + python-version: 3.14 + allow-prereleases: true - run: uv sync --group=euler-validate --group=test - run: uv run pytest scripts/validate_solutions.py env: diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index bd253dc3de65..c12ebb23ded3 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -25,11 +25,18 @@ jobs: build_docs: runs-on: ubuntu-24.04-arm steps: + - run: + sudo apt-get update && sudo apt-get install -y libtiff5-dev libjpeg8-dev libopenjp2-7-dev + zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk + libharfbuzz-dev libfribidi-dev libxcb1-dev + libxml2-dev libxslt-dev + libhdf5-dev + libopenblas-dev - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 allow-prereleases: true - run: uv sync --group=docs - uses: actions/configure-pages@v5 diff --git a/ciphers/gronsfeld_cipher.py b/ciphers/gronsfeld_cipher.py index 8fbeab4307fc..a72b141bd502 100644 --- a/ciphers/gronsfeld_cipher.py +++ b/ciphers/gronsfeld_cipher.py @@ -20,7 +20,7 @@ def gronsfeld(text: str, key: str) -> str: >>> gronsfeld('yes, ¥€$ - _!@#%?', '') Traceback (most recent call last): ... - ZeroDivisionError: integer modulo by zero + ZeroDivisionError: division by zero """ ascii_len = len(ascii_uppercase) key_len = len(key) diff --git a/machine_learning/xgboost_classifier.py b/machine_learning/xgboost_classifier.py index 1da933cf690f..e845480074b9 100644 --- a/machine_learning/xgboost_classifier.py +++ b/machine_learning/xgboost_classifier.py @@ -42,8 +42,6 @@ def xgboost(features: np.ndarray, target: np.ndarray) -> XGBClassifier: def main() -> None: """ - >>> main() - Url for the algorithm: https://xgboost.readthedocs.io/en/stable/ Iris type dataset is used to demonstrate algorithm. diff --git a/maths/largest_of_very_large_numbers.py b/maths/largest_of_very_large_numbers.py index edee50371e02..e38ab2edb932 100644 --- a/maths/largest_of_very_large_numbers.py +++ b/maths/largest_of_very_large_numbers.py @@ -15,7 +15,7 @@ def res(x, y): >>> res(-1, 5) Traceback (most recent call last): ... - ValueError: math domain error + ValueError: expected a positive input """ if 0 not in (x, y): # We use the relation x^y = y*log10(x), where 10 is the base. diff --git a/pyproject.toml b/pyproject.toml index 71eb730f1329..7e64ad6f150b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,16 +10,17 @@ classifiers = [ ] dependencies = [ "beautifulsoup4>=4.12.3", + "cython>=3.1.2", "fake-useragent>=1.5.1", "httpx>=0.28.1", "imageio>=2.36.1", "keras>=3.7", - "lxml>=5.3", + "lxml>=6", "matplotlib>=3.9.3", "numpy>=2.1.3", "opencv-python>=4.10.0.84", "pandas>=2.2.3", - "pillow>=11", + "pillow>=11.3", "rich>=13.9.4", "scikit-learn>=1.5.2", "sphinx-pyproject>=0.3", @@ -32,7 +33,7 @@ dependencies = [ [dependency-groups] test = [ - "pytest>=8.3.4", + "pytest>=8.4.1", "pytest-cov>=6", ] From f0d5949e5a86134b6a6172ea102281a142e9f69f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 07:41:20 +0200 Subject: [PATCH 15/76] Bump astral-sh/setup-uv from 6 to 7 (#13335) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6 to 7. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/project_euler.yml | 4 ++-- .github/workflows/ruff.yml | 2 +- .github/workflows/sphinx.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 666d45b13c1b..43b3b3d9de1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: libhdf5-dev libopenblas-dev - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: uv.lock diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index dbea5aeeea02..05adb43b0bd4 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -22,7 +22,7 @@ jobs: libhdf5-dev libopenblas-dev - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: python-version: 3.14 @@ -40,7 +40,7 @@ jobs: libhdf5-dev libopenblas-dev - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: python-version: 3.14 diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 7bcc2850782f..b17236ccbff9 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -12,5 +12,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@v7 - run: uvx ruff check --output-format=github . diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index c12ebb23ded3..b945869e84a7 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -33,7 +33,7 @@ jobs: libhdf5-dev libopenblas-dev - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: python-version: 3.14 From 788d95b4101389617ca9f7fd043998089f98df78 Mon Sep 17 00:00:00 2001 From: Anuska Roy Date: Wed, 8 Oct 2025 17:41:28 +0530 Subject: [PATCH 16/76] added rotate_array.py (#13336) * added rotate_array.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fixed issues * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fixed reverse issue * added doctests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * changed k to steps for a descriptive name * fixed non-pep --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- data_structures/arrays/rotate_array.py | 80 ++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 data_structures/arrays/rotate_array.py diff --git a/data_structures/arrays/rotate_array.py b/data_structures/arrays/rotate_array.py new file mode 100644 index 000000000000..d5ce4b4078b3 --- /dev/null +++ b/data_structures/arrays/rotate_array.py @@ -0,0 +1,80 @@ +def rotate_array(arr: list[int], steps: int) -> list[int]: + """ + Rotates a list to the right by steps positions. + + Parameters: + arr (List[int]): The list of integers to rotate. + steps (int): Number of positions to rotate. Can be negative for left rotation. + + Returns: + List[int]: Rotated list. + + Examples: + >>> rotate_array([1, 2, 3, 4, 5], 2) + [4, 5, 1, 2, 3] + >>> rotate_array([1, 2, 3, 4, 5], -2) + [3, 4, 5, 1, 2] + >>> rotate_array([1, 2, 3, 4, 5], 7) + [4, 5, 1, 2, 3] + >>> rotate_array([], 3) + [] + """ + + n = len(arr) + if n == 0: + return arr + + steps = steps % n + + if steps < 0: + steps += n + + def reverse(start: int, end: int) -> None: + """ + Reverses a portion of the list in place from index start to end. + + Parameters: + start (int): Starting index of the portion to reverse. + end (int): Ending index of the portion to reverse. + + Returns: + None + + Examples: + >>> example = [1, 2, 3, 4, 5] + >>> def reverse_test(arr, start, end): + ... while start < end: + ... arr[start], arr[end] = arr[end], arr[start] + ... start += 1 + ... end -= 1 + >>> reverse_test(example, 0, 2) + >>> example + [3, 2, 1, 4, 5] + >>> reverse_test(example, 2, 4) + >>> example + [3, 2, 5, 4, 1] + """ + + while start < end: + arr[start], arr[end] = arr[end], arr[start] + start += 1 + end -= 1 + + reverse(0, n - 1) + reverse(0, steps - 1) + reverse(steps, n - 1) + + return arr + + +if __name__ == "__main__": + examples = [ + ([1, 2, 3, 4, 5], 2), + ([1, 2, 3, 4, 5], -2), + ([1, 2, 3, 4, 5], 7), + ([], 3), + ] + + for arr, steps in examples: + rotated = rotate_array(arr.copy(), steps) + print(f"Rotate {arr} by {steps}: {rotated}") From 1562ae1ec39bed716fc3f9da873844747dc0686c Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 14 Oct 2025 09:33:01 +0200 Subject: [PATCH 17/76] Add a README.md file to the scripts directory (#13480) * Add a README.md file to the scripts directory * updating DIRECTORY.md --------- Co-authored-by: cclauss --- DIRECTORY.md | 1 + scripts/README.md | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 scripts/README.md diff --git a/DIRECTORY.md b/DIRECTORY.md index 36acb3b97f1e..6249b75c4231 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -195,6 +195,7 @@ * [Permutations](data_structures/arrays/permutations.py) * [Prefix Sum](data_structures/arrays/prefix_sum.py) * [Product Sum](data_structures/arrays/product_sum.py) + * [Rotate Array](data_structures/arrays/rotate_array.py) * [Sparse Table](data_structures/arrays/sparse_table.py) * [Sudoku Solver](data_structures/arrays/sudoku_solver.py) * Binary Tree diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000000..92ebf3a7e8ba --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,27 @@ +Dealing with the onslaught of Hacktoberfest +* https://hacktoberfest.com + +Each year, October brings a swarm of new contributors participating in Hacktoberfest. This event has its pros and cons, but it presents a monumental workload for the few active maintainers of this repo. The maintainer workload is further impacted by a new version of CPython being released in the first week of each October. + +To help make our algorithms more valuable to visitors, our CONTRIBUTING.md file outlines several strict requirements, such as tests, type hints, descriptive names, functions, and/or classes. Maintainers reviewing pull requests should try to encourage improvements to meet these goals, but when the workload becomes overwhelming (esp. in October), pull requests that do not meet these goals should be closed. + +Below are a few [`gh`](https://cli.github.com) scripts that should close pull requests that do not match the definition of an acceptable algorithm as defined in CONTRIBUTING.md. I tend to run these scripts in the following order. + +* close_pull_requests_with_require_descriptive_names.sh +* close_pull_requests_with_require_tests.sh +* close_pull_requests_with_require_type_hints.sh +* close_pull_requests_with_failing_tests.sh +* close_pull_requests_with_awaiting_changes.sh +* find_git_conflicts.sh + +### Run on 14 Oct 2025: 107 of 541 (19.77%) pull requests closed. + +Script run | Open pull requests | Pull requests closed +--- | --- | --- +None | 541 | 0 +require_descriptive_names | 515 | 26 +require_tests | 498 | 17 +require_type_hints | 496 | 2 +failing_tests | 438 | ___58___ +awaiting_changes | 434 | 4 +git_conflicts | [ broken ] | 0 From 709c18ee9f1a19659a2187bb3f022037328de09a Mon Sep 17 00:00:00 2001 From: Khansa435 Date: Tue, 14 Oct 2025 16:14:22 +0500 Subject: [PATCH 18/76] Add t stochastic neighbour embedding using Iris dataset (#13476) * Added t-SNE with Iris dataset example * Added t-SNE with Iris dataset example * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Updated with descriptive variables * Add descriptive variable names * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add Descriptive Variable names * Adding Descriptive variable names * Update machine_learning/t_stochastic_neighbour_embedding.py Co-authored-by: Christian Clauss * Update machine_learning/t_stochastic_neighbour_embedding.py Co-authored-by: Christian Clauss * Improved line formatting * Adding URL for t-SNE Wikipedia * Apply suggestion from @cclauss --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss --- .../t_stochastic_neighbour_embedding.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 machine_learning/t_stochastic_neighbour_embedding.py diff --git a/machine_learning/t_stochastic_neighbour_embedding.py b/machine_learning/t_stochastic_neighbour_embedding.py new file mode 100644 index 000000000000..d6f630149087 --- /dev/null +++ b/machine_learning/t_stochastic_neighbour_embedding.py @@ -0,0 +1,178 @@ +""" +t-distributed stochastic neighbor embedding (t-SNE) + +For more details, see: +https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding +""" + +import doctest + +import numpy as np +from numpy import ndarray +from sklearn.datasets import load_iris + + +def collect_dataset() -> tuple[ndarray, ndarray]: + """ + Load the Iris dataset and return features and labels. + + Returns: + tuple[ndarray, ndarray]: Feature matrix and target labels. + + >>> features, targets = collect_dataset() + >>> features.shape + (150, 4) + >>> targets.shape + (150,) + """ + iris_dataset = load_iris() + return np.array(iris_dataset.data), np.array(iris_dataset.target) + + +def compute_pairwise_affinities(data_matrix: ndarray, sigma: float = 1.0) -> ndarray: + """ + Compute high-dimensional affinities (P matrix) using a Gaussian kernel. + + Args: + data_matrix: Input data of shape (n_samples, n_features). + sigma: Gaussian kernel bandwidth. + + Returns: + ndarray: Symmetrized probability matrix. + + >>> x = np.array([[0.0, 0.0], [1.0, 0.0]]) + >>> probabilities = compute_pairwise_affinities(x) + >>> float(round(probabilities[0, 1], 3)) + 0.25 + """ + n_samples = data_matrix.shape[0] + squared_sum = np.sum(np.square(data_matrix), axis=1) + squared_distance = np.add( + np.add(-2 * np.dot(data_matrix, data_matrix.T), squared_sum).T, squared_sum + ) + + affinity_matrix = np.exp(-squared_distance / (2 * sigma**2)) + np.fill_diagonal(affinity_matrix, 0) + + affinity_matrix /= np.sum(affinity_matrix) + return (affinity_matrix + affinity_matrix.T) / (2 * n_samples) + + +def compute_low_dim_affinities(embedding_matrix: ndarray) -> tuple[ndarray, ndarray]: + """ + Compute low-dimensional affinities (Q matrix) using a Student-t distribution. + + Args: + embedding_matrix: Low-dimensional embedding of shape (n_samples, n_components). + + Returns: + tuple[ndarray, ndarray]: (Q probability matrix, numerator matrix). + + >>> y = np.array([[0.0, 0.0], [1.0, 0.0]]) + >>> q_matrix, numerators = compute_low_dim_affinities(y) + >>> q_matrix.shape + (2, 2) + """ + squared_sum = np.sum(np.square(embedding_matrix), axis=1) + numerator_matrix = 1 / ( + 1 + + np.add( + np.add(-2 * np.dot(embedding_matrix, embedding_matrix.T), squared_sum).T, + squared_sum, + ) + ) + np.fill_diagonal(numerator_matrix, 0) + + q_matrix = numerator_matrix / np.sum(numerator_matrix) + return q_matrix, numerator_matrix + + +def apply_tsne( + data_matrix: ndarray, + n_components: int = 2, + learning_rate: float = 200.0, + n_iter: int = 500, +) -> ndarray: + """ + Apply t-SNE for dimensionality reduction. + + Args: + data_matrix: Original dataset (features). + n_components: Target dimension (2D or 3D). + learning_rate: Step size for gradient descent. + n_iter: Number of iterations. + + Returns: + ndarray: Low-dimensional embedding of the data. + + >>> features, _ = collect_dataset() + >>> embedding = apply_tsne(features, n_components=2, n_iter=50) + >>> embedding.shape + (150, 2) + """ + if n_components < 1 or n_iter < 1: + raise ValueError("n_components and n_iter must be >= 1") + + n_samples = data_matrix.shape[0] + rng = np.random.default_rng() + embedding = rng.standard_normal((n_samples, n_components)) * 1e-4 + + high_dim_affinities = compute_pairwise_affinities(data_matrix) + high_dim_affinities = np.maximum(high_dim_affinities, 1e-12) + + embedding_increment = np.zeros_like(embedding) + momentum = 0.5 + + for iteration in range(n_iter): + low_dim_affinities, numerator_matrix = compute_low_dim_affinities(embedding) + low_dim_affinities = np.maximum(low_dim_affinities, 1e-12) + + affinity_diff = high_dim_affinities - low_dim_affinities + + gradient = 4 * ( + np.dot((affinity_diff * numerator_matrix), embedding) + - np.multiply( + np.sum(affinity_diff * numerator_matrix, axis=1)[:, np.newaxis], + embedding, + ) + ) + + embedding_increment = momentum * embedding_increment - learning_rate * gradient + embedding += embedding_increment + + if iteration == int(n_iter / 4): + momentum = 0.8 + + return embedding + + +def main() -> None: + """ + Run t-SNE on the Iris dataset and display the first 5 embeddings. + + >>> main() # doctest: +ELLIPSIS + t-SNE embedding (first 5 points): + [[... + """ + features, _labels = collect_dataset() + embedding = apply_tsne(features, n_components=2, n_iter=300) + + if not isinstance(embedding, np.ndarray): + raise TypeError("t-SNE embedding must be an ndarray") + + print("t-SNE embedding (first 5 points):") + print(embedding[:5]) + + # Optional visualization (Ruff/mypy compliant) + + # import matplotlib.pyplot as plt + # plt.scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap="viridis") + # plt.title("t-SNE Visualization of the Iris Dataset") + # plt.xlabel("Dimension 1") + # plt.ylabel("Dimension 2") + # plt.show() + + +if __name__ == "__main__": + doctest.testmod() + main() From e731514bd5f6111c1859895c6b19fae0de551513 Mon Sep 17 00:00:00 2001 From: iddu <127777022+1drie5@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:33:33 +0530 Subject: [PATCH 19/76] Fix typo and function call in maths module (#13515) --- maths/factorial.py | 2 +- maths/fibonacci.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maths/factorial.py b/maths/factorial.py index aaf90f384bb9..ba61447c7564 100644 --- a/maths/factorial.py +++ b/maths/factorial.py @@ -56,7 +56,7 @@ def factorial_recursive(n: int) -> int: raise ValueError("factorial() only accepts integral values") if n < 0: raise ValueError("factorial() not defined for negative values") - return 1 if n in {0, 1} else n * factorial(n - 1) + return 1 if n in {0, 1} else n * factorial_recursive(n - 1) if __name__ == "__main__": diff --git a/maths/fibonacci.py b/maths/fibonacci.py index 24b2d7ae449e..71ff479f9cc2 100644 --- a/maths/fibonacci.py +++ b/maths/fibonacci.py @@ -183,7 +183,7 @@ def fib_memoization(n: int) -> list[int]: """ if n < 0: raise ValueError("n is negative") - # Cache must be outside recursuive function + # Cache must be outside recursive function # other it will reset every time it calls itself. cache: dict[int, int] = {0: 0, 1: 1, 2: 1} # Prefilled cache From 9902c23e14fd90f163ee95d557d374fc44793cd9 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Oct 2025 19:06:25 +0200 Subject: [PATCH 20/76] Delete requirements.txt because dependencies are in pyproject.toml (#13486) --- .github/workflows/build.yml | 9 ++------- DIRECTORY.md | 1 + pyproject.toml | 1 + requirements.txt | 19 ------------------- 4 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 requirements.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43b3b3d9de1b..9a97424c56c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,13 +9,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - run: - sudo apt-get update && sudo apt-get install -y libtiff5-dev libjpeg8-dev libopenjp2-7-dev - zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk - libharfbuzz-dev libfribidi-dev libxcb1-dev - libxml2-dev libxslt-dev - libhdf5-dev - libopenblas-dev + - run: sudo apt-get update && sudo apt-get install -y libhdf5-dev - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v7 with: @@ -32,6 +26,7 @@ jobs: --ignore=computer_vision/cnn_classification.py --ignore=docs/conf.py --ignore=dynamic_programming/k_means_clustering_tensorflow.py + --ignore=machine_learning/local_weighted_learning/local_weighted_learning.py --ignore=machine_learning/lstm/lstm_prediction.py --ignore=neural_network/input_data.py --ignore=project_euler/ diff --git a/DIRECTORY.md b/DIRECTORY.md index 6249b75c4231..0f9859577493 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -624,6 +624,7 @@ * [Sequential Minimum Optimization](machine_learning/sequential_minimum_optimization.py) * [Similarity Search](machine_learning/similarity_search.py) * [Support Vector Machines](machine_learning/support_vector_machines.py) + * [T Stochastic Neighbour Embedding](machine_learning/t_stochastic_neighbour_embedding.py) * [Word Frequency Functions](machine_learning/word_frequency_functions.py) * [Xgboost Classifier](machine_learning/xgboost_classifier.py) * [Xgboost Regressor](machine_learning/xgboost_regressor.py) diff --git a/pyproject.toml b/pyproject.toml index 7e64ad6f150b..537ba79bd5f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "pillow>=11.3", "rich>=13.9.4", "scikit-learn>=1.5.2", + "scipy>=1.16.2", "sphinx-pyproject>=0.3", "statsmodels>=0.14.4", "sympy>=1.13.3", diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 66b5d8a6b94e..000000000000 --- a/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -beautifulsoup4 -fake-useragent -httpx -imageio -keras -lxml -matplotlib -numpy -opencv-python -pandas -pillow -rich -scikit-learn -sphinx-pyproject -statsmodels -sympy -tweepy -typing_extensions -xgboost From 85e67302d8ad4e813b939e18cfd734824d8f966a Mon Sep 17 00:00:00 2001 From: Matt Ryan <44824894+mattryanmtl@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:22:51 -0400 Subject: [PATCH 21/76] Test on 3.14 (#13473) Tested on 3.14. --- sorts/binary_insertion_sort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sorts/binary_insertion_sort.py b/sorts/binary_insertion_sort.py index 50653a99e7ce..b928316a849d 100644 --- a/sorts/binary_insertion_sort.py +++ b/sorts/binary_insertion_sort.py @@ -56,7 +56,7 @@ def binary_insertion_sort(collection: list) -> list: return collection -if __name__ == "__main": +if __name__ == "__main__": user_input = input("Enter numbers separated by a comma:\n").strip() try: unsorted = [int(item) for item in user_input.split(",")] From 3cea94179d40bc80e94fc4191808de86eadf6642 Mon Sep 17 00:00:00 2001 From: Omkaar <79257339+Ombucha@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:05:55 +0530 Subject: [PATCH 22/76] Fix a few typos (#13346) * Fix typo in spheres intersection print statement * Fix typo in CONTRIBUTING.md * Improve comments in comb_sort.py * pyproject.toml: tool.ruff.target-version = "py314" * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix comment formatting in lint.ignore section --------- Co-authored-by: Christian Clauss Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- maths/volume.py | 2 +- pyproject.toml | 8 ++++---- sorts/comb_sort.py | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3df39f95b784..35de0bf75ed5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,7 @@ We want your work to be readable by others; therefore, we encourage you to note ruff check ``` -- Original code submission require docstrings or comments to describe your work. +- Original code submissions require docstrings or comments to describe your work. - More on docstrings and comments: diff --git a/maths/volume.py b/maths/volume.py index 08bdf72b013b..1715c9c300d5 100644 --- a/maths/volume.py +++ b/maths/volume.py @@ -555,7 +555,7 @@ def main(): print(f"Torus: {vol_torus(2, 2) = }") # ~= 157.9 print(f"Conical Frustum: {vol_conical_frustum(2, 2, 4) = }") # ~= 58.6 print(f"Spherical cap: {vol_spherical_cap(1, 2) = }") # ~= 5.24 - print(f"Spheres intersetion: {vol_spheres_intersect(2, 2, 1) = }") # ~= 21.21 + print(f"Spheres intersection: {vol_spheres_intersect(2, 2, 1) = }") # ~= 21.21 print(f"Spheres union: {vol_spheres_union(2, 2, 1) = }") # ~= 45.81 print( f"Hollow Circular Cylinder: {vol_hollow_circular_cylinder(1, 2, 3) = }" diff --git a/pyproject.toml b/pyproject.toml index 537ba79bd5f1..60ba0d3b65d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,10 +3,9 @@ name = "thealgorithms-python" version = "0.0.1" description = "TheAlgorithms in Python" authors = [ { name = "TheAlgorithms Contributors" } ] -requires-python = ">=3.13" +requires-python = ">=3.14" classifiers = [ "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.13", ] dependencies = [ "beautifulsoup4>=4.12.3", @@ -49,7 +48,7 @@ euler-validate = [ ] [tool.ruff] -target-version = "py313" +target-version = "py314" output-format = "full" lint.select = [ @@ -110,7 +109,7 @@ lint.ignore = [ # `ruff rule S101` for a description of that rule "B904", # Within an `except` clause, raise exceptions with `raise ... from err` -- FIX ME "B905", # `zip()` without an explicit `strict=` parameter -- FIX ME - "EM101", # Exception must not use a string literal, assign to variable first + "EM101", # Exception must not use a string literal, assign to a variable first "EXE001", # Shebang is present but file is not executable -- DO NOT FIX "G004", # Logging statement uses f-string "ISC001", # Conflicts with ruff format -- DO NOT FIX @@ -126,6 +125,7 @@ lint.ignore = [ "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME "SIM905", # Consider using a list literal instead of `str.split` -- DO NOT FIX "SLF001", # Private member accessed: `_Iterator` -- FIX ME + "UP037", # FIX ME ] lint.per-file-ignores."data_structures/hashing/tests/test_hash_map.py" = [ diff --git a/sorts/comb_sort.py b/sorts/comb_sort.py index 3c8b1e99a454..94ad8f533328 100644 --- a/sorts/comb_sort.py +++ b/sorts/comb_sort.py @@ -5,8 +5,7 @@ Comb sort improves on bubble sort algorithm. In bubble sort, distance (or gap) between two compared elements is always one. Comb sort improvement is that gap can be much more than 1, in order to prevent slowing -down by small values -at the end of a list. +down by small values at the end of a list. More info on: https://en.wikipedia.org/wiki/Comb_sort From 8edc478a19dda14a63300fc86b3520cc037621f9 Mon Sep 17 00:00:00 2001 From: Gunish Mukherji Date: Fri, 17 Oct 2025 06:06:38 +0530 Subject: [PATCH 23/76] Adding missing return type to pi_estimator function (#13427) - Add -> None return type annotation to pi_estimator function - Improves code clarity and follows Python type hinting best practices - Function already had proper type hints for parameters Co-authored-by: Gunish Mukherji Co-authored-by: Maxim Smolskiy --- maths/monte_carlo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maths/monte_carlo.py b/maths/monte_carlo.py index d174a0b188a2..5eb176238ffb 100644 --- a/maths/monte_carlo.py +++ b/maths/monte_carlo.py @@ -8,7 +8,7 @@ from statistics import mean -def pi_estimator(iterations: int): +def pi_estimator(iterations: int) -> None: """ An implementation of the Monte Carlo method used to find pi. 1. Draw a 2x2 square centred at (0,0). From c79034ca2114e56ede887a473c2853b8c6d49257 Mon Sep 17 00:00:00 2001 From: Harsh Pathak <156679457+HarshPathak310@users.noreply.github.com> Date: Fri, 17 Oct 2025 06:30:44 +0530 Subject: [PATCH 24/76] Update logical issue in decision_tree.py (#13303) Co-authored-by: Maxim Smolskiy --- machine_learning/decision_tree.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/machine_learning/decision_tree.py b/machine_learning/decision_tree.py index 72970431c3fc..b4df64796bb1 100644 --- a/machine_learning/decision_tree.py +++ b/machine_learning/decision_tree.py @@ -146,14 +146,13 @@ def predict(self, x): """ if self.prediction is not None: return self.prediction - elif self.left or self.right is not None: + elif self.left is not None and self.right is not None: if x >= self.decision_boundary: return self.right.predict(x) else: return self.left.predict(x) else: - print("Error: Decision tree not yet trained") - return None + raise ValueError("Decision tree not yet trained") class TestDecisionTree: @@ -201,4 +200,4 @@ def main(): main() import doctest - doctest.testmod(name="mean_squarred_error", verbose=True) + doctest.testmod(name="mean_squared_error", verbose=True) From 3b08413ab32bb5526c6043681db6e3ae9df4fd4a Mon Sep 17 00:00:00 2001 From: Tejasrahane <161036451+Tejasrahane@users.noreply.github.com> Date: Mon, 20 Oct 2025 02:21:11 +0530 Subject: [PATCH 25/76] Add doctest for circular queue overflow condition (#13590) * Add doctest for circular queue overflow condition Added a doctest to test the QUEUE IS FULL exception when attempting to enqueue an element into a full circular queue. This improves test coverage for line 67 in data_structures/queues/circular_queue.py. Fixes #9943 * Update circular_queue.py * Update circular_queue.py * Update circular_queue.py * Update circular_queue.py --------- Co-authored-by: Maxim Smolskiy --- data_structures/queues/circular_queue.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/data_structures/queues/circular_queue.py b/data_structures/queues/circular_queue.py index efbf1efdc42d..e9cb2cac4fd8 100644 --- a/data_structures/queues/circular_queue.py +++ b/data_structures/queues/circular_queue.py @@ -17,7 +17,7 @@ def __len__(self) -> int: >>> len(cq) 0 >>> cq.enqueue("A") # doctest: +ELLIPSIS - >>> cq.array ['A', None, None, None, None] >>> len(cq) @@ -51,17 +51,24 @@ def enqueue(self, data): """ This function inserts an element at the end of the queue using self.rear value as an index. + >>> cq = CircularQueue(5) >>> cq.enqueue("A") # doctest: +ELLIPSIS - >>> (cq.size, cq.first()) (1, 'A') >>> cq.enqueue("B") # doctest: +ELLIPSIS - >>> cq.array ['A', 'B', None, None, None] >>> (cq.size, cq.first()) (2, 'A') + >>> cq.enqueue("C").enqueue("D").enqueue("E") # doctest: +ELLIPSIS + + >>> cq.enqueue("F") + Traceback (most recent call last): + ... + Exception: QUEUE IS FULL """ if self.size >= self.n: raise Exception("QUEUE IS FULL") @@ -75,6 +82,7 @@ def dequeue(self): """ This function removes an element from the queue using on self.front value as an index and returns it + >>> cq = CircularQueue(5) >>> cq.dequeue() Traceback (most recent call last): From 154cd3e4002d22756cf192c76db0a9ac8a918867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neto?= Date: Sun, 19 Oct 2025 19:21:00 -0300 Subject: [PATCH 26/76] feat: optimizing the prune function at the apriori_algorithm.py archive (#12992) * feat: optimizing the prune function at the apriori_algorithm.py archive * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: fixing the unsorted importing statment * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: fixing the key structure to a tuple that can be an hashable structure * Update apriori_algorithm.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update apriori_algorithm.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- machine_learning/apriori_algorithm.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/machine_learning/apriori_algorithm.py b/machine_learning/apriori_algorithm.py index 09a89ac236bd..5c3e2baba2c2 100644 --- a/machine_learning/apriori_algorithm.py +++ b/machine_learning/apriori_algorithm.py @@ -11,6 +11,7 @@ Examples: https://www.kaggle.com/code/earthian/apriori-association-rules-mining """ +from collections import Counter from itertools import combinations @@ -44,11 +45,16 @@ def prune(itemset: list, candidates: list, length: int) -> list: >>> prune(itemset, candidates, 3) [] """ + itemset_counter = Counter(tuple(item) for item in itemset) pruned = [] for candidate in candidates: is_subsequence = True for item in candidate: - if item not in itemset or itemset.count(item) < length - 1: + item_tuple = tuple(item) + if ( + item_tuple not in itemset_counter + or itemset_counter[item_tuple] < length - 1 + ): is_subsequence = False break if is_subsequence: From 1b0bd167290bbdd5cb56972f1c6fb8d18698c839 Mon Sep 17 00:00:00 2001 From: michaelmccamy <149010657+michaelmccamy@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:29:59 -0400 Subject: [PATCH 27/76] Add doctest for add_vertex in GraphAdjacencyList. Contributes to #9943 (#13143) * Add doctest for add_vertex in GraphAdjacencyList. Contributes to #9943 * Update graph_adjacency_list.py --------- Co-authored-by: Maxim Smolskiy --- graphs/graph_adjacency_list.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/graphs/graph_adjacency_list.py b/graphs/graph_adjacency_list.py index c901e2cf3dac..34014d69dfb8 100644 --- a/graphs/graph_adjacency_list.py +++ b/graphs/graph_adjacency_list.py @@ -61,6 +61,15 @@ def add_vertex(self, vertex: T) -> None: """ Adds a vertex to the graph. If the given vertex already exists, a ValueError will be thrown. + + >>> g = GraphAdjacencyList(vertices=[], edges=[], directed=False) + >>> g.add_vertex("A") + >>> g.adj_list + {'A': []} + >>> g.add_vertex("A") + Traceback (most recent call last): + ... + ValueError: Incorrect input: A is already in the graph. """ if self.contains_vertex(vertex): msg = f"Incorrect input: {vertex} is already in the graph." From e2a78d4e76adbce1f6b93f649820982165d6092d Mon Sep 17 00:00:00 2001 From: Md Mahiuddin <68785084+mahiuddin-dev@users.noreply.github.com> Date: Mon, 20 Oct 2025 06:59:36 +0600 Subject: [PATCH 28/76] Add test for non-integer input to factorial function (#13024) * Add test for non-integer input to factorial function * Update test_factorial.py --------- Co-authored-by: Maxim Smolskiy --- maths/test_factorial.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maths/test_factorial.py b/maths/test_factorial.py index d80d88add745..1795ebba194f 100644 --- a/maths/test_factorial.py +++ b/maths/test_factorial.py @@ -33,5 +33,11 @@ def test_negative_number(function): function(-3) +@pytest.mark.parametrize("function", [factorial, factorial_recursive]) +def test_float_number(function): + with pytest.raises(ValueError): + function(1.5) + + if __name__ == "__main__": pytest.main(["-v", __file__]) From af17867f409ec80f4d0cc499943c455da4307cd4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:02:38 +0100 Subject: [PATCH 29/76] [pre-commit.ci] pre-commit autoupdate (#13860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.3) - [github.com/tox-dev/pyproject-fmt: v2.7.0 → v2.11.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.7.0...v2.11.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82a669007945..e305772298d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.3 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.7.0 + rev: v2.11.0 hooks: - id: pyproject-fmt diff --git a/pyproject.toml b/pyproject.toml index 60ba0d3b65d9..f1559d6bc1b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ authors = [ { name = "TheAlgorithms Contributors" } ] requires-python = ">=3.14" classifiers = [ "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.14", ] dependencies = [ "beautifulsoup4>=4.12.3", From ae68a7800883021b4bb5f40ccfb3773a189a8d09 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 6 Nov 2025 13:11:49 +0100 Subject: [PATCH 30/76] uv run --with=pytest-run-parallel --iterations=8 --parallel-threads=auto (#13863) https://github.com/Quansight-Labs/pytest-run-parallel * https://py-free-threading.github.io * https://www.python.org/downloads/release/python-3140/ --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a97424c56c4..bbe4b782a00a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,8 @@ jobs: - run: uv sync --group=test - name: Run tests # TODO: #8818 Re-enable quantum tests - run: uv run pytest + run: uv run --with=pytest-run-parallel pytest + --iterations=8 --parallel-threads=auto --ignore=computer_vision/cnn_classification.py --ignore=docs/conf.py --ignore=dynamic_programming/k_means_clustering_tensorflow.py From a051ab5b0957c9a42559cc4bad30463af5708771 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 08:11:22 +0100 Subject: [PATCH 31/76] Bump actions/checkout from 5 to 6 (#13937) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/devcontainer_ci.yml | 2 +- .github/workflows/directory_writer.yml | 2 +- .github/workflows/project_euler.yml | 4 ++-- .github/workflows/ruff.yml | 2 +- .github/workflows/sphinx.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbe4b782a00a..2bb8e1d69217 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - run: sudo apt-get update && sudo apt-get install -y libhdf5-dev - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 with: enable-cache: true diff --git a/.github/workflows/devcontainer_ci.yml b/.github/workflows/devcontainer_ci.yml index 71623e5e6e69..d1b81593866f 100644 --- a/.github/workflows/devcontainer_ci.yml +++ b/.github/workflows/devcontainer_ci.yml @@ -12,7 +12,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: devcontainers/ci@v0.3 with: push: never diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index 866440a37b31..deffbe9e364f 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -6,7 +6,7 @@ jobs: directory_writer: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6 diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index 05adb43b0bd4..591b2163cc1a 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -21,7 +21,7 @@ jobs: libxml2-dev libxslt-dev libhdf5-dev libopenblas-dev - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: @@ -39,7 +39,7 @@ jobs: libxml2-dev libxslt-dev libhdf5-dev libopenblas-dev - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index b17236ccbff9..13df19c8d743 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -11,6 +11,6 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - run: uvx ruff check --output-format=github . diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index b945869e84a7..bf0a74a239c8 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -32,7 +32,7 @@ jobs: libxml2-dev libxslt-dev libhdf5-dev libopenblas-dev - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: From 8934babb34b60d94c1a3b6a0409e2942d791c35a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:08:41 +0100 Subject: [PATCH 32/76] [pre-commit.ci] pre-commit autoupdate (#13979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.3 → v0.14.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.7) - [github.com/tox-dev/pyproject-fmt: v2.11.0 → v2.11.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.0...v2.11.1) - [github.com/pre-commit/mirrors-mypy: v1.18.2 → v1.19.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.18.2...v1.19.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e305772298d3..57f92ce941d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.7 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.11.0 + rev: v2.11.1 hooks: - id: pyproject-fmt @@ -50,7 +50,7 @@ repos: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy args: From 2c15b8c54eb8130e83640fe1d911c10eb6cd70d4 Mon Sep 17 00:00:00 2001 From: Alan718 <75001847+shuhao-alan-fan@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:15:31 -0500 Subject: [PATCH 33/76] [Searches] Fix Binary Search bug with duplicate elements (#13946) * Fix binary search with duplicates issue #13886 * Add docstrings to binary search functions Added docstrings for lower_bound and upper_bound functions. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update searches/binary_search.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor docstrings for lower_bound and upper_bound Updated docstring parameter and return type annotations for lower_bound and upper_bound functions. * Update searches/binary_search.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: John Law --- searches/binary_search.py | 75 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/searches/binary_search.py b/searches/binary_search.py index 2e66b672d5b4..5125dc6bdb9a 100644 --- a/searches/binary_search.py +++ b/searches/binary_search.py @@ -243,6 +243,81 @@ def binary_search_std_lib(sorted_collection: list[int], item: int) -> int: return -1 +def binary_search_with_duplicates(sorted_collection: list[int], item: int) -> list[int]: + """Pure implementation of a binary search algorithm in Python that supports + duplicates. + + Resources used: + https://stackoverflow.com/questions/13197552/using-binary-search-with-sorted-array-with-duplicates + + The collection must be sorted in ascending order; otherwise the result will be + unpredictable. If the target appears multiple times, this function returns a + list of all indexes where the target occurs. If the target is not found, + this function returns an empty list. + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item value to search for + :return: a list of indexes where the item is found (empty list if not found) + + Examples: + >>> binary_search_with_duplicates([0, 5, 7, 10, 15], 0) + [0] + >>> binary_search_with_duplicates([0, 5, 7, 10, 15], 15) + [4] + >>> binary_search_with_duplicates([1, 2, 2, 2, 3], 2) + [1, 2, 3] + >>> binary_search_with_duplicates([1, 2, 2, 2, 3], 4) + [] + """ + if list(sorted_collection) != sorted(sorted_collection): + raise ValueError("sorted_collection must be sorted in ascending order") + + def lower_bound(sorted_collection: list[int], item: int) -> int: + """ + Returns the index of the first element greater than or equal to the item. + + :param sorted_collection: The sorted list to search. + :param item: The item to find the lower bound for. + :return: The index where the item can be inserted while maintaining order. + """ + left = 0 + right = len(sorted_collection) + while left < right: + midpoint = left + (right - left) // 2 + current_item = sorted_collection[midpoint] + if current_item < item: + left = midpoint + 1 + else: + right = midpoint + return left + + def upper_bound(sorted_collection: list[int], item: int) -> int: + """ + Returns the index of the first element strictly greater than the item. + + :param sorted_collection: The sorted list to search. + :param item: The item to find the upper bound for. + :return: The index where the item can be inserted after all existing instances. + """ + left = 0 + right = len(sorted_collection) + while left < right: + midpoint = left + (right - left) // 2 + current_item = sorted_collection[midpoint] + if current_item <= item: + left = midpoint + 1 + else: + right = midpoint + return left + + left = lower_bound(sorted_collection, item) + right = upper_bound(sorted_collection, item) + + if left == len(sorted_collection) or sorted_collection[left] != item: + return [] + return list(range(left, right)) + + def binary_search_by_recursion( sorted_collection: list[int], item: int, left: int = 0, right: int = -1 ) -> int: From 68f876afef05a2eadd546135004b218b4c14eec2 Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:03:10 -0800 Subject: [PATCH 34/76] Add narcissistic number finder with dynamic programming (#13971) * Add narcissistic number finder with dynamic programming * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update dynamic_programming/narcissistic_number.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dynamic_programming/narcissistic_number.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dynamic_programming/narcissistic_number.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dynamic_programming/narcissistic_number.py | 103 +++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 dynamic_programming/narcissistic_number.py diff --git a/dynamic_programming/narcissistic_number.py b/dynamic_programming/narcissistic_number.py new file mode 100644 index 000000000000..dc1c6f5a5660 --- /dev/null +++ b/dynamic_programming/narcissistic_number.py @@ -0,0 +1,103 @@ +""" +Find all narcissistic numbers up to a given limit using dynamic programming. + +A narcissistic number (also known as an Armstrong number or plus perfect number) +is a number that is the sum of its own digits each raised to the power of the +number of digits. + +For example, 153 is a narcissistic number because 153 = 1^3 + 5^3 + 3^3. + +This implementation uses dynamic programming with memoization to efficiently +compute digit powers and find all narcissistic numbers up to a specified limit. + +The DP optimization caches digit^power calculations. When searching through many +numbers, the same digit power calculations occur repeatedly (e.g., 153, 351, 135 +all need 1^3, 5^3, 3^3). Memoization avoids these redundant calculations. + +Examples of narcissistic numbers: + Single digit: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + Three digit: 153, 370, 371, 407 + Four digit: 1634, 8208, 9474 + Five digit: 54748, 92727, 93084 + +Reference: https://en.wikipedia.org/wiki/Narcissistic_number +""" + + +def find_narcissistic_numbers(limit: int) -> list[int]: + """ + Find all narcissistic numbers up to the given limit using dynamic programming. + + This function uses memoization to cache digit power calculations, avoiding + redundant computations across different numbers with the same digit count. + + Args: + limit: The upper bound for searching narcissistic numbers (exclusive) + + Returns: + list[int]: A sorted list of all narcissistic numbers below the limit + + Examples: + >>> find_narcissistic_numbers(10) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + >>> find_narcissistic_numbers(160) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153] + >>> find_narcissistic_numbers(400) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371] + >>> find_narcissistic_numbers(1000) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407] + >>> find_narcissistic_numbers(10000) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407, 1634, 8208, 9474] + >>> find_narcissistic_numbers(1) + [0] + >>> find_narcissistic_numbers(0) + [] + """ + if limit <= 0: + return [] + + narcissistic_nums = [] + + # Memoization: cache[(power, digit)] = digit^power + # This avoids recalculating the same power for different numbers + power_cache: dict[tuple[int, int], int] = {} + + def get_digit_power(digit: int, power: int) -> int: + """Get digit^power using memoization (DP optimization).""" + if (power, digit) not in power_cache: + power_cache[(power, digit)] = digit**power + return power_cache[(power, digit)] + + # Check each number up to the limit + for number in range(limit): + # Count digits + num_digits = len(str(number)) + + # Calculate sum of powered digits using memoized powers + remaining = number + digit_sum = 0 + while remaining > 0: + digit = remaining % 10 + digit_sum += get_digit_power(digit, num_digits) + remaining //= 10 + + # Check if narcissistic + if digit_sum == number: + narcissistic_nums.append(number) + + return narcissistic_nums + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Demonstrate the dynamic programming approach + print("Finding all narcissistic numbers up to 10000:") + print("(Using memoization to cache digit power calculations)") + print() + + narcissistic_numbers = find_narcissistic_numbers(10000) + print(f"Found {len(narcissistic_numbers)} narcissistic numbers:") + print(narcissistic_numbers) From 3c887358e59bcfa0c1cc6df840e6781d9a8efac9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:57:27 +0100 Subject: [PATCH 35/76] [pre-commit.ci] pre-commit autoupdate (#14078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.10) - [github.com/pre-commit/mirrors-mypy: v1.19.0 → v1.19.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.19.0...v1.19.1) * updating DIRECTORY.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] --- .pre-commit-config.yaml | 4 ++-- DIRECTORY.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57f92ce941d9..75e43225f588 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.7 + rev: v0.14.10 hooks: - id: ruff-check - id: ruff-format @@ -50,7 +50,7 @@ repos: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.19.1 hooks: - id: mypy args: diff --git a/DIRECTORY.md b/DIRECTORY.md index 0f9859577493..29dde5b106cb 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -398,6 +398,7 @@ * [Minimum Squares To Represent A Number](dynamic_programming/minimum_squares_to_represent_a_number.py) * [Minimum Steps To One](dynamic_programming/minimum_steps_to_one.py) * [Minimum Tickets Cost](dynamic_programming/minimum_tickets_cost.py) + * [Narcissistic Number](dynamic_programming/narcissistic_number.py) * [Optimal Binary Search Tree](dynamic_programming/optimal_binary_search_tree.py) * [Palindrome Partitioning](dynamic_programming/palindrome_partitioning.py) * [Range Sum Query](dynamic_programming/range_sum_query.py) From ca5b8c156be42b7033c8849b570a0a59b77b8eab Mon Sep 17 00:00:00 2001 From: Parag Sharma Date: Wed, 21 Jan 2026 21:31:41 +0530 Subject: [PATCH 36/76] Add sliding window maximum using monotonic deque (#14133) * feat: add sliding window maximum using monotonic deque * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: add sliding window maximum using monotonic deque --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- other/sliding_window_maximum.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 other/sliding_window_maximum.py diff --git a/other/sliding_window_maximum.py b/other/sliding_window_maximum.py new file mode 100644 index 000000000000..1c2c3c8e37e6 --- /dev/null +++ b/other/sliding_window_maximum.py @@ -0,0 +1,58 @@ +from collections import deque + + +def sliding_window_maximum(numbers: list[int], window_size: int) -> list[int]: + """ + Return a list containing the maximum of each sliding window of size window_size. + + This implementation uses a monotonic deque to achieve O(n) time complexity. + + Args: + numbers: List of integers representing the input array. + window_size: Size of the sliding window (must be positive). + + Returns: + List of maximum values for each valid window. + + Raises: + ValueError: If window_size is not a positive integer. + + Time Complexity: O(n) - each element is added and removed at most once + Space Complexity: O(k) - deque stores at most window_size indices + + Examples: + >>> sliding_window_maximum([1, 3, -1, -3, 5, 3, 6, 7], 3) + [3, 3, 5, 5, 6, 7] + >>> sliding_window_maximum([9, 11], 2) + [11] + >>> sliding_window_maximum([], 3) + [] + >>> sliding_window_maximum([4, 2, 12, 3], 1) + [4, 2, 12, 3] + >>> sliding_window_maximum([1], 1) + [1] + """ + if window_size <= 0: + raise ValueError("Window size must be a positive integer") + if not numbers: + return [] + + result: list[int] = [] + index_deque: deque[int] = deque() + + for current_index, current_value in enumerate(numbers): + # Remove the element which is out of this window + if index_deque and index_deque[0] == current_index - window_size: + index_deque.popleft() + + # Remove useless elements (smaller than current) from back + while index_deque and numbers[index_deque[-1]] < current_value: + index_deque.pop() + + index_deque.append(current_index) + + # Start adding to result once we have a full window + if current_index >= window_size - 1: + result.append(numbers[index_deque[0]]) + + return result From 8fa4161587ab931adb65392a2bcd6bebe5e061f3 Mon Sep 17 00:00:00 2001 From: Tithi Joshi Date: Sun, 25 Jan 2026 19:24:15 +0530 Subject: [PATCH 37/76] docs: improve docstring clarity in reverse_words (#14212) Updated function name and docstring for clarity. --- strings/reverse_words.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/strings/reverse_words.py b/strings/reverse_words.py index 504c1c2089dd..bee237f2a2d9 100644 --- a/strings/reverse_words.py +++ b/strings/reverse_words.py @@ -1,12 +1,14 @@ -def reverse_words(input_str: str) -> str: - """ - Reverses words in a given string +def reverse_words(sentence: str) -> str: + """Reverse the order of words in a given string. + + Extra whitespace between words is ignored. + >>> reverse_words("I love Python") 'Python love I' >>> reverse_words("I Love Python") 'Python Love I' """ - return " ".join(input_str.split()[::-1]) + return " ".join(sentence.split()[::-1]) if __name__ == "__main__": From 8106aea67f0248479908eed313911c23c631b6c7 Mon Sep 17 00:00:00 2001 From: Parth Pawar Date: Sun, 25 Jan 2026 19:33:25 +0530 Subject: [PATCH 38/76] Correct typo from 'two large' to 'too large' (#14135) Fix typo in documentation regarding shift size. --- ciphers/caesar_cipher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ciphers/caesar_cipher.py b/ciphers/caesar_cipher.py index 1cf4d67cbaed..ef5f49313ee7 100644 --- a/ciphers/caesar_cipher.py +++ b/ciphers/caesar_cipher.py @@ -45,7 +45,7 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str: And our shift is ``2`` We can then encode the message, one letter at a time. ``H`` would become ``J``, - since ``J`` is two letters away, and so on. If the shift is ever two large, or + since ``J`` is two letters away, and so on. If the shift is ever too large, or our letter is at the end of the alphabet, we just start at the beginning (``Z`` would shift to ``a`` then ``b`` and so on). From 678dedbbf94be54b3c9c258368e28bb8e7736d62 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:20:07 +0100 Subject: [PATCH 39/76] [pre-commit.ci] pre-commit autoupdate (#14238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/MarcoGorelli/auto-walrus: 0.3.4 → 0.4.1](https://github.com/MarcoGorelli/auto-walrus/compare/0.3.4...0.4.1) - [github.com/astral-sh/ruff-pre-commit: v0.14.10 → v0.14.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.10...v0.14.14) - [github.com/tox-dev/pyproject-fmt: v2.11.1 → v2.12.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.11.1...v2.12.1) * updating DIRECTORY.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] --- .pre-commit-config.yaml | 6 +++--- DIRECTORY.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75e43225f588..765d5cff38d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,12 +14,12 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/MarcoGorelli/auto-walrus - rev: 0.3.4 + rev: 0.4.1 hooks: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 + rev: v0.14.14 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.11.1 + rev: v2.12.1 hooks: - id: pyproject-fmt diff --git a/DIRECTORY.md b/DIRECTORY.md index 29dde5b106cb..a73c630bc8a7 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -881,6 +881,7 @@ * [Quine](other/quine.py) * [Scoring Algorithm](other/scoring_algorithm.py) * [Sdes](other/sdes.py) + * [Sliding Window Maximum](other/sliding_window_maximum.py) * [Tower Of Hanoi](other/tower_of_hanoi.py) * [Word Search](other/word_search.py) From ac05f1248c84f022f3f32df111554f04c36de77f Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:47:34 -0800 Subject: [PATCH 40/76] feat: add Jarvis March (Gift Wrapping) convex hull algorithm (#14225) * Add Jarvis March (Gift Wrapping) convex hull algorithm * Use descriptive parameter names per algorithms-keeper review * Update jarvis_march.py * Update jarvis_march.py * fix: add pytest marker * Update jarvis_march.py * feat: removed doctests and created a separate test file for CI to pass * Update jarvis_march_unit.py * Update jarvis_march.py * Update jarvis_march_unit.py * feat: added test folder with tests to pass CI checks * fix: duplicate points handled * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: fixed ruff errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../hashing/hash_table_with_linked_list.py | 2 +- geometry/jarvis_march.py | 187 ++++++++++++++++++ geometry/tests/__init__.py | 0 geometry/tests/test_jarvis_march.py | 115 +++++++++++ 4 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 geometry/jarvis_march.py create mode 100644 geometry/tests/__init__.py create mode 100644 geometry/tests/test_jarvis_march.py diff --git a/data_structures/hashing/hash_table_with_linked_list.py b/data_structures/hashing/hash_table_with_linked_list.py index f404c5251246..c8dffa30b8e8 100644 --- a/data_structures/hashing/hash_table_with_linked_list.py +++ b/data_structures/hashing/hash_table_with_linked_list.py @@ -8,7 +8,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _set_value(self, key, data): - self.values[key] = deque([]) if self.values[key] is None else self.values[key] + self.values[key] = deque() if self.values[key] is None else self.values[key] self.values[key].appendleft(data) self._keys[key] = self.values[key] diff --git a/geometry/jarvis_march.py b/geometry/jarvis_march.py new file mode 100644 index 000000000000..55a0872ff60e --- /dev/null +++ b/geometry/jarvis_march.py @@ -0,0 +1,187 @@ +""" +Jarvis March (Gift Wrapping) algorithm for finding the convex hull of a set of points. + +The convex hull is the smallest convex polygon that contains all the points. + +Time Complexity: O(n*h) where n is the number of points and h is the number of +hull points. +Space Complexity: O(h) where h is the number of hull points. + +USAGE: + -> Import this file into your project. + -> Use the jarvis_march() function to find the convex hull of a set of points. + -> Parameters: + -> points: A list of Point objects representing 2D coordinates + +REFERENCES: + -> Wikipedia reference: https://en.wikipedia.org/wiki/Gift_wrapping_algorithm + -> GeeksforGeeks: + https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/ +""" + +from __future__ import annotations + + +class Point: + """Represents a 2D point with x and y coordinates.""" + + def __init__(self, x_coordinate: float, y_coordinate: float) -> None: + self.x = x_coordinate + self.y = y_coordinate + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Point): + return NotImplemented + return self.x == other.x and self.y == other.y + + def __repr__(self) -> str: + return f"Point({self.x}, {self.y})" + + def __hash__(self) -> int: + return hash((self.x, self.y)) + + +def _cross_product(origin: Point, point_a: Point, point_b: Point) -> float: + """ + Calculate the cross product of vectors OA and OB. + + Returns: + > 0: Counter-clockwise turn (left turn) + = 0: Collinear + < 0: Clockwise turn (right turn) + """ + return (point_a.x - origin.x) * (point_b.y - origin.y) - (point_a.y - origin.y) * ( + point_b.x - origin.x + ) + + +def _is_point_on_segment(p1: Point, p2: Point, point: Point) -> bool: + """Check if a point lies on the line segment between p1 and p2.""" + # Check if point is collinear with segment endpoints + cross = (point.y - p1.y) * (p2.x - p1.x) - (point.x - p1.x) * (p2.y - p1.y) + + if abs(cross) > 1e-9: + return False + + # Check if point is within the bounding box of the segment + return min(p1.x, p2.x) <= point.x <= max(p1.x, p2.x) and min( + p1.y, p2.y + ) <= point.y <= max(p1.y, p2.y) + + +def _find_leftmost_point(points: list[Point]) -> int: + """Find index of leftmost point (and bottom-most in case of tie).""" + left_idx = 0 + for i in range(1, len(points)): + if points[i].x < points[left_idx].x or ( + points[i].x == points[left_idx].x and points[i].y < points[left_idx].y + ): + left_idx = i + return left_idx + + +def _find_next_hull_point(points: list[Point], current_idx: int) -> int: + """Find the next point on the convex hull.""" + next_idx = (current_idx + 1) % len(points) + # Ensure next_idx is not the same as current_idx + while next_idx == current_idx: + next_idx = (next_idx + 1) % len(points) + + for i in range(len(points)): + if i == current_idx: + continue + cross = _cross_product(points[current_idx], points[i], points[next_idx]) + if cross > 0: + next_idx = i + + return next_idx + + +def _is_valid_polygon(hull: list[Point]) -> bool: + """Check if hull forms a valid polygon (has at least one non-collinear turn).""" + for i in range(len(hull)): + p1 = hull[i] + p2 = hull[(i + 1) % len(hull)] + p3 = hull[(i + 2) % len(hull)] + if abs(_cross_product(p1, p2, p3)) > 1e-9: + return True + return False + + +def _add_point_to_hull(hull: list[Point], point: Point) -> None: + """Add a point to hull, removing collinear intermediate points.""" + last = len(hull) - 1 + if len(hull) > 1 and _is_point_on_segment(hull[last - 1], hull[last], point): + hull[last] = Point(point.x, point.y) + else: + hull.append(Point(point.x, point.y)) + + +def jarvis_march(points: list[Point]) -> list[Point]: + """ + Find the convex hull of a set of points using the Jarvis March algorithm. + + The algorithm starts with the leftmost point and wraps around the set of + points, selecting the most counter-clockwise point at each step. + + Args: + points: List of Point objects representing 2D coordinates + + Returns: + List of Points that form the convex hull in counter-clockwise order. + Returns empty list if there are fewer than 3 non-collinear points. + """ + if len(points) <= 2: + return [] + + # Remove duplicate points to avoid infinite loops + unique_points = list(set(points)) + + if len(unique_points) <= 2: + return [] + + convex_hull: list[Point] = [] + + # Find the leftmost point + left_point_idx = _find_leftmost_point(unique_points) + convex_hull.append( + Point(unique_points[left_point_idx].x, unique_points[left_point_idx].y) + ) + + current_idx = left_point_idx + while True: + # Find the next counter-clockwise point + next_idx = _find_next_hull_point(unique_points, current_idx) + + if next_idx == left_point_idx: + break + + if next_idx == current_idx: + break + + current_idx = next_idx + _add_point_to_hull(convex_hull, unique_points[current_idx]) + + # Check for degenerate cases + if len(convex_hull) <= 2: + return [] + + # Check if last point is collinear with first and second-to-last + last = len(convex_hull) - 1 + if _is_point_on_segment(convex_hull[last - 1], convex_hull[last], convex_hull[0]): + convex_hull.pop() + if len(convex_hull) == 2: + return [] + + # Verify the hull forms a valid polygon + if not _is_valid_polygon(convex_hull): + return [] + + return convex_hull + + +if __name__ == "__main__": + # Example usage + points = [Point(0, 0), Point(1, 1), Point(0, 1), Point(1, 0), Point(0.5, 0.5)] + hull = jarvis_march(points) + print(f"Convex hull: {hull}") diff --git a/geometry/tests/__init__.py b/geometry/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/geometry/tests/test_jarvis_march.py b/geometry/tests/test_jarvis_march.py new file mode 100644 index 000000000000..6e7defe414a3 --- /dev/null +++ b/geometry/tests/test_jarvis_march.py @@ -0,0 +1,115 @@ +""" +Unit tests for Jarvis March (Gift Wrapping) algorithm. +""" + +from geometry.jarvis_march import Point, jarvis_march + + +class TestPoint: + """Tests for the Point class.""" + + def test_point_creation(self) -> None: + """Test Point initialization.""" + p = Point(1.0, 2.0) + assert p.x == 1.0 + assert p.y == 2.0 + + def test_point_equality(self) -> None: + """Test Point equality comparison.""" + p1 = Point(1.0, 2.0) + p2 = Point(1.0, 2.0) + p3 = Point(2.0, 1.0) + assert p1 == p2 + assert p1 != p3 + + def test_point_repr(self) -> None: + """Test Point string representation.""" + p = Point(1.5, 2.5) + assert repr(p) == "Point(1.5, 2.5)" + + def test_point_hash(self) -> None: + """Test Point hashing.""" + p1 = Point(1.0, 2.0) + p2 = Point(1.0, 2.0) + assert hash(p1) == hash(p2) + + +class TestJarvisMarch: + """Tests for the jarvis_march function.""" + + def test_triangle(self) -> None: + """Test convex hull of a triangle.""" + p1, p2, p3 = Point(1, 1), Point(2, 1), Point(1.5, 2) + hull = jarvis_march([p1, p2, p3]) + assert len(hull) == 3 + assert all(p in hull for p in [p1, p2, p3]) + + def test_collinear_points(self) -> None: + """Test that collinear points return empty hull.""" + points = [Point(i, 0) for i in range(5)] + hull = jarvis_march(points) + assert hull == [] + + def test_rectangle_with_interior_point(self) -> None: + """Test rectangle with interior point - interior point excluded.""" + p1, p2 = Point(1, 1), Point(2, 1) + p3, p4 = Point(2, 2), Point(1, 2) + p5 = Point(1.5, 1.5) + hull = jarvis_march([p1, p2, p3, p4, p5]) + assert len(hull) == 4 + assert p5 not in hull + + def test_star_shape(self) -> None: + """Test star shape - only tips are in hull.""" + tips = [ + Point(-5, 6), + Point(-11, 0), + Point(-9, -8), + Point(4, 4), + Point(6, -7), + ] + interior = [Point(-7, -2), Point(-2, -4), Point(0, 1)] + hull = jarvis_march(tips + interior) + assert len(hull) == 5 + assert all(p in hull for p in tips) + assert not any(p in hull for p in interior) + + def test_empty_list(self) -> None: + """Test empty list returns empty hull.""" + assert jarvis_march([]) == [] + + def test_single_point(self) -> None: + """Test single point returns empty hull.""" + assert jarvis_march([Point(0, 0)]) == [] + + def test_two_points(self) -> None: + """Test two points return empty hull.""" + assert jarvis_march([Point(0, 0), Point(1, 1)]) == [] + + def test_square(self) -> None: + """Test convex hull of a square.""" + p1, p2 = Point(0, 0), Point(1, 0) + p3, p4 = Point(1, 1), Point(0, 1) + hull = jarvis_march([p1, p2, p3, p4]) + assert len(hull) == 4 + assert all(p in hull for p in [p1, p2, p3, p4]) + + def test_duplicate_points(self) -> None: + """Test handling of duplicate points.""" + p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) + points = [p1, p2, p3, p1, p2] # Include duplicates + hull = jarvis_march(points) + assert len(hull) == 3 + + def test_pentagon(self) -> None: + """Test convex hull of a pentagon.""" + points = [ + Point(0, 1), + Point(1, 2), + Point(2, 1), + Point(1.5, 0), + Point(0.5, 0), + ] + hull = jarvis_march(points) + assert len(hull) == 5 + assert all(p in hull for p in points) From e333ddb852f58c9083c7ef1180a4be9473f436dd Mon Sep 17 00:00:00 2001 From: John Law Date: Sat, 7 Mar 2026 20:29:05 +0000 Subject: [PATCH 41/76] chore: Fix ruff build failures (#14347) --- machine_learning/linear_discriminant_analysis.py | 6 +----- searches/jump_search.py | 7 ++----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/machine_learning/linear_discriminant_analysis.py b/machine_learning/linear_discriminant_analysis.py index 8528ccbbae51..de2d1de46ba1 100644 --- a/machine_learning/linear_discriminant_analysis.py +++ b/machine_learning/linear_discriminant_analysis.py @@ -47,7 +47,6 @@ from math import log from os import name, system from random import gauss, seed -from typing import TypeVar # Make a training dataset drawn from a gaussian distribution @@ -249,10 +248,7 @@ def accuracy(actual_y: list, predicted_y: list) -> float: return (correct / len(actual_y)) * 100 -num = TypeVar("num") - - -def valid_input( +def valid_input[num]( input_type: Callable[[object], num], # Usually float or int input_msg: str, err_msg: str, diff --git a/searches/jump_search.py b/searches/jump_search.py index e72d85e8a868..437faf306bb2 100644 --- a/searches/jump_search.py +++ b/searches/jump_search.py @@ -10,17 +10,14 @@ import math from collections.abc import Sequence -from typing import Any, Protocol, TypeVar +from typing import Any, Protocol class Comparable(Protocol): def __lt__(self, other: Any, /) -> bool: ... -T = TypeVar("T", bound=Comparable) - - -def jump_search(arr: Sequence[T], item: T) -> int: +def jump_search[T: Comparable](arr: Sequence[T], item: T) -> int: """ Python implementation of the jump search algorithm. Return the index if the `item` is found, otherwise return -1. From f527d43d6d07a68c1b4691a21b5a316810bb3cb8 Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:35:52 -0800 Subject: [PATCH 42/76] feat: add Graham Scan convex hull algorithm (#14251) * feat: add Graham Scan convex hull algorithm * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: address pre-commit issues * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore: Algorithm-Keeper's comments addressed --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: John Law Co-authored-by: Christian Clauss --- geometry/graham_scan.py | 246 ++++++++++++++++++++++++++ geometry/tests/test_graham_scan.py | 266 +++++++++++++++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 geometry/graham_scan.py create mode 100644 geometry/tests/test_graham_scan.py diff --git a/geometry/graham_scan.py b/geometry/graham_scan.py new file mode 100644 index 000000000000..a48391dfbc5d --- /dev/null +++ b/geometry/graham_scan.py @@ -0,0 +1,246 @@ +""" +Graham Scan algorithm for finding the convex hull of a set of points. + +The Graham scan is a method of computing the convex hull of a finite set of points +in the plane with time complexity O(n log n). It is named after Ronald Graham, who +published the original algorithm in 1972. + +The algorithm finds all vertices of the convex hull ordered along its boundary. +It uses a stack to efficiently identify and remove points that would create +non-convex angles. + +References: +- https://en.wikipedia.org/wiki/Graham_scan +- Graham, R.L. (1972). "An Efficient Algorithm for Determining the Convex Hull of a + Finite Planar Set" +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import TypeVar + +T = TypeVar("T", bound="Point") + + +@dataclass +class Point: + """ + A point in 2D space. + + >>> Point(0, 0) + Point(x=0.0, y=0.0) + >>> Point(1.5, 2.5) + Point(x=1.5, y=2.5) + """ + + x: float + y: float + + def __init__(self, x_coordinate: float, y_coordinate: float) -> None: + """ + Initialize a 2D point. + + Args: + x_coordinate: The x-coordinate (horizontal position) of the point + y_coordinate: The y-coordinate (vertical position) of the point + """ + self.x = float(x_coordinate) + self.y = float(y_coordinate) + + def __eq__(self, other: object) -> bool: + """ + Check if two points are equal. + + >>> Point(1, 2) == Point(1, 2) + True + >>> Point(1, 2) == Point(2, 1) + False + """ + if not isinstance(other, Point): + return NotImplemented + return self.x == other.x and self.y == other.y + + def __lt__(self, other: Point) -> bool: + """ + Compare two points for sorting (bottom-most, then left-most). + + >>> Point(1, 2) < Point(1, 3) + True + >>> Point(1, 2) < Point(2, 2) + True + >>> Point(2, 2) < Point(1, 2) + False + """ + if self.y == other.y: + return self.x < other.x + return self.y < other.y + + def euclidean_distance(self, other: Point) -> float: + """ + Calculate Euclidean distance between two points. + + >>> Point(0, 0).euclidean_distance(Point(3, 4)) + 5.0 + >>> Point(1, 1).euclidean_distance(Point(4, 5)) + 5.0 + """ + return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5 + + def consecutive_orientation(self, point_a: Point, point_b: Point) -> float: + """ + Calculate the cross product of vectors (self -> point_a) and + (point_a -> point_b). + + Returns: + - Positive value: counter-clockwise turn + - Negative value: clockwise turn + - Zero: collinear points + + >>> Point(0, 0).consecutive_orientation(Point(1, 0), Point(1, 1)) + 1.0 + >>> Point(0, 0).consecutive_orientation(Point(1, 0), Point(1, -1)) + -1.0 + >>> Point(0, 0).consecutive_orientation(Point(1, 0), Point(2, 0)) + 0.0 + """ + return (point_a.x - self.x) * (point_b.y - point_a.y) - (point_a.y - self.y) * ( + point_b.x - point_a.x + ) + + +def graham_scan(points: Sequence[Point]) -> list[Point]: + """ + Find the convex hull of a set of points using the Graham scan algorithm. + + The algorithm works as follows: + 1. Find the bottom-most point (or left-most in case of tie) + 2. Sort all other points by polar angle with respect to the bottom-most point + 3. Process points in order, maintaining a stack of hull candidates + 4. Remove points that would create a clockwise turn + + Args: + points: A sequence of Point objects + + Returns: + A list of Point objects representing the convex hull in counter-clockwise order. + Returns an empty list if there are fewer than 3 distinct points or if all + points are collinear. + + Time Complexity: O(n log n) due to sorting + Space Complexity: O(n) for the output hull + + >>> graham_scan([]) + [] + >>> graham_scan([Point(0, 0)]) + [] + >>> graham_scan([Point(0, 0), Point(1, 1)]) + [] + >>> hull = graham_scan([Point(0, 0), Point(1, 0), Point(0.5, 1)]) + >>> len(hull) + 3 + >>> Point(0, 0) in hull and Point(1, 0) in hull and Point(0.5, 1) in hull + True + """ + if len(points) <= 2: + return [] + + # Find the bottom-most point (left-most in case of tie) + min_point = min(points) + + # Remove the min_point from the list + points_list = [p for p in points if p != min_point] + if not points_list: + # Edge case where all points are the same + return [] + + def polar_angle_key(point: Point) -> tuple[float, float, float]: + """ + Key function for sorting points by polar angle relative to min_point. + + Points are sorted counter-clockwise. When two points have the same angle, + the farther point comes first (we'll remove duplicates later). + """ + # We use a dummy third point (min_point itself) to calculate relative angles + # Instead, we'll compute the angle between points + dx = point.x - min_point.x + dy = point.y - min_point.y + + # Use atan2 for angle, but we can also use cross product for comparison + # For sorting, we compare orientations between consecutive points + distance = min_point.euclidean_distance(point) + return (dx, dy, -distance) # Negative distance to sort farther points first + + # Sort by polar angle using a comparison based on cross product + def compare_points(point_a: Point, point_b: Point) -> int: + """Compare two points by polar angle relative to min_point.""" + orientation = min_point.consecutive_orientation(point_a, point_b) + if orientation < 0.0: + return 1 # point_a comes after point_b (clockwise) + elif orientation > 0.0: + return -1 # point_a comes before point_b (counter-clockwise) + else: + # Collinear: farther point should come first + dist_a = min_point.euclidean_distance(point_a) + dist_b = min_point.euclidean_distance(point_b) + if dist_b < dist_a: + return -1 + elif dist_b > dist_a: + return 1 + else: + return 0 + + from functools import cmp_to_key + + points_list.sort(key=cmp_to_key(compare_points)) + + # Build the convex hull + convex_hull: list[Point] = [min_point, points_list[0]] + + for point in points_list[1:]: + # Skip consecutive points with the same angle (collinear with min_point) + if min_point.consecutive_orientation(point, convex_hull[-1]) == 0.0: + continue + + # Remove points that create a clockwise turn (or are collinear) + while len(convex_hull) >= 2: + orientation = convex_hull[-2].consecutive_orientation( + convex_hull[-1], point + ) + if orientation <= 0.0: + convex_hull.pop() + else: + break + + convex_hull.append(point) + + # Need at least 3 points for a valid convex hull + if len(convex_hull) <= 2: + return [] + + return convex_hull + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Example usage + points = [ + Point(0, 0), + Point(1, 0), + Point(2, 0), + Point(2, 1), + Point(2, 2), + Point(1, 2), + Point(0, 2), + Point(0, 1), + Point(1, 1), # Interior point + ] + + hull = graham_scan(points) + print("Convex hull vertices:") + for point in hull: + print(f" ({point.x}, {point.y})") diff --git a/geometry/tests/test_graham_scan.py b/geometry/tests/test_graham_scan.py new file mode 100644 index 000000000000..d9a573289ce9 --- /dev/null +++ b/geometry/tests/test_graham_scan.py @@ -0,0 +1,266 @@ +""" +Tests for the Graham scan convex hull algorithm. +""" + +from geometry.graham_scan import Point, graham_scan + + +def test_empty_points() -> None: + """Test with no points.""" + assert graham_scan([]) == [] + + +def test_single_point() -> None: + """Test with a single point.""" + assert graham_scan([Point(0, 0)]) == [] + + +def test_two_points() -> None: + """Test with two points.""" + assert graham_scan([Point(0, 0), Point(1, 1)]) == [] + + +def test_duplicate_points() -> None: + """Test with all duplicate points.""" + p = Point(0, 0) + points = [p, Point(0, 0), Point(0, 0), Point(0, 0), Point(0, 0)] + assert graham_scan(points) == [] + + +def test_collinear_points() -> None: + """Test with all points on the same line.""" + points = [ + Point(1, 0), + Point(2, 0), + Point(3, 0), + Point(4, 0), + Point(5, 0), + ] + assert graham_scan(points) == [] + + +def test_triangle() -> None: + """Test with a triangle (3 points).""" + p1 = Point(1, 1) + p2 = Point(2, 1) + p3 = Point(1.5, 2) + points = [p1, p2, p3] + hull = graham_scan(points) + + assert len(hull) == 3 + assert p1 in hull + assert p2 in hull + assert p3 in hull + + +def test_rectangle() -> None: + """Test with a rectangle (4 points).""" + p1 = Point(1, 1) + p2 = Point(2, 1) + p3 = Point(2, 2) + p4 = Point(1, 2) + points = [p1, p2, p3, p4] + hull = graham_scan(points) + + assert len(hull) == 4 + assert all(p in hull for p in points) + + +def test_triangle_with_interior_points() -> None: + """Test triangle with points inside.""" + p1 = Point(1, 1) + p2 = Point(2, 1) + p3 = Point(1.5, 2) + p4 = Point(1.5, 1.5) # Interior + p5 = Point(1.2, 1.3) # Interior + p6 = Point(1.8, 1.2) # Interior + p7 = Point(1.5, 1.9) # Interior + + hull_points = [p1, p2, p3] + interior_points = [p4, p5, p6, p7] + all_points = hull_points + interior_points + + hull = graham_scan(all_points) + + # All hull points should be in the result + for p in hull_points: + assert p in hull + + # No interior points should be in the result + for p in interior_points: + assert p not in hull + + +def test_rectangle_with_interior_points() -> None: + """Test rectangle with points inside.""" + p1 = Point(1, 1) + p2 = Point(2, 1) + p3 = Point(2, 2) + p4 = Point(1, 2) + p5 = Point(1.5, 1.5) # Interior + p6 = Point(1.2, 1.3) # Interior + p7 = Point(1.8, 1.2) # Interior + p8 = Point(1.9, 1.7) # Interior + p9 = Point(1.4, 1.9) # Interior + + hull_points = [p1, p2, p3, p4] + interior_points = [p5, p6, p7, p8, p9] + all_points = hull_points + interior_points + + hull = graham_scan(all_points) + + # All hull points should be in the result + for p in hull_points: + assert p in hull + + # No interior points should be in the result + for p in interior_points: + assert p not in hull + + +def test_star_shape() -> None: + """Test with a star shape where only tips are on the convex hull.""" + # Tips of the star (on convex hull) + p1 = Point(-5, 6) + p2 = Point(-11, 0) + p3 = Point(-9, -8) + p4 = Point(4, 4) + p5 = Point(6, -7) + + # Interior points (not on convex hull) + p6 = Point(-7, -2) + p7 = Point(-2, -4) + p8 = Point(0, 1) + p9 = Point(1, 0) + p10 = Point(-6, 1) + + hull_points = [p1, p2, p3, p4, p5] + interior_points = [p6, p7, p8, p9, p10] + all_points = hull_points + interior_points + + hull = graham_scan(all_points) + + # All hull points should be in the result + for p in hull_points: + assert p in hull + + # No interior points should be in the result + for p in interior_points: + assert p not in hull + + +def test_rectangle_with_collinear_points() -> None: + """Test rectangle with points on the edges (collinear with vertices).""" + p1 = Point(1, 1) + p2 = Point(2, 1) + p3 = Point(2, 2) + p4 = Point(1, 2) + p5 = Point(1.5, 1) # On edge p1-p2 + p6 = Point(1, 1.5) # On edge p1-p4 + p7 = Point(2, 1.5) # On edge p2-p3 + p8 = Point(1.5, 2) # On edge p3-p4 + + hull_points = [p1, p2, p3, p4] + edge_points = [p5, p6, p7, p8] + all_points = hull_points + edge_points + + hull = graham_scan(all_points) + + # All corner points should be in the result + for p in hull_points: + assert p in hull + + # Edge points should not be in the result (only corners) + for p in edge_points: + assert p not in hull + + +def test_point_equality() -> None: + """Test Point equality.""" + p1 = Point(1, 2) + p2 = Point(1, 2) + p3 = Point(2, 1) + + assert p1 == p2 + assert p1 != p3 + + +def test_point_comparison() -> None: + """Test Point comparison for sorting.""" + p1 = Point(1, 2) + p2 = Point(1, 3) + p3 = Point(2, 2) + + assert p1 < p2 # Lower y value + assert p1 < p3 # Same y, lower x + assert not p2 < p1 + + +def test_euclidean_distance() -> None: + """Test Euclidean distance calculation.""" + p1 = Point(0, 0) + p2 = Point(3, 4) + + assert p1.euclidean_distance(p2) == 5.0 + + +def test_consecutive_orientation() -> None: + """Test orientation calculation.""" + p1 = Point(0, 0) + p2 = Point(1, 0) + p3_ccw = Point(1, 1) # Counter-clockwise + p3_cw = Point(1, -1) # Clockwise + p3_collinear = Point(2, 0) # Collinear + + assert p1.consecutive_orientation(p2, p3_ccw) > 0 # Counter-clockwise + assert p1.consecutive_orientation(p2, p3_cw) < 0 # Clockwise + assert p1.consecutive_orientation(p2, p3_collinear) == 0 # Collinear + + +def test_large_hull() -> None: + """Test with a larger set of points.""" + # Create a circle of points + import math + + points = [] + for i in range(20): + angle = 2 * math.pi * i / 20 + x = math.cos(angle) + y = math.sin(angle) + points.append(Point(x, y)) + + # Add some interior points + points.append(Point(0, 0)) + points.append(Point(0.5, 0.5)) + points.append(Point(-0.3, 0.2)) + + hull = graham_scan(points) + + # The hull should contain the circle points but not the interior points + assert len(hull) >= 3 + assert Point(0, 0) not in hull + assert Point(0.5, 0.5) not in hull + assert Point(-0.3, 0.2) not in hull + + +def test_random_order() -> None: + """Test that point order doesn't affect the result.""" + p1 = Point(0, 0) + p2 = Point(4, 0) + p3 = Point(4, 3) + p4 = Point(0, 3) + p5 = Point(2, 1.5) # Interior + + # Try different orderings + order1 = [p1, p2, p3, p4, p5] + order2 = [p5, p4, p3, p2, p1] + order3 = [p3, p5, p1, p4, p2] + + hull1 = graham_scan(order1) + hull2 = graham_scan(order2) + hull3 = graham_scan(order3) + + # All should have the same points (though possibly in different order) + assert len(hull1) == len(hull2) == len(hull3) == 4 + assert {(p.x, p.y) for p in hull1} == {(p.x, p.y) for p in hull2} + assert {(p.x, p.y) for p in hull2} == {(p.x, p.y) for p in hull3} From 8e70e2e77bc534d09f655410fd1a8a38d2404928 Mon Sep 17 00:00:00 2001 From: zain-cs Date: Mon, 9 Mar 2026 07:40:39 +0500 Subject: [PATCH 43/76] Add type hints and improve generate_parentheses_iterative (#14324) * Add type hints and improve code quality for generate_parentheses_iterative * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix line length issue * Update generate_parentheses_iterative.py * Update generate_parentheses_iterative.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- backtracking/generate_parentheses_iterative.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/backtracking/generate_parentheses_iterative.py b/backtracking/generate_parentheses_iterative.py index 175941c7ae95..84c032f52dc4 100644 --- a/backtracking/generate_parentheses_iterative.py +++ b/backtracking/generate_parentheses_iterative.py @@ -1,11 +1,12 @@ -def generate_parentheses_iterative(length: int) -> list: +def generate_parentheses_iterative(length: int) -> list[str]: """ Generate all valid combinations of parentheses (Iterative Approach). The algorithm works as follows: 1. Initialize an empty list to store the combinations. 2. Initialize a stack to keep track of partial combinations. - 3. Start with empty string and push it onstack along with the counts of '(' and ')'. + 3. Start with empty string and push it on stack along with + the counts of '(' and ')'. 4. While the stack is not empty: a. Pop a partial combination and its open and close counts from the stack. b. If the combination length is equal to 2*length, add it to the result. @@ -34,8 +35,11 @@ def generate_parentheses_iterative(length: int) -> list: >>> generate_parentheses_iterative(0) [''] """ - result = [] - stack = [] + if length == 0: + return [""] + + result: list[str] = [] + stack: list[tuple[str, int, int]] = [] # Each element in stack is a tuple (current_combination, open_count, close_count) stack.append(("", 0, 0)) @@ -45,6 +49,7 @@ def generate_parentheses_iterative(length: int) -> list: if len(current_combination) == 2 * length: result.append(current_combination) + continue if open_count < length: stack.append((current_combination + "(", open_count + 1, close_count)) From 0324e6098d1e44e52b9a12efd64fb72264fef513 Mon Sep 17 00:00:00 2001 From: umutKaracelebi Date: Mon, 9 Mar 2026 06:18:15 +0300 Subject: [PATCH 44/76] style: add type hints to matrix_exponentiation.py (#14288) * style: add type hints to matrix_exponentiation.py * Refactor kth_permutation and fix linter errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Delete machine_learning/linear_discriminant_analysis.py * Revert "Delete machine_learning/linear_discriminant_analysis.py" This reverts commit de2964731113d240db9d4e5451e2a51b48cc6ace. * Update linear_discriminant_analysis.py * Update kth_lexicographic_permutation.py * Update matrix_exponentiation.py * Update matrix_exponentiation.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- maths/matrix_exponentiation.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/maths/matrix_exponentiation.py b/maths/matrix_exponentiation.py index 7cdac9d34674..15b0c96e0f07 100644 --- a/maths/matrix_exponentiation.py +++ b/maths/matrix_exponentiation.py @@ -11,7 +11,7 @@ class Matrix: - def __init__(self, arg): + def __init__(self, arg: list[list] | int) -> None: if isinstance(arg, list): # Initializes a matrix identical to the one provided. self.t = arg self.n = len(arg) @@ -19,7 +19,7 @@ def __init__(self, arg): self.n = arg self.t = [[0 for _ in range(self.n)] for _ in range(self.n)] - def __mul__(self, b): + def __mul__(self, b: Matrix) -> Matrix: matrix = Matrix(self.n) for i in range(self.n): for j in range(self.n): @@ -28,7 +28,7 @@ def __mul__(self, b): return matrix -def modular_exponentiation(a, b): +def modular_exponentiation(a: Matrix, b: int) -> Matrix: matrix = Matrix([[1, 0], [0, 1]]) while b > 0: if b & 1: @@ -38,7 +38,7 @@ def modular_exponentiation(a, b): return matrix -def fibonacci_with_matrix_exponentiation(n, f1, f2): +def fibonacci_with_matrix_exponentiation(n: int, f1: int, f2: int) -> int: """ Returns the nth number of the Fibonacci sequence that starts with f1 and f2 @@ -64,7 +64,7 @@ def fibonacci_with_matrix_exponentiation(n, f1, f2): return f2 * matrix.t[0][0] + f1 * matrix.t[0][1] -def simple_fibonacci(n, f1, f2): +def simple_fibonacci(n: int, f1: int, f2: int) -> int: """ Returns the nth number of the Fibonacci sequence that starts with f1 and f2 @@ -95,7 +95,7 @@ def simple_fibonacci(n, f1, f2): return f2 -def matrix_exponentiation_time(): +def matrix_exponentiation_time() -> float: setup = """ from random import randint from __main__ import fibonacci_with_matrix_exponentiation @@ -106,7 +106,7 @@ def matrix_exponentiation_time(): return exec_time -def simple_fibonacci_time(): +def simple_fibonacci_time() -> float: setup = """ from random import randint from __main__ import simple_fibonacci @@ -119,7 +119,7 @@ def simple_fibonacci_time(): return exec_time -def main(): +def main() -> None: matrix_exponentiation_time() simple_fibonacci_time() From 81fcb90f7b7f88bb4b9bbc1f85967b5f26840fda Mon Sep 17 00:00:00 2001 From: Aarav Arya <159852168+Aarav-Arya@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:32:52 -0400 Subject: [PATCH 45/76] Add type hints for bogo_sort.py (#14306) * Added type hints for bogo_sort * Update bogo_sort.py --------- Co-authored-by: Maxim Smolskiy --- sorts/bogo_sort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sorts/bogo_sort.py b/sorts/bogo_sort.py index 9c133f0d8a55..70785140ee5c 100644 --- a/sorts/bogo_sort.py +++ b/sorts/bogo_sort.py @@ -16,7 +16,7 @@ import random -def bogo_sort(collection): +def bogo_sort(collection: list) -> list: """Pure implementation of the bogosort algorithm in Python :param collection: some mutable ordered collection with heterogeneous comparable items inside @@ -30,7 +30,7 @@ def bogo_sort(collection): [-45, -5, -2] """ - def is_sorted(collection): + def is_sorted(collection: list) -> bool: for i in range(len(collection) - 1): if collection[i] > collection[i + 1]: return False From 84b59c878186b7ed9e713f7409ccbf0473e3bc0c Mon Sep 17 00:00:00 2001 From: Yaadhuu Date: Mon, 9 Mar 2026 09:38:51 +0530 Subject: [PATCH 46/76] Handle gcd(0, 0) edge case (#14215) * Use TypeError for non-string input in count_vowels * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix docstring and improve input validation in kth_lexicographic_permutation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Handle gcd(0, 0) edge case * Update kth_lexicographic_permutation.py * Update count_vowels.py * Update greatest_common_divisor.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- maths/greatest_common_divisor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/maths/greatest_common_divisor.py b/maths/greatest_common_divisor.py index a2174a8eb74a..1fc123fc2b14 100644 --- a/maths/greatest_common_divisor.py +++ b/maths/greatest_common_divisor.py @@ -30,6 +30,8 @@ def greatest_common_divisor(a: int, b: int) -> int: 3 >>> greatest_common_divisor(-3, -9) 3 + >>> greatest_common_divisor(0, 0) + 0 """ return abs(b) if a == 0 else greatest_common_divisor(b % a, a) @@ -50,6 +52,8 @@ def gcd_by_iterative(x: int, y: int) -> int: 1 >>> gcd_by_iterative(11, 37) 1 + >>> gcd_by_iterative(0, 0) + 0 """ while y: # --> when y=0 then loop will terminate and return x as final GCD. x, y = y, x % y From 6f9f4318af4e93a0f7eaa4a2c3dfe698e0d31f6f Mon Sep 17 00:00:00 2001 From: Kalyani Date: Mon, 9 Mar 2026 09:45:03 +0530 Subject: [PATCH 47/76] all "is" replaced with "==" (#14209) Co-authored-by: Maxim Smolskiy --- strings/palindrome.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/strings/palindrome.py b/strings/palindrome.py index e765207e5942..4df5639b0c49 100644 --- a/strings/palindrome.py +++ b/strings/palindrome.py @@ -15,14 +15,14 @@ "AB": False, } # Ensure our test data is valid -assert all((key == key[::-1]) is value for key, value in test_data.items()) +assert all((key == key[::-1]) == value for key, value in test_data.items()) def is_palindrome(s: str) -> bool: """ Return True if s is a palindrome otherwise return False. - >>> all(is_palindrome(key) is value for key, value in test_data.items()) + >>> all(is_palindrome(key) == value for key, value in test_data.items()) True """ @@ -41,7 +41,7 @@ def is_palindrome_traversal(s: str) -> bool: """ Return True if s is a palindrome otherwise return False. - >>> all(is_palindrome_traversal(key) is value for key, value in test_data.items()) + >>> all(is_palindrome_traversal(key) == value for key, value in test_data.items()) True """ end = len(s) // 2 @@ -60,7 +60,7 @@ def is_palindrome_recursive(s: str) -> bool: """ Return True if s is a palindrome otherwise return False. - >>> all(is_palindrome_recursive(key) is value for key, value in test_data.items()) + >>> all(is_palindrome_recursive(key) == value for key, value in test_data.items()) True """ if len(s) <= 1: @@ -75,14 +75,14 @@ def is_palindrome_slice(s: str) -> bool: """ Return True if s is a palindrome otherwise return False. - >>> all(is_palindrome_slice(key) is value for key, value in test_data.items()) + >>> all(is_palindrome_slice(key) == value for key, value in test_data.items()) True """ return s == s[::-1] def benchmark_function(name: str) -> None: - stmt = f"all({name}(key) is value for key, value in test_data.items())" + stmt = f"all({name}(key) == value for key, value in test_data.items())" setup = f"from __main__ import test_data, {name}" number = 500000 result = timeit(stmt=stmt, setup=setup, number=number) @@ -91,8 +91,8 @@ def benchmark_function(name: str) -> None: if __name__ == "__main__": for key, value in test_data.items(): - assert is_palindrome(key) is is_palindrome_recursive(key) - assert is_palindrome(key) is is_palindrome_slice(key) + assert is_palindrome(key) == is_palindrome_recursive(key) + assert is_palindrome(key) == is_palindrome_slice(key) print(f"{key:21} {value}") print("a man a plan a canal panama") From 049a34d62be6abe5ce5dcef9b7c75580c6fd3e8f Mon Sep 17 00:00:00 2001 From: Tithi Joshi Date: Mon, 9 Mar 2026 10:00:39 +0530 Subject: [PATCH 48/76] Refine docstring and simplify reverse_letters implementation (#14205) * docs: refine docstring and simplify reverse_letters implementation Updated the docstring for clarity and improved the logic for reversing words. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update reverse_letters.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- strings/reverse_letters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strings/reverse_letters.py b/strings/reverse_letters.py index 4f73f816b382..cd1b7832d066 100644 --- a/strings/reverse_letters.py +++ b/strings/reverse_letters.py @@ -1,7 +1,7 @@ def reverse_letters(sentence: str, length: int = 0) -> str: """ Reverse all words that are longer than the given length of characters in a sentence. - If unspecified, length is taken as 0 + If ``length`` is not specified, it defaults to 0. >>> reverse_letters("Hey wollef sroirraw", 3) 'Hey fellow warriors' @@ -13,7 +13,7 @@ def reverse_letters(sentence: str, length: int = 0) -> str: 'racecar' """ return " ".join( - "".join(word[::-1]) if len(word) > length else word for word in sentence.split() + word[::-1] if len(word) > length else word for word in sentence.split() ) From f5c3e7c80821512ee2d47a891b9636d436e76705 Mon Sep 17 00:00:00 2001 From: Anusha <135559258+Anusha-DeviE@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:07:31 +0530 Subject: [PATCH 49/76] Replace assert-based validation with explicit errors in modular_division (#14204) * Improve documentation for linear search algorithm * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Replace asserts with explicit validation in modular_division * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update linear_search.py --------- Co-authored-by: Anusha-DeviE Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- maths/modular_division.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/maths/modular_division.py b/maths/modular_division.py index 94f12b3e096e..ed4ae6ae8ce3 100644 --- a/maths/modular_division.py +++ b/maths/modular_division.py @@ -28,9 +28,13 @@ def modular_division(a: int, b: int, n: int) -> int: 4 """ - assert n > 1 - assert a > 0 - assert greatest_common_divisor(a, n) == 1 + if n <= 1: + raise ValueError("Modulus n must be greater than 1") + if a <= 0: + raise ValueError("Divisor a must be a positive integer") + if greatest_common_divisor(a, n) != 1: + raise ValueError("a and n must be coprime (gcd(a, n) = 1)") + (_d, _t, s) = extended_gcd(n, a) # Implemented below x = (b * s) % n return x From 1ae906a97bd1245107d61bba06558f9a35222b91 Mon Sep 17 00:00:00 2001 From: Shivang Arya Date: Mon, 9 Mar 2026 10:19:15 +0530 Subject: [PATCH 50/76] Fix incorrect doctest references in fibonacci functions (#14200) * Fix incorrect doctest references in fibonacci functions * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- maths/fibonacci.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/maths/fibonacci.py b/maths/fibonacci.py index 71ff479f9cc2..595233cf8446 100644 --- a/maths/fibonacci.py +++ b/maths/fibonacci.py @@ -91,15 +91,15 @@ def fib_iterative(n: int) -> list[int]: def fib_recursive(n: int) -> list[int]: """ Calculates the first n (0-indexed) Fibonacci numbers using recursion - >>> fib_iterative(0) + >>> fib_recursive(0) [0] - >>> fib_iterative(1) + >>> fib_recursive(1) [0, 1] - >>> fib_iterative(5) + >>> fib_recursive(5) [0, 1, 1, 2, 3, 5] - >>> fib_iterative(10) + >>> fib_recursive(10) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55] - >>> fib_iterative(-1) + >>> fib_recursive(-1) Traceback (most recent call last): ... ValueError: n is negative @@ -119,7 +119,7 @@ def fib_recursive_term(i: int) -> int: >>> fib_recursive_term(-1) Traceback (most recent call last): ... - Exception: n is negative + ValueError: n is negative """ if i < 0: raise ValueError("n is negative") @@ -135,15 +135,15 @@ def fib_recursive_term(i: int) -> int: def fib_recursive_cached(n: int) -> list[int]: """ Calculates the first n (0-indexed) Fibonacci numbers using recursion - >>> fib_iterative(0) + >>> fib_recursive_cached(0) [0] - >>> fib_iterative(1) + >>> fib_recursive_cached(1) [0, 1] - >>> fib_iterative(5) + >>> fib_recursive_cached(5) [0, 1, 1, 2, 3, 5] - >>> fib_iterative(10) + >>> fib_recursive_cached(10) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55] - >>> fib_iterative(-1) + >>> fib_recursive_cached(-1) Traceback (most recent call last): ... ValueError: n is negative @@ -176,7 +176,7 @@ def fib_memoization(n: int) -> list[int]: [0, 1, 1, 2, 3, 5] >>> fib_memoization(10) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55] - >>> fib_iterative(-1) + >>> fib_memoization(-1) Traceback (most recent call last): ... ValueError: n is negative From da6b9e9687148f6f99810f761ac59e45b5c1031c Mon Sep 17 00:00:00 2001 From: Yaadhuu Date: Mon, 9 Mar 2026 10:27:20 +0530 Subject: [PATCH 51/76] Use TypeError for non-string input in count_vowels (#14196) * Use TypeError for non-string input in count_vowels * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix docstring and improve input validation in kth_lexicographic_permutation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update kth_lexicographic_permutation.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- strings/count_vowels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strings/count_vowels.py b/strings/count_vowels.py index 8a52b331c81b..e222d80590ff 100644 --- a/strings/count_vowels.py +++ b/strings/count_vowels.py @@ -22,7 +22,7 @@ def count_vowels(s: str) -> int: 1 """ if not isinstance(s, str): - raise ValueError("Input must be a string") + raise TypeError("Input must be a string") vowels = "aeiouAEIOU" return sum(1 for char in s if char in vowels) From 71c7fc8eed7f119a6749fe65efba15ef63ce2c8f Mon Sep 17 00:00:00 2001 From: JAVED KHAN AHMED <149111497+JavedKhan93@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:39:38 +0530 Subject: [PATCH 52/76] docs: upgrade mypy link to https (#14184) Co-authored-by: JavedKhan93 Co-authored-by: Maxim Smolskiy --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35de0bf75ed5..aa6bff3ad1da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,7 +159,7 @@ We want your work to be readable by others; therefore, we encourage you to note starting_value = int(input("Please enter a starting value: ").strip()) ``` - The use of [Python type hints](https://docs.python.org/3/library/typing.html) is encouraged for function parameters and return values. Our automated testing will run [mypy](http://mypy-lang.org) so run that locally before making your submission. + The use of [Python type hints](https://docs.python.org/3/library/typing.html) is encouraged for function parameters and return values. Our automated testing will run [mypy](https://mypy-lang.org) so run that locally before making your submission. ```python def sum_ab(a: int, b: int) -> int: From 6da02abdce243c947688ddaf1926c9aacb0cbb7b Mon Sep 17 00:00:00 2001 From: radhikaRM06 Date: Mon, 9 Mar 2026 10:52:38 +0530 Subject: [PATCH 53/76] Add doctests for duplicate and sorted inputs in bubble sort (#14154) Co-authored-by: Maxim Smolskiy --- sorts/bubble_sort.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index 9ec3d5384f38..b09a2afed928 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -17,6 +17,12 @@ def bubble_sort_iterative(collection: list[Any]) -> list[Any]: [-45, -5, -2] >>> bubble_sort_iterative([-23, 0, 6, -4, 34]) [-23, -4, 0, 6, 34] + >>> bubble_sort_iterative([1, 2, 3, 4]) + [1, 2, 3, 4] + >>> bubble_sort_iterative([3, 3, 3, 3]) + [3, 3, 3, 3] + >>> bubble_sort_iterative([56]) + [56] >>> bubble_sort_iterative([0, 5, 2, 3, 2]) == sorted([0, 5, 2, 3, 2]) True >>> bubble_sort_iterative([]) == sorted([]) From 589d12972d185872b955633f9e7879fb6ecc4317 Mon Sep 17 00:00:00 2001 From: Parth Pawar Date: Mon, 9 Mar 2026 11:03:34 +0530 Subject: [PATCH 54/76] Fix return type description in bubble_sort.py (#14137) * Fix return type description in bubble_sort.py Better (clearer, standard phrasing): :return: the same collection ordered in ascending order * Update bubble_sort.py --------- Co-authored-by: Maxim Smolskiy --- sorts/bubble_sort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index b09a2afed928..77d173290aff 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -6,7 +6,7 @@ def bubble_sort_iterative(collection: list[Any]) -> list[Any]: :param collection: some mutable ordered collection with heterogeneous comparable items inside - :return: the same collection ordered by ascending + :return: the same collection ordered in ascending order Examples: >>> bubble_sort_iterative([0, 5, 2, 3, 2]) From 7af5aba250cd9b382de5b489e0e571f08f4a4416 Mon Sep 17 00:00:00 2001 From: Yaswanth Naga Sai K <140506928+YASWANTH1976@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:25:03 +0530 Subject: [PATCH 55/76] Improve sorted input validation in binary search (#14074) * Improve sorted input validation in binary search * Update binary_search.py * Update binary_search.py * Update binary_search.py --------- Co-authored-by: Maxim Smolskiy --- searches/binary_search.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/searches/binary_search.py b/searches/binary_search.py index 5125dc6bdb9a..bec87b3c5aec 100644 --- a/searches/binary_search.py +++ b/searches/binary_search.py @@ -10,9 +10,8 @@ python3 binary_search.py """ -from __future__ import annotations - import bisect +from itertools import pairwise def bisect_left( @@ -198,7 +197,7 @@ def binary_search(sorted_collection: list[int], item: int) -> int: >>> binary_search([0, 5, 7, 10, 15], 6) -1 """ - if list(sorted_collection) != sorted(sorted_collection): + if any(a > b for a, b in pairwise(sorted_collection)): raise ValueError("sorted_collection must be sorted in ascending order") left = 0 right = len(sorted_collection) - 1 From 9ea690e098c357fa3459a42ca2c2d5f71530b000 Mon Sep 17 00:00:00 2001 From: Yaswanth Naga Sai K <140506928+YASWANTH1976@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:35:13 +0530 Subject: [PATCH 56/76] Improve grammar in linear_search docstring (#14081) * Improve grammar in linear_search docstring Improved wording in the linear search docstring for better clarity. No code logic changed. * Update linear_search.py * Update linear_search.py --------- Co-authored-by: Maxim Smolskiy --- searches/linear_search.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/searches/linear_search.py b/searches/linear_search.py index ba6e81d6bae4..8adb4a7015f0 100644 --- a/searches/linear_search.py +++ b/searches/linear_search.py @@ -1,5 +1,5 @@ """ -This is pure Python implementation of linear search algorithm +This is a pure Python implementation of the linear search algorithm. For doctests run following command: python3 -m doctest -v linear_search.py @@ -12,8 +12,8 @@ def linear_search(sequence: list, target: int) -> int: """A pure Python implementation of a linear search algorithm - :param sequence: a collection with comparable items (as sorted items not required - in Linear Search) + :param sequence: a collection with comparable items (sorting is not required for + linear search) :param target: item value to search :return: index of found item or -1 if item is not found From 32a3d0d0bb5d9b85a2642c82275185281b061878 Mon Sep 17 00:00:00 2001 From: ADDALA MATHEW Date: Mon, 9 Mar 2026 22:58:01 +0530 Subject: [PATCH 57/76] Fix doctest bug in bubble_sort_recursive - incorrect function call (#13821) Fixed bug in bubble_sort_recursive docstring where the doctest was calling bubble_sort_iterative([]) instead of bubble_sort_recursive([]). This was a copy-paste error that would cause the doctest to pass even if bubble_sort_recursive had issues with empty lists. Change: - Line 71: Changed '>>> bubble_sort_iterative([])' to '>>> bubble_sort_recursive([])' This ensures the doctest properly validates the recursive implementation. Co-authored-by: Maxim Smolskiy --- sorts/bubble_sort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index 77d173290aff..4d658a4a12e4 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -69,7 +69,7 @@ def bubble_sort_recursive(collection: list[Any]) -> list[Any]: Examples: >>> bubble_sort_recursive([0, 5, 2, 3, 2]) [0, 2, 2, 3, 5] - >>> bubble_sort_iterative([]) + >>> bubble_sort_recursive([]) [] >>> bubble_sort_recursive([-2, -45, -5]) [-45, -5, -2] From af131b7c3f9311607878565acd49e3e3002c294c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Fern=C3=A1ndez=20Gonz=C3=A1lez?= Date: Tue, 10 Mar 2026 06:44:09 +0100 Subject: [PATCH 58/76] Remove duplicated return statement in area_reg_polygon (#14362) --- maths/area.py | 1 - 1 file changed, 1 deletion(-) diff --git a/maths/area.py b/maths/area.py index 31a654206977..e14cc0aa7195 100644 --- a/maths/area.py +++ b/maths/area.py @@ -552,7 +552,6 @@ def area_reg_polygon(sides: int, length: float) -> float: length of a side" ) return (sides * length**2) / (4 * tan(pi / sides)) - return (sides * length**2) / (4 * tan(pi / sides)) if __name__ == "__main__": From 135c748e55a415f5c61af49024b688dd8708bf83 Mon Sep 17 00:00:00 2001 From: Tejas Rahane <161036451+Tejasrahane@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:26:30 +0530 Subject: [PATCH 59/76] Fix doctests in factorial_recursive function (#13703) * Fix doctests in factorial_recursive function The doctests in factorial_recursive were calling factorial() instead of factorial_recursive(). This fix ensures that the tests correctly validate the factorial_recursive function itself. * Update factorial.py --------- Co-authored-by: Maxim Smolskiy --- maths/factorial.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/maths/factorial.py b/maths/factorial.py index ba61447c7564..2b8b68764d89 100644 --- a/maths/factorial.py +++ b/maths/factorial.py @@ -41,21 +41,21 @@ def factorial_recursive(n: int) -> int: https://en.wikipedia.org/wiki/Factorial >>> import math - >>> all(factorial(i) == math.factorial(i) for i in range(20)) + >>> all(factorial_recursive(i) == math.factorial(i) for i in range(20)) True - >>> factorial(0.1) + >>> factorial_recursive(0.1) Traceback (most recent call last): ... - ValueError: factorial() only accepts integral values - >>> factorial(-1) + ValueError: factorial_recursive() only accepts integral values + >>> factorial_recursive(-1) Traceback (most recent call last): ... - ValueError: factorial() not defined for negative values + ValueError: factorial_recursive() not defined for negative values """ if not isinstance(n, int): - raise ValueError("factorial() only accepts integral values") + raise ValueError("factorial_recursive() only accepts integral values") if n < 0: - raise ValueError("factorial() not defined for negative values") + raise ValueError("factorial_recursive() not defined for negative values") return 1 if n in {0, 1} else n * factorial_recursive(n - 1) From c34f23e47e53cd5068448039eba5748f0f6a80d3 Mon Sep 17 00:00:00 2001 From: Thrive <147845726+VAIBHAVVARORA@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:30:21 +0530 Subject: [PATCH 60/76] Add Extra edge cases (#12995) * Add Extra edge cases * Update coloring.py --------- Co-authored-by: Maxim Smolskiy --- backtracking/coloring.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backtracking/coloring.py b/backtracking/coloring.py index f10cdbcf9d26..abfdf16f1342 100644 --- a/backtracking/coloring.py +++ b/backtracking/coloring.py @@ -104,6 +104,14 @@ def color(graph: list[list[int]], max_colors: int) -> list[int]: >>> max_colors = 2 >>> color(graph, max_colors) [] + >>> color([], 2) # empty graph + [] + >>> color([[0]], 1) # single node, 1 color + [0] + >>> color([[0, 1], [1, 0]], 1) # 2 nodes, 1 color (impossible) + [] + >>> color([[0, 1], [1, 0]], 2) # 2 nodes, 2 colors (possible) + [0, 1] """ colored_vertices = [-1] * len(graph) From a2efba59bfb0dd5920cfa05572fc7843ea454d28 Mon Sep 17 00:00:00 2001 From: Mindaugas <76015221+mindaugl@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:00:28 +0200 Subject: [PATCH 61/76] Add euler project problem 15 additional solution (#12774) * Add euler project problem 15 additional solution by explicitly counting the paths. * Update sol2.py * updating DIRECTORY.md * updating DIRECTORY.md * Trigger CI * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Maxim Smolskiy Co-authored-by: MaximSmolskiy Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- DIRECTORY.md | 6 ++++++ project_euler/problem_015/sol2.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 project_euler/problem_015/sol2.py diff --git a/DIRECTORY.md b/DIRECTORY.md index a73c630bc8a7..ca454bd5fd82 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -469,6 +469,11 @@ ## Geometry * [Geometry](geometry/geometry.py) + * [Graham Scan](geometry/graham_scan.py) + * [Jarvis March](geometry/jarvis_march.py) + * Tests + * [Test Graham Scan](geometry/tests/test_graham_scan.py) + * [Test Jarvis March](geometry/tests/test_jarvis_march.py) ## Graphics * [Bezier Curve](graphics/bezier_curve.py) @@ -981,6 +986,7 @@ * [Sol2](project_euler/problem_014/sol2.py) * Problem 015 * [Sol1](project_euler/problem_015/sol1.py) + * [Sol2](project_euler/problem_015/sol2.py) * Problem 016 * [Sol1](project_euler/problem_016/sol1.py) * [Sol2](project_euler/problem_016/sol2.py) diff --git a/project_euler/problem_015/sol2.py b/project_euler/problem_015/sol2.py new file mode 100644 index 000000000000..903095e144ec --- /dev/null +++ b/project_euler/problem_015/sol2.py @@ -0,0 +1,32 @@ +""" +Problem 15: https://projecteuler.net/problem=15 + +Starting in the top left corner of a 2x2 grid, and only being able to move to +the right and down, there are exactly 6 routes to the bottom right corner. +How many such routes are there through a 20x20 grid? +""" + + +def solution(n: int = 20) -> int: + """ + Solve by explicitly counting the paths with dynamic programming. + + >>> solution(6) + 924 + >>> solution(2) + 6 + >>> solution(1) + 2 + """ + + counts = [[1 for _ in range(n + 1)] for _ in range(n + 1)] + + for i in range(1, n + 1): + for j in range(1, n + 1): + counts[i][j] = counts[i - 1][j] + counts[i][j - 1] + + return counts[n][n] + + +if __name__ == "__main__": + print(solution()) From 7e4b60b05cb0378118e866bd7acbcde2286528af Mon Sep 17 00:00:00 2001 From: Mozart Maia <37476677+mozart-maia@users.noreply.github.com> Date: Fri, 13 Mar 2026 01:40:13 -0300 Subject: [PATCH 62/76] add some doctests to algos in backtracking (#11911) * add some doctests to algos in backtracking * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- backtracking/generate_parentheses.py | 4 ++++ backtracking/n_queens.py | 8 ++++++++ backtracking/word_break.py | 3 +++ 3 files changed, 15 insertions(+) diff --git a/backtracking/generate_parentheses.py b/backtracking/generate_parentheses.py index 18c21e2a9b51..5094f4b08619 100644 --- a/backtracking/generate_parentheses.py +++ b/backtracking/generate_parentheses.py @@ -64,6 +64,10 @@ def generate_parenthesis(n: int) -> list[str]: Example 2: >>> generate_parenthesis(1) ['()'] + + Example 3: + >>> generate_parenthesis(0) + [''] """ result: list[str] = [] diff --git a/backtracking/n_queens.py b/backtracking/n_queens.py index d10181f319b3..6fac93aa77d6 100644 --- a/backtracking/n_queens.py +++ b/backtracking/n_queens.py @@ -33,6 +33,14 @@ def is_safe(board: list[list[int]], row: int, column: int) -> bool: False >>> is_safe([[0, 0, 1], [0, 0, 0], [0, 0, 0]], 1, 1) False + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 2) + True + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 2, 1) + True + >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 0, 2) + True + >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 2, 2) + True """ n = len(board) # Size of the board diff --git a/backtracking/word_break.py b/backtracking/word_break.py index 1f2ab073f499..2e874a02b61c 100644 --- a/backtracking/word_break.py +++ b/backtracking/word_break.py @@ -66,6 +66,9 @@ def word_break(input_string: str, word_dict: set[str]) -> bool: >>> word_break("catsandog", {"cats", "dog", "sand", "and", "cat"}) False + + >>> word_break("applepenapple", {}) + False """ return backtrack(input_string, word_dict, 0) From 68473afc4b2207967453842553875aba6b9c7123 Mon Sep 17 00:00:00 2001 From: Jose Nelson Date: Fri, 13 Mar 2026 14:53:47 -0700 Subject: [PATCH 63/76] Add latitude and longitude validation to lamberts_ellipsoidal_distance (#14373) * Add latitude and longitude validation with doctests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update lamberts_ellipsoidal_distance.py --------- Co-authored-by: Pamela Cano Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- geodesy/lamberts_ellipsoidal_distance.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/geodesy/lamberts_ellipsoidal_distance.py b/geodesy/lamberts_ellipsoidal_distance.py index 4805674e51ab..a5c43c5656e9 100644 --- a/geodesy/lamberts_ellipsoidal_distance.py +++ b/geodesy/lamberts_ellipsoidal_distance.py @@ -32,6 +32,26 @@ def lamberts_ellipsoidal_distance( Returns: geographical distance between two points in metres + >>> lamberts_ellipsoidal_distance(100, 0, 0, 0) + Traceback (most recent call last): + ... + ValueError: Latitude must be between -90 and 90 degrees + + >>> lamberts_ellipsoidal_distance(0, 0, -100, 0) + Traceback (most recent call last): + ... + ValueError: Latitude must be between -90 and 90 degrees + + >>> lamberts_ellipsoidal_distance(0, 200, 0, 0) + Traceback (most recent call last): + ... + ValueError: Longitude must be between -180 and 180 degrees + + >>> lamberts_ellipsoidal_distance(0, 0, 0, -200) + Traceback (most recent call last): + ... + ValueError: Longitude must be between -180 and 180 degrees + >>> from collections import namedtuple >>> point_2d = namedtuple("point_2d", "lat lon") >>> SAN_FRANCISCO = point_2d(37.774856, -122.424227) @@ -46,6 +66,14 @@ def lamberts_ellipsoidal_distance( '9,737,326 meters' """ + # Validate latitude values + if not -90 <= lat1 <= 90 or not -90 <= lat2 <= 90: + raise ValueError("Latitude must be between -90 and 90 degrees") + + # Validate longitude values + if not -180 <= lon1 <= 180 or not -180 <= lon2 <= 180: + raise ValueError("Longitude must be between -180 and 180 degrees") + # CONSTANTS per WGS84 https://en.wikipedia.org/wiki/World_Geodetic_System # Distance in metres(m) # Equation Parameters From 257400487e3e5da24d9e1b7c8b696a42acb18c82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:51:17 +0100 Subject: [PATCH 64/76] Bump actions/deploy-pages from 4 to 5 (#14445) Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sphinx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index bf0a74a239c8..c5707804b1fa 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -53,5 +53,5 @@ jobs: needs: build_docs runs-on: ubuntu-latest steps: - - uses: actions/deploy-pages@v4 + - uses: actions/deploy-pages@v5 id: deployment From 841e947e6865cd097db6bd023dfd6171c41d7dc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:00:33 +0100 Subject: [PATCH 65/76] Bump actions/configure-pages from 5 to 6 (#14470) Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6. - [Release notes](https://github.com/actions/configure-pages/releases) - [Commits](https://github.com/actions/configure-pages/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/configure-pages dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sphinx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index c5707804b1fa..5f5a90ce1a2d 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -39,7 +39,7 @@ jobs: python-version: 3.14 allow-prereleases: true - run: uv sync --group=docs - - uses: actions/configure-pages@v5 + - uses: actions/configure-pages@v6 - run: uv run sphinx-build -c docs . docs/_build/html - uses: actions/upload-pages-artifact@v4 with: From 840ca00ad389a947205a4a928171499f2228cbb7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:50:30 +0200 Subject: [PATCH 66/76] [pre-commit.ci] pre-commit autoupdate (#14325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.14 → v0.15.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.14...v0.15.4) - [github.com/tox-dev/pyproject-fmt: v2.12.1 → v2.16.2](https://github.com/tox-dev/pyproject-fmt/compare/v2.12.1...v2.16.2) - [github.com/abravalheri/validate-pyproject: v0.24.1 → v0.25](https://github.com/abravalheri/validate-pyproject/compare/v0.24.1...v0.25) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * updating DIRECTORY.md * Configure mypy to target Python 3.14 and update to v1.20.0 Agent-Logs-Url: https://github.com/TheAlgorithms/Python/sessions/332481be-4c5f-4412-abf8-6ef4a3913828 Co-authored-by: cclauss <3709715+cclauss@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy Co-authored-by: MaximSmolskiy Co-authored-by: Christian Clauss Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cclauss <3709715+cclauss@users.noreply.github.com> --- .pre-commit-config.yaml | 24 ++++++------ .../filters/local_binary_pattern.py | 2 +- divide_and_conquer/convex_hull.py | 2 +- dynamic_programming/catalan_numbers.py | 2 +- maths/greatest_common_divisor.py | 2 +- project_euler/problem_002/sol4.py | 2 +- project_euler/problem_003/sol1.py | 2 +- project_euler/problem_003/sol2.py | 2 +- project_euler/problem_003/sol3.py | 2 +- project_euler/problem_005/sol1.py | 2 +- project_euler/problem_007/sol2.py | 2 +- pyproject.toml | 38 ++++++++++--------- web_programming/fetch_well_rx_price.py | 2 +- web_programming/instagram_crawler.py | 2 +- 14 files changed, 44 insertions(+), 42 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 765d5cff38d8..1e352f7dcb48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.4 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.12.1 + rev: v2.16.2 hooks: - id: pyproject-fmt @@ -45,19 +45,19 @@ repos: pass_filenames: false - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24.1 + rev: v0.25 hooks: - id: validate-pyproject - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 - hooks: - - id: mypy - args: - - --explicit-package-bases - - --ignore-missing-imports - - --install-types - - --non-interactive + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.20.0 + # hooks: + # - id: mypy + # args: + # - --explicit-package-bases + # - --ignore-missing-imports + # - --install-types + # - --non-interactive - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 diff --git a/digital_image_processing/filters/local_binary_pattern.py b/digital_image_processing/filters/local_binary_pattern.py index 861369ba6a32..ac54ecce755c 100644 --- a/digital_image_processing/filters/local_binary_pattern.py +++ b/digital_image_processing/filters/local_binary_pattern.py @@ -19,7 +19,7 @@ def get_neighbors_pixel( try: return int(image[x_coordinate][y_coordinate] >= center) - except (IndexError, TypeError): + except IndexError, TypeError: return 0 diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py index 93f6daf1f88c..b1ab33cc9415 100644 --- a/divide_and_conquer/convex_hull.py +++ b/divide_and_conquer/convex_hull.py @@ -124,7 +124,7 @@ def _construct_points( else: try: points.append(Point(p[0], p[1])) - except (IndexError, TypeError): + except IndexError, TypeError: print( f"Ignoring deformed point {p}. All points" " must have at least 2 coordinates." diff --git a/dynamic_programming/catalan_numbers.py b/dynamic_programming/catalan_numbers.py index 7b74f2763d43..a62abe36d670 100644 --- a/dynamic_programming/catalan_numbers.py +++ b/dynamic_programming/catalan_numbers.py @@ -71,7 +71,7 @@ def catalan_numbers(upper_limit: int) -> "list[int]": print(f"The Catalan numbers from 0 through {N} are:") print(catalan_numbers(N)) print("Try another upper limit for the sequence: ", end="") - except (NameError, ValueError): + except NameError, ValueError: print("\n********* Invalid input, goodbye! ************\n") import doctest diff --git a/maths/greatest_common_divisor.py b/maths/greatest_common_divisor.py index 1fc123fc2b14..ce0abc664cf9 100644 --- a/maths/greatest_common_divisor.py +++ b/maths/greatest_common_divisor.py @@ -73,7 +73,7 @@ def main(): f"{greatest_common_divisor(num_1, num_2)}" ) print(f"By iterative gcd({num_1}, {num_2}) = {gcd_by_iterative(num_1, num_2)}") - except (IndexError, UnboundLocalError, ValueError): + except IndexError, UnboundLocalError, ValueError: print("Wrong input") diff --git a/project_euler/problem_002/sol4.py b/project_euler/problem_002/sol4.py index a13d34fd760e..3341aa1d4569 100644 --- a/project_euler/problem_002/sol4.py +++ b/project_euler/problem_002/sol4.py @@ -56,7 +56,7 @@ def solution(n: int = 4000000) -> int: try: n = int(n) - except (TypeError, ValueError): + except TypeError, ValueError: raise TypeError("Parameter n must be int or castable to int.") if n <= 0: raise ValueError("Parameter n must be greater than or equal to one.") diff --git a/project_euler/problem_003/sol1.py b/project_euler/problem_003/sol1.py index d1c0e61cf1a6..dbf9a84f68bb 100644 --- a/project_euler/problem_003/sol1.py +++ b/project_euler/problem_003/sol1.py @@ -80,7 +80,7 @@ def solution(n: int = 600851475143) -> int: try: n = int(n) - except (TypeError, ValueError): + except TypeError, ValueError: raise TypeError("Parameter n must be int or castable to int.") if n <= 0: raise ValueError("Parameter n must be greater than or equal to one.") diff --git a/project_euler/problem_003/sol2.py b/project_euler/problem_003/sol2.py index 0af0daceed06..4c4f88220514 100644 --- a/project_euler/problem_003/sol2.py +++ b/project_euler/problem_003/sol2.py @@ -44,7 +44,7 @@ def solution(n: int = 600851475143) -> int: try: n = int(n) - except (TypeError, ValueError): + except TypeError, ValueError: raise TypeError("Parameter n must be int or castable to int.") if n <= 0: raise ValueError("Parameter n must be greater than or equal to one.") diff --git a/project_euler/problem_003/sol3.py b/project_euler/problem_003/sol3.py index e13a0eb74ec1..1a454b618f75 100644 --- a/project_euler/problem_003/sol3.py +++ b/project_euler/problem_003/sol3.py @@ -44,7 +44,7 @@ def solution(n: int = 600851475143) -> int: try: n = int(n) - except (TypeError, ValueError): + except TypeError, ValueError: raise TypeError("Parameter n must be int or castable to int.") if n <= 0: raise ValueError("Parameter n must be greater than or equal to one.") diff --git a/project_euler/problem_005/sol1.py b/project_euler/problem_005/sol1.py index 01cbd0e15ff7..f889c420c61d 100644 --- a/project_euler/problem_005/sol1.py +++ b/project_euler/problem_005/sol1.py @@ -47,7 +47,7 @@ def solution(n: int = 20) -> int: try: n = int(n) - except (TypeError, ValueError): + except TypeError, ValueError: raise TypeError("Parameter n must be int or castable to int.") if n <= 0: raise ValueError("Parameter n must be greater than or equal to one.") diff --git a/project_euler/problem_007/sol2.py b/project_euler/problem_007/sol2.py index fd99453c1100..d63b2f2d86ec 100644 --- a/project_euler/problem_007/sol2.py +++ b/project_euler/problem_007/sol2.py @@ -87,7 +87,7 @@ def solution(nth: int = 10001) -> int: try: nth = int(nth) - except (TypeError, ValueError): + except TypeError, ValueError: raise TypeError("Parameter nth must be int or castable to int.") from None if nth <= 0: raise ValueError("Parameter nth must be greater than or equal to one.") diff --git a/pyproject.toml b/pyproject.toml index f1559d6bc1b1..6ec899d7840f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ test = [ "pytest>=8.4.1", "pytest-cov>=6", ] - docs = [ "myst-parser>=4", "sphinx-autoapi>=3.4", @@ -50,7 +49,6 @@ euler-validate = [ [tool.ruff] target-version = "py314" - output-format = "full" lint.select = [ # https://beta.ruff.rs/docs/rules @@ -128,7 +126,6 @@ lint.ignore = [ "SLF001", # Private member accessed: `_Iterator` -- FIX ME "UP037", # FIX ME ] - lint.per-file-ignores."data_structures/hashing/tests/test_hash_map.py" = [ "BLE001", ] @@ -150,37 +147,43 @@ lint.per-file-ignores."project_euler/problem_099/sol1.py" = [ lint.per-file-ignores."sorts/external_sort.py" = [ "SIM115", ] -lint.mccabe.max-complexity = 17 # default: 10 +lint.mccabe.max-complexity = 17 # default: 10 lint.pylint.allow-magic-value-types = [ "float", "int", "str", ] -lint.pylint.max-args = 10 # default: 5 -lint.pylint.max-branches = 20 # default: 12 -lint.pylint.max-returns = 8 # default: 6 -lint.pylint.max-statements = 88 # default: 50 +lint.pylint.max-args = 10 # default: 5 +lint.pylint.max-branches = 20 # default: 12 +lint.pylint.max-returns = 8 # default: 6 +lint.pylint.max-statements = 88 # default: 50 [tool.codespell] ignore-words-list = "3rt,abd,aer,ans,bitap,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar" -skip = "./.*,*.json,*.lock,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictionary.txt,strings/words.txt" +skip = """\ + ./.*,*.json,*.lock,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictio\ + nary.txt,strings/words.txt\ + """ -[tool.pytest.ini_options] -markers = [ +[tool.pytest] +ini_options.markers = [ "mat_ops: mark a test as utilizing matrix operations.", ] -addopts = [ +ini_options.addopts = [ "--durations=10", "--doctest-modules", "--showlocals", ] -[tool.coverage.report] -omit = [ +[tool.coverage] +report.omit = [ ".env/*", "project_euler/*", ] -sort = "Cover" +report.sort = "Cover" + +[tool.mypy] +python_version = "3.14" [tool.sphinx-pyproject] copyright = "2014, TheAlgorithms" @@ -261,7 +264,6 @@ myst_fence_as_directive = [ "include", ] templates_path = [ "_templates" ] -[tool.sphinx-pyproject.source_suffix] -".rst" = "restructuredtext" +source_suffix.".rst" = "restructuredtext" # ".txt" = "markdown" -".md" = "markdown" +source_suffix.".md" = "markdown" diff --git a/web_programming/fetch_well_rx_price.py b/web_programming/fetch_well_rx_price.py index e34a89c19cc8..680d7444bd1c 100644 --- a/web_programming/fetch_well_rx_price.py +++ b/web_programming/fetch_well_rx_price.py @@ -67,7 +67,7 @@ def fetch_pharmacy_and_price_list(drug_name: str, zip_code: str) -> list | None: return pharmacy_price_list - except (httpx.HTTPError, ValueError): + except httpx.HTTPError, ValueError: return None diff --git a/web_programming/instagram_crawler.py b/web_programming/instagram_crawler.py index 68271c1c4643..0b91db01ca09 100644 --- a/web_programming/instagram_crawler.py +++ b/web_programming/instagram_crawler.py @@ -53,7 +53,7 @@ def get_json(self) -> dict: scripts = BeautifulSoup(html, "html.parser").find_all("script") try: return extract_user_profile(scripts[4]) - except (json.decoder.JSONDecodeError, KeyError): + except json.decoder.JSONDecodeError, KeyError: return extract_user_profile(scripts[3]) def __repr__(self) -> str: From 754dac42d9927fd5a727a181fec6c4007c9ebaf5 Mon Sep 17 00:00:00 2001 From: Max <183308611+max-klingner@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:55:59 -0700 Subject: [PATCH 67/76] fix: remove dead code in hamming_code.py to resolve SIM113 (#14509) Co-authored-by: Max <183308611+max938-coder@users.noreply.github.com> --- hashes/hamming_code.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/hashes/hamming_code.py b/hashes/hamming_code.py index b3095852ac51..fead26cf7536 100644 --- a/hashes/hamming_code.py +++ b/hashes/hamming_code.py @@ -118,7 +118,6 @@ def emitter_converter(size_par, data): data_ord.append(None) # Calculates parity - qtd_bp = 0 # parity bit counter for bp in range(1, size_par + 1): # Bit counter one for a given parity cont_bo = 0 @@ -133,8 +132,6 @@ def emitter_converter(size_par, data): cont_bo += 1 parity.append(cont_bo % 2) - qtd_bp += 1 - # Mount the message cont_bp = 0 # parity bit counter for x in range(size_par + len(data)): @@ -208,7 +205,6 @@ def receptor_converter(size_par, data): data_ord.append(None) # Calculates parity - qtd_bp = 0 # parity bit counter for bp in range(1, size_par + 1): # Bit counter one for a certain parity cont_bo = 0 @@ -222,8 +218,6 @@ def receptor_converter(size_par, data): cont_bo += 1 parity.append(str(cont_bo % 2)) - qtd_bp += 1 - # Mount the message cont_bp = 0 # Parity bit counter for x in range(size_par + len(data_output)): From 02680c905522d8c8abae08030dfd754aeb4c2ae4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:03:33 +0300 Subject: [PATCH 68/76] [pre-commit.ci] pre-commit autoupdate (#14513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.4 → v0.15.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.4...v0.15.9) - [github.com/codespell-project/codespell: v2.4.1 → v2.4.2](https://github.com/codespell-project/codespell/compare/v2.4.1...v2.4.2) - [github.com/tox-dev/pyproject-fmt: v2.16.2 → v2.21.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.16.2...v2.21.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- .pre-commit-config.yaml | 6 +++--- pyproject.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e352f7dcb48..39daf3dd7f88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,20 +19,20 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 + rev: v0.15.9 hooks: - id: ruff-check - id: ruff-format - repo: https://github.com/codespell-project/codespell - rev: v2.4.1 + rev: v2.4.2 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.16.2 + rev: v2.21.0 hooks: - id: pyproject-fmt diff --git a/pyproject.toml b/pyproject.toml index 6ec899d7840f..34e099a46435 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -165,6 +165,9 @@ skip = """\ nary.txt,strings/words.txt\ """ +[tool.mypy] +python_version = "3.14" + [tool.pytest] ini_options.markers = [ "mat_ops: mark a test as utilizing matrix operations.", @@ -182,9 +185,6 @@ report.omit = [ ] report.sort = "Cover" -[tool.mypy] -python_version = "3.14" - [tool.sphinx-pyproject] copyright = "2014, TheAlgorithms" autoapi_dirs = [ From f944b9125d148f5a0e487e445f6196e2e280fca3 Mon Sep 17 00:00:00 2001 From: Aarav Arya <159852168+Aarav-Arya@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:07:41 -0400 Subject: [PATCH 69/76] Add type hints to unknown_sort (#14489) * Added type hints for bogo_sort * Add type hints to unknown_sort * Update bogo_sort.py * Update unknown_sort.py --------- Co-authored-by: Maxim Smolskiy --- sorts/unknown_sort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sorts/unknown_sort.py b/sorts/unknown_sort.py index 9fa9d22fb5e0..3545da68ea80 100644 --- a/sorts/unknown_sort.py +++ b/sorts/unknown_sort.py @@ -6,7 +6,7 @@ """ -def merge_sort(collection): +def merge_sort(collection: list) -> list: """Pure implementation of the fastest merge sort algorithm in Python :param collection: some mutable ordered collection with heterogeneous From fc2f947e0fabb84862737aa7fed05e6322389f0b Mon Sep 17 00:00:00 2001 From: Mahema Date: Sat, 11 Apr 2026 03:37:38 +0530 Subject: [PATCH 70/76] Fix empty input edge case and correct output formatting (#14444) * Fix empty input edge case and correct output formatting * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update pigeonhole_sort.py * Update pigeonhole_sort.py * Update pigeonhole_sort.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy --- sorts/pigeonhole_sort.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sorts/pigeonhole_sort.py b/sorts/pigeonhole_sort.py index bfa9bb11b8a6..7fbc6188cfb4 100644 --- a/sorts/pigeonhole_sort.py +++ b/sorts/pigeonhole_sort.py @@ -10,7 +10,11 @@ def pigeonhole_sort(a): >>> pigeonhole_sort(a) # a destructive sort >>> a == b True + + >>> pigeonhole_sort([]) """ + if not a: + return # size of range of values in the list (ie, number of pigeonholes we need) min_val = min(a) # min() finds the minimum value @@ -38,7 +42,7 @@ def pigeonhole_sort(a): def main(): a = [8, 3, 2, 7, 4, 6, 8] pigeonhole_sort(a) - print("Sorted order is:", " ".join(a)) + print("Sorted order is:", *a) if __name__ == "__main__": From 791deb40f93c65a020f2c75e41b55c97ad566cc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 00:00:57 +0200 Subject: [PATCH 71/76] Bump actions/upload-pages-artifact from 4 to 5 (#14551) Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sphinx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 5f5a90ce1a2d..3f00094e0264 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -41,7 +41,7 @@ jobs: - run: uv sync --group=docs - uses: actions/configure-pages@v6 - run: uv run sphinx-build -c docs . docs/_build/html - - uses: actions/upload-pages-artifact@v4 + - uses: actions/upload-pages-artifact@v5 with: path: docs/_build/html From abf7168f572cb42db0783fac02763869be43201d Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Mon, 18 May 2026 14:53:15 -0700 Subject: [PATCH 72/76] feat: add Segment Intersection algorithm (#14416) * feat: add Segment Intersection algorithm * fix: use descriptive parameter names --------- Co-authored-by: John Law --- geometry/segment_intersection.py | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 geometry/segment_intersection.py diff --git a/geometry/segment_intersection.py b/geometry/segment_intersection.py new file mode 100644 index 000000000000..e2e2e10f1e4d --- /dev/null +++ b/geometry/segment_intersection.py @@ -0,0 +1,112 @@ +""" +Given two line segments, determine whether they intersect. + +This is based on the algorithm described in Introduction to Algorithms +(CLRS), Chapter 33. + +Reference: + - https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection + - https://en.wikipedia.org/wiki/Orientation_(geometry) +""" + +from __future__ import annotations + +from typing import NamedTuple + + +class Point(NamedTuple): + """A point in 2D space. + + >>> Point(0, 0) + Point(x=0, y=0) + >>> Point(1, -3) + Point(x=1, y=-3) + """ + + x: float + y: float + + +def direction(pivot: Point, target: Point, query: Point) -> float: + """Return the cross product of vectors (pivot->query) and (pivot->target). + + The sign of the result encodes the orientation of the ordered triple + (pivot, target, query): + - Negative -> counter-clockwise (left turn) + - Positive -> clockwise (right turn) + - Zero -> collinear + + >>> direction(Point(0, 0), Point(1, 0), Point(0, 1)) + -1 + >>> direction(Point(0, 0), Point(0, 1), Point(1, 0)) + 1 + >>> direction(Point(0, 0), Point(1, 1), Point(2, 2)) + 0 + """ + return (query.x - pivot.x) * (target.y - pivot.y) - (target.x - pivot.x) * ( + query.y - pivot.y + ) + + +def on_segment(seg_start: Point, seg_end: Point, point: Point) -> bool: + """Check whether *point*, known to be collinear with the segment, lies on it. + + >>> on_segment(Point(0, 0), Point(4, 4), Point(2, 2)) + True + >>> on_segment(Point(0, 0), Point(4, 4), Point(5, 5)) + False + >>> on_segment(Point(0, 0), Point(4, 0), Point(2, 0)) + True + """ + return min(seg_start.x, seg_end.x) <= point.x <= max( + seg_start.x, seg_end.x + ) and min(seg_start.y, seg_end.y) <= point.y <= max(seg_start.y, seg_end.y) + + +def segments_intersect(p1: Point, p2: Point, p3: Point, p4: Point) -> bool: + """Return True if line segment p1p2 intersects line segment p3p4. + + Uses the CLRS cross-product / orientation method. Handles both the + general case (proper crossing) and degenerate cases where one endpoint + lies exactly on the other segment. + + >>> segments_intersect(Point(0, 0), Point(2, 2), Point(0, 2), Point(2, 0)) + True + >>> segments_intersect(Point(0, 0), Point(2, 2), Point(1, 1), Point(3, 3)) + True + >>> segments_intersect(Point(0, 0), Point(1, 0), Point(2, 0), Point(3, 0)) + False + >>> segments_intersect(Point(0, 0), Point(1, 1), Point(1, 0), Point(2, 1)) + False + >>> segments_intersect(Point(0, 0), Point(1, 1), Point(0, 1), Point(0, 2)) + False + >>> segments_intersect(Point(0, 0), Point(1, 0), Point(1, 0), Point(2, 0)) + True + """ + d1 = direction(p3, p4, p1) + d2 = direction(p3, p4, p2) + d3 = direction(p1, p2, p3) + d4 = direction(p1, p2, p4) + + if ((d1 < 0 < d2) or (d2 < 0 < d1)) and ((d3 < 0 < d4) or (d4 < 0 < d3)): + return True + + if d1 == 0 and on_segment(p3, p4, p1): + return True + if d2 == 0 and on_segment(p3, p4, p2): + return True + if d3 == 0 and on_segment(p1, p2, p3): + return True + return d4 == 0 and on_segment(p1, p2, p4) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print("Enter four points as 'x y' pairs (one per line):") + points = [Point(*map(float, input().split())) for _ in range(4)] + p1, p2, p3, p4 = points + result = segments_intersect(p1, p2, p3, p4) + print(1 if result else 0) From 144ef9c022d556b3546de2ece425da709e50ec0c Mon Sep 17 00:00:00 2001 From: Alessandro Molinari Date: Tue, 19 May 2026 00:14:42 +0200 Subject: [PATCH 73/76] Fix type hints in sorts/tim_sort.py, relates to #14457 (#14474) * Add type hints to tim_sort.py, relates to #14457 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix ruff PYI041 error: use float instead of int | float * Fix mypy error: support str and tuple inputs as defined in doctests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix ruff E501: wrap binary_search parameters to respect 88 char limit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Refactor generics to use Python 3.12 type parameter syntax (PEP 695) * Use Any from typing to resolve mypy list unpacking bugs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: John Law --- sorts/tim_sort.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sorts/tim_sort.py b/sorts/tim_sort.py index 41ab4a10a87b..2eeed88b7399 100644 --- a/sorts/tim_sort.py +++ b/sorts/tim_sort.py @@ -1,4 +1,7 @@ -def binary_search(lst, item, start, end): +from typing import Any + + +def binary_search(lst: list[Any], item: Any, start: int, end: int) -> int: if start == end: return start if lst[start] > item else start + 1 if start > end: @@ -13,7 +16,7 @@ def binary_search(lst, item, start, end): return mid -def insertion_sort(lst): +def insertion_sort(lst: list[Any]) -> list[Any]: length = len(lst) for index in range(1, length): @@ -24,7 +27,7 @@ def insertion_sort(lst): return lst -def merge(left, right): +def merge(left: list[Any], right: list[Any]) -> list[Any]: if not left: return right @@ -37,7 +40,7 @@ def merge(left, right): return [right[0], *merge(left, right[1:])] -def tim_sort(lst): +def tim_sort(lst: list[Any] | tuple[Any, ...] | str) -> list[Any]: """ >>> tim_sort("Python") ['P', 'h', 'n', 'o', 't', 'y'] @@ -53,7 +56,7 @@ def tim_sort(lst): length = len(lst) runs, sorted_runs = [], [] new_run = [lst[0]] - sorted_array = [] + sorted_array: list[Any] = [] i = 1 while i < length: if lst[i] < lst[i - 1]: From 33a8e0f21ae492364db3786048de0b824b7c38d3 Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Tue, 19 May 2026 16:43:43 -0700 Subject: [PATCH 74/76] feat: add Ramer-Douglas-Peucker polyline simplification algorithm (#14372) * feat: add Ramer-Douglas-Peucker polyline simplification algorithm * Use descriptive parameter names * Update geometry/ramer_douglas_peucker.py * Update geometry/ramer_douglas_peucker.py * Update ramer_douglas_peucker.py * Update ramer_douglas_peucker.py * Update ramer_douglas_peucker.py --------- Co-authored-by: John Law --- geometry/ramer_douglas_peucker.py | 184 ++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 geometry/ramer_douglas_peucker.py diff --git a/geometry/ramer_douglas_peucker.py b/geometry/ramer_douglas_peucker.py new file mode 100644 index 000000000000..a03bbb2e5086 --- /dev/null +++ b/geometry/ramer_douglas_peucker.py @@ -0,0 +1,184 @@ +""" +Ramer-Douglas-Peucker polyline simplification algorithm. + +Given a sequence of 2-D points and a tolerance epsilon, the algorithm +reduces the number of points while preserving the overall shape of the curve. + +Time complexity: O(n log n) average, O(n²) worst case +Space complexity: O(n) + +References: + https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm +""" + +from __future__ import annotations + +import math + + +def _euclidean_distance( + point_a: tuple[float, float], + point_b: tuple[float, float], +) -> float: + """Return the Euclidean distance between two 2-D points. + + >>> _euclidean_distance((0.0, 0.0), (3.0, 4.0)) + 5.0 + >>> _euclidean_distance((1.0, 1.0), (1.0, 1.0)) + 0.0 + """ + return math.hypot(point_b[0] - point_a[0], point_b[1] - point_a[1]) + + +def _perpendicular_distance( + point: tuple[float, float], + line_start: tuple[float, float], + line_end: tuple[float, float], +) -> float: + """Return the distance from *point* to the line **segment** between + *line_start* and *line_end*. + + When the perpendicular projection of *point* onto the infinite line falls + within the segment, this equals the perpendicular distance to that line. + When the projection falls outside the segment, the distance to the nearest + endpoint is returned instead (projection clamped to [0, 1]). + + This is the correct distance measure for the Ramer-Douglas-Peucker + algorithm: using the infinite-line distance can incorrectly discard points + whose projection lies beyond a segment endpoint. + + >>> _perpendicular_distance((4.0, 0.0), (0.0, 0.0), (0.0, 3.0)) + 4.0 + >>> # order of line_start and line_end does not affect the result + >>> _perpendicular_distance((4.0, 0.0), (0.0, 3.0), (0.0, 0.0)) + 4.0 + >>> _perpendicular_distance((4.0, 1.0), (0.0, 1.0), (0.0, 4.0)) + 4.0 + >>> _perpendicular_distance((2.0, 1.0), (-2.0, 1.0), (-2.0, 4.0)) + 4.0 + >>> # projection falls outside the segment; distance to nearest endpoint + >>> round(_perpendicular_distance((0.0, 2.0), (1.0, 0.0), (3.0, 0.0)), 6) + 2.236068 + """ + px, py = point + ax, ay = line_start + bx, by = line_end + dx, dy = bx - ax, by - ay + seg_len_sq = dx * dx + dy * dy + if seg_len_sq == 0.0: + # line_start and line_end coincide; fall back to point-to-point distance + return _euclidean_distance(point, line_start) + # Project point onto the segment line, then clamp t to [0, 1] so the + # nearest point is always on the segment rather than the infinite line. + t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / seg_len_sq)) + nearest_x = ax + t * dx + nearest_y = ay + t * dy + return math.hypot(px - nearest_x, py - nearest_y) + + +def ramer_douglas_peucker( + pts: list[tuple[float, float]], + epsilon: float, +) -> list[tuple[float, float]]: + """Simplify a polyline using the Ramer-Douglas-Peucker algorithm. + + Given a sequence of 2-D points and a maximum allowable deviation + *epsilon* (>= 0), returns a simplified list of points such that no + discarded point is farther than *epsilon* from the simplified polyline. + + Parameters + ---------- + pts: + Ordered sequence of ``(x, y)`` points describing the polyline. + epsilon: + Maximum allowable distance of any discarded point from the + simplified polyline. Must be non-negative. + + Returns + ------- + list[tuple[float, float]] + Simplified list of ``(x, y)`` points. The first and last points of + *pts* are always preserved. + + Raises + ------ + ValueError + If *epsilon* is negative. + + References + ---------- + https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm + + Examples + -------- + >>> ramer_douglas_peucker([], epsilon=1.0) + [] + >>> ramer_douglas_peucker([(0.0, 0.0)], epsilon=1.0) + [(0.0, 0.0)] + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.0)], epsilon=1.0) + [(0.0, 0.0), (1.0, 0.0)] + >>> # middle point is within epsilon - it is discarded + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.1), (2.0, 0.0)], epsilon=0.5) + [(0.0, 0.0), (2.0, 0.0)] + >>> # middle point exceeds epsilon - it is kept + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)], epsilon=0.5) + [(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)] + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.5), (2.0, 0.0)], epsilon=-1.0) + Traceback (most recent call last): + ... + ValueError: epsilon must be non-negative, got -1.0 + """ + if epsilon < 0: + msg = f"epsilon must be non-negative, got {epsilon!r}" + raise ValueError(msg) + + if len(pts) < 3: + return list(pts) + + # --------------------------------------------------------------------------- + # Iterative, stack-based implementation. + # + # The naive recursive approach copies sublists at every level via slicing + # (pts[:max_index+1] / pts[max_index:]), which is O(n) per call and makes + # the overall algorithm O(n²) in memory even for well-balanced splits. An + # explicit stack operating on index ranges avoids all copying and also + # eliminates the risk of hitting Python's recursion limit for long polylines. + # --------------------------------------------------------------------------- + n = len(pts) + + # keep[i] is True when pts[i] must appear in the output. + keep: list[bool] = [False] * n + keep[0] = True + keep[-1] = True + + # Stack of (start_index, end_index) pairs still to be examined. + stack: list[tuple[int, int]] = [(0, n - 1)] + + while stack: + start, end = stack.pop() + if end - start < 2: + # Only one interior candidate at most; nothing to split further. + continue + + # Find the interior point with the greatest distance to the segment. + max_dist = 0.0 + max_index = start + for i in range(start + 1, end): + dist = _perpendicular_distance(pts[i], pts[start], pts[end]) + if dist > max_dist: + max_dist = dist + max_index = i + + if max_dist > epsilon: + keep[max_index] = True + stack.append((start, max_index)) + stack.append((max_index, end)) + # else: all interior points are within epsilon; discard them all. + + return [pts[i] for i in range(n) if keep[i]] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() From a9f2e72541a30cabf96a4ba459527f4ecb40eca3 Mon Sep 17 00:00:00 2001 From: Sangam Paudel Date: Thu, 21 May 2026 00:01:03 +0545 Subject: [PATCH 75/76] Added Johnson's algorithm for all-pairs shortest paths (#13340) * Fix typos in Johnson's algorithm (nd -> and) to pass codespell * Rename type aliases and h parameter to follow snake_case and descriptive naming * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: John Law Co-authored-by: Christian Clauss Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- graphs/johnson.py | 118 +++++++++++++++++++++++++++++++++++ graphs/tests/test_johnson.py | 24 +++++++ 2 files changed, 142 insertions(+) create mode 100644 graphs/johnson.py create mode 100644 graphs/tests/test_johnson.py diff --git a/graphs/johnson.py b/graphs/johnson.py new file mode 100644 index 000000000000..6306ab5f8654 --- /dev/null +++ b/graphs/johnson.py @@ -0,0 +1,118 @@ +import heapq +from collections.abc import Hashable + +Node = Hashable +edge = tuple[Node, Node, float] +adjacency = dict[Node, list[tuple[Node, float]]] + + +def _collect_nodes_and_edges(graph: adjacency) -> tuple[list[Node], list[edge]]: + nodes = set() + edges: list[edge] = [] + for u, neighbors in graph.items(): + nodes.add(u) + for v, w in neighbors: + nodes.add(v) + edges.append((u, v, w)) + return list(nodes), edges + + +def _bellman_ford(nodes: list[Node], edges: list[edge]) -> dict[Node, float]: + """ + Bellman-Ford relaxation to compute potentials h[v] for all vertices. + Raises ValueError if a negative weight cycle exists. + """ + dist: dict[Node, float] = dict.fromkeys(nodes, 0.0) + n = len(nodes) + + for _ in range(n - 1): + updated = False + for u, v, w in edges: + if dist[u] + w < dist[v]: + dist[v] = dist[u] + w + updated = True + if not updated: + break + else: + for u, v, w in edges: + if dist[u] + w < dist[v]: + raise ValueError("Negative weight cycle detected") + return dist + + +def _dijkstra( + start: Node, + nodes: list[Node], + graph: adjacency, + potentials: dict[Node, float], +) -> dict[Node, float]: + """ + Dijkstra over reweighted graph, using potentials h to make weights non-negative. + Returns distances from start in the reweighted space. + """ + inf = float("inf") + dist: dict[Node, float] = dict.fromkeys(nodes, inf) + dist[start] = 0.0 + heap: list[tuple[float, Node]] = [(0.0, start)] + + while heap: + d_u, u = heapq.heappop(heap) + if d_u > dist[u]: + continue + for v, w in graph.get(u, []): + w_prime = w + potentials[u] - potentials[v] + if w_prime < 0: + raise ValueError( + "Negative edge weight after reweighting: numeric error" + ) + new_dist = d_u + w_prime + if new_dist < dist[v]: + dist[v] = new_dist + heapq.heappush(heap, (new_dist, v)) + return dist + + +def johnson(graph: adjacency) -> dict[Node, dict[Node, float]]: + """ + Compute all-pairs shortest paths using Johnson's algorithm. + + Reference: + https://en.wikipedia.org/wiki/Johnson%27s_algorithm + + Args: + graph: adjacency list {u: [(v, weight), ...], ...} + + Returns: + dict of dicts: dist[u][v] = shortest distance from u to v + + Raises: + ValueError: if a negative weight cycle is detected + + Example: + >>> g = { + ... 0: [(1, 3), (2, 8), (4, -4)], + ... 1: [(3, 1), (4, 7)], + ... 2: [(1, 4)], + ... 3: [(0, 2), (2, -5)], + ... 4: [(3, 6)], + ... } + >>> round(johnson(g)[0][3], 2) + 2.0 + """ + nodes, edges = _collect_nodes_and_edges(graph) + potentials = _bellman_ford(nodes, edges) + + all_pairs: dict[Node, dict[Node, float]] = {} + inf = float("inf") + for s in nodes: + dist_reweighted = _dijkstra(s, nodes, graph, potentials) + dists_orig: dict[Node, float] = {} + for v in nodes: + d_prime = dist_reweighted[v] + if d_prime < inf: + dists_orig[v] = d_prime - potentials[s] + potentials[v] + else: + dists_orig[v] = inf + all_pairs[s] = dists_orig + + return all_pairs diff --git a/graphs/tests/test_johnson.py b/graphs/tests/test_johnson.py new file mode 100644 index 000000000000..e149aac85d0f --- /dev/null +++ b/graphs/tests/test_johnson.py @@ -0,0 +1,24 @@ +import math + +import pytest + +from graphs.johnson import johnson + + +def test_johnson_basic(): + g = { + 0: [(1, 3), (2, 8), (4, -4)], + 1: [(3, 1), (4, 7)], + 2: [(1, 4)], + 3: [(0, 2), (2, -5)], + 4: [(3, 6)], + } + dist = johnson(g) + assert math.isclose(dist[0][3], 2.0, abs_tol=1e-9) + assert math.isclose(dist[3][2], -5.0, abs_tol=1e-9) + + +def test_johnson_negative_cycle(): + g2 = {0: [(1, 1)], 1: [(0, -3)]} + with pytest.raises(ValueError): + johnson(g2) From 456d644c23e17502443b9e6dabb669078e9a895e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 13:51:30 +0200 Subject: [PATCH 76/76] [pre-commit.ci] pre-commit autoupdate (#14629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.9 → v0.15.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.9...v0.15.12) - [github.com/tox-dev/pyproject-fmt: v2.21.0 → v2.21.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.21.0...v2.21.1) * updating DIRECTORY.md * Update pre-commit hook versions --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss Co-authored-by: cclauss --- .pre-commit-config.yaml | 4 ++-- DIRECTORY.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39daf3dd7f88..adca030fefe0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.9 + rev: v0.15.14 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.21.0 + rev: v2.21.2 hooks: - id: pyproject-fmt diff --git a/DIRECTORY.md b/DIRECTORY.md index ca454bd5fd82..daf71bab8162 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -471,6 +471,8 @@ * [Geometry](geometry/geometry.py) * [Graham Scan](geometry/graham_scan.py) * [Jarvis March](geometry/jarvis_march.py) + * [Ramer Douglas Peucker](geometry/ramer_douglas_peucker.py) + * [Segment Intersection](geometry/segment_intersection.py) * Tests * [Test Graham Scan](geometry/tests/test_graham_scan.py) * [Test Jarvis March](geometry/tests/test_jarvis_march.py) @@ -523,6 +525,7 @@ * [Graphs Floyd Warshall](graphs/graphs_floyd_warshall.py) * [Greedy Best First](graphs/greedy_best_first.py) * [Greedy Min Vertex Cover](graphs/greedy_min_vertex_cover.py) + * [Johnson](graphs/johnson.py) * [Kahns Algorithm Long](graphs/kahns_algorithm_long.py) * [Kahns Algorithm Topo](graphs/kahns_algorithm_topo.py) * [Karger](graphs/karger.py) @@ -543,6 +546,7 @@ * [Strongly Connected Components](graphs/strongly_connected_components.py) * [Tarjans Scc](graphs/tarjans_scc.py) * Tests + * [Test Johnson](graphs/tests/test_johnson.py) * [Test Min Spanning Tree Kruskal](graphs/tests/test_min_spanning_tree_kruskal.py) * [Test Min Spanning Tree Prim](graphs/tests/test_min_spanning_tree_prim.py)