diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index bf0a74a239c8..3f00094e0264 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -39,9 +39,9 @@ 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 + - uses: actions/upload-pages-artifact@v5 with: path: docs/_build/html @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 765d5cff38d8..adca030fefe0 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.14.14 + rev: v0.15.14 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.12.1 + rev: v2.21.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/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: diff --git a/DIRECTORY.md b/DIRECTORY.md index a73c630bc8a7..daf71bab8162 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -469,6 +469,13 @@ ## Geometry * [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) ## Graphics * [Bezier Curve](graphics/bezier_curve.py) @@ -518,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) @@ -538,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) @@ -981,6 +990,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/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) 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/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)) 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) 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/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/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 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/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/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() 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) 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_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} 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) 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) 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)): 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/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__": 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) 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 diff --git a/maths/greatest_common_divisor.py b/maths/greatest_common_divisor.py index a2174a8eb74a..ce0abc664cf9 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 @@ -69,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/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() 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 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/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()) diff --git a/pyproject.toml b/pyproject.toml index f1559d6bc1b1..34e099a46435 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.mypy] +python_version = "3.14" -[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.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/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 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. 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 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 diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index 9ec3d5384f38..4d658a4a12e4 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]) @@ -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([]) @@ -63,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] 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__": 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]: 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 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) 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") 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() ) 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: