From a796ccf1ce2594bffdb938156987a0cbb16ee52e Mon Sep 17 00:00:00 2001 From: ulwlu Date: Tue, 2 Mar 2021 21:24:41 +0900 Subject: [PATCH] Add graham scan algorithm (#4205) * Add graham scan algorithm * Fix argument name p with point * Add tests in inner function * updating DIRECTORY.md * Fix graham scan for isort --profile=black Co-authored-by: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> --- DIRECTORY.md | 7 ++ other/graham_scan.py | 171 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 other/graham_scan.py diff --git a/DIRECTORY.md b/DIRECTORY.md index d487b3949..61e20eeb5 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -30,6 +30,8 @@ * [Binary Count Trailing Zeros](https://github.com/TheAlgorithms/Python/blob/master/bit_manipulation/binary_count_trailing_zeros.py) * [Binary Or Operator](https://github.com/TheAlgorithms/Python/blob/master/bit_manipulation/binary_or_operator.py) * [Binary Xor Operator](https://github.com/TheAlgorithms/Python/blob/master/bit_manipulation/binary_xor_operator.py) + * [Count Number Of One Bits](https://github.com/TheAlgorithms/Python/blob/master/bit_manipulation/count_number_of_one_bits.py) + * [Reverse Bits](https://github.com/TheAlgorithms/Python/blob/master/bit_manipulation/reverse_bits.py) * [Single Bit Manipulation Operations](https://github.com/TheAlgorithms/Python/blob/master/bit_manipulation/single_bit_manipulation_operations.py) ## Blockchain @@ -122,6 +124,7 @@ * [Fenwick Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/fenwick_tree.py) * [Lazy Segment Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/lazy_segment_tree.py) * [Lowest Common Ancestor](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/lowest_common_ancestor.py) + * [Merge Two Binary Trees](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/merge_two_binary_trees.py) * [Non Recursive Segment Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/non_recursive_segment_tree.py) * [Number Of Possible Binary Trees](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/number_of_possible_binary_trees.py) * [Red Black Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/red_black_tree.py) @@ -274,6 +277,7 @@ ## Graphics * [Bezier Curve](https://github.com/TheAlgorithms/Python/blob/master/graphics/bezier_curve.py) + * [Koch Snowflake](https://github.com/TheAlgorithms/Python/blob/master/graphics/koch_snowflake.py) * [Vector3 For 2D Rendering](https://github.com/TheAlgorithms/Python/blob/master/graphics/vector3_for_2d_rendering.py) ## Graphs @@ -520,6 +524,7 @@ * [Frequency Finder](https://github.com/TheAlgorithms/Python/blob/master/other/frequency_finder.py) * [Game Of Life](https://github.com/TheAlgorithms/Python/blob/master/other/game_of_life.py) * [Gauss Easter](https://github.com/TheAlgorithms/Python/blob/master/other/gauss_easter.py) + * [Graham Scan](https://github.com/TheAlgorithms/Python/blob/master/other/graham_scan.py) * [Greedy](https://github.com/TheAlgorithms/Python/blob/master/other/greedy.py) * [Integeration By Simpson Approx](https://github.com/TheAlgorithms/Python/blob/master/other/integeration_by_simpson_approx.py) * [Largest Subarray Sum](https://github.com/TheAlgorithms/Python/blob/master/other/largest_subarray_sum.py) @@ -840,6 +845,7 @@ * [Merge Insertion Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/merge_insertion_sort.py) * [Merge Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/merge_sort.py) * [Natural Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/natural_sort.py) + * [Odd Even Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/odd_even_sort.py) * [Odd Even Transposition Parallel](https://github.com/TheAlgorithms/Python/blob/master/sorts/odd_even_transposition_parallel.py) * [Odd Even Transposition Single Threaded](https://github.com/TheAlgorithms/Python/blob/master/sorts/odd_even_transposition_single_threaded.py) * [Pancake Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/pancake_sort.py) @@ -856,6 +862,7 @@ * [Recursive Quick Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/recursive_quick_sort.py) * [Selection Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/selection_sort.py) * [Shell Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/shell_sort.py) + * [Slowsort](https://github.com/TheAlgorithms/Python/blob/master/sorts/slowsort.py) * [Stooge Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/stooge_sort.py) * [Strand Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/strand_sort.py) * [Tim Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/tim_sort.py) diff --git a/other/graham_scan.py b/other/graham_scan.py new file mode 100644 index 000000000..67c5cd8ab --- /dev/null +++ b/other/graham_scan.py @@ -0,0 +1,171 @@ +""" +This is a pure Python implementation of the merge-insertion sort algorithm +Source: https://en.wikipedia.org/wiki/Graham_scan + +For doctests run following command: +python3 -m doctest -v graham_scan.py +""" + +from __future__ import annotations + +from collections import deque +from enum import Enum +from math import atan2, degrees +from sys import maxsize + + +def graham_scan(points: list[list[int, int]]) -> list[list[int, int]]: + """Pure implementation of graham scan algorithm in Python + + :param points: The unique points on coordinates. + :return: The points on convex hell. + + Examples: + >>> graham_scan([(9, 6), (3, 1), (0, 0), (5, 5), (5, 2), (7, 0), (3, 3), (1, 4)]) + [(0, 0), (7, 0), (9, 6), (5, 5), (1, 4)] + + >>> graham_scan([(0, 0), (1, 0), (1, 1), (0, 1)]) + [(0, 0), (1, 0), (1, 1), (0, 1)] + + >>> graham_scan([(0, 0), (1, 1), (2, 2), (3, 3), (-1, 2)]) + [(0, 0), (1, 1), (2, 2), (3, 3), (-1, 2)] + + >>> graham_scan([(-100, 20), (99, 3), (1, 10000001), (5133186, -25), (-66, -4)]) + [(5133186, -25), (1, 10000001), (-100, 20), (-66, -4)] + """ + + if len(points) <= 2: + # There is no convex hull + raise ValueError("graham_scan: argument must contain more than 3 points.") + if len(points) == 3: + return points + # find the lowest and the most left point + minidx = 0 + miny, minx = maxsize, maxsize + for i, point in enumerate(points): + x = point[0] + y = point[1] + if y < miny: + miny = y + minx = x + minidx = i + if y == miny: + if x < minx: + minx = x + minidx = i + + # remove the lowest and the most left point from points for preparing for sort + points.pop(minidx) + + def angle_comparer(point: list[int, int], minx: int, miny: int) -> float: + """Return the angle toward to point from (minx, miny) + + :param point: The target point + minx: The starting point's x + miny: The starting point's y + :return: the angle + + Examples: + >>> angle_comparer([1,1], 0, 0) + 45.0 + + >>> angle_comparer([100,1], 10, 10) + -5.710593137499642 + + >>> angle_comparer([5,5], 2, 3) + 33.690067525979785 + """ + # sort the points accorgind to the angle from the lowest and the most left point + x = point[0] + y = point[1] + angle = degrees(atan2(y - miny, x - minx)) + return angle + + sorted_points = sorted(points, key=lambda point: angle_comparer(point, minx, miny)) + # This insert actually costs complexity, + # and you should insteadly add (minx, miny) into stack later. + # I'm using insert just for easy understanding. + sorted_points.insert(0, (minx, miny)) + + # traversal from the lowest and the most left point in anti-clockwise direction + # if direction gets right, the previous point is not the convex hull. + class Direction(Enum): + left = 1 + straight = 2 + right = 3 + + def check_direction( + starting: list[int, int], via: list[int, int], target: list[int, int] + ) -> Direction: + """Return the direction toward to the line from via to target from starting + + :param starting: The starting point + via: The via point + target: The target point + :return: the Direction + + Examples: + >>> check_direction([1,1], [2,2], [3,3]) + Direction.straight + + >>> check_direction([60,1], [-50,199], [30,2]) + Direction.left + + >>> check_direction([0,0], [5,5], [10,0]) + Direction.right + """ + x0, y0 = starting + x1, y1 = via + x2, y2 = target + via_angle = degrees(atan2(y1 - y0, x1 - x0)) + if via_angle < 0: + via_angle += 360 + target_angle = degrees(atan2(y2 - y0, x2 - x0)) + if target_angle < 0: + target_angle += 360 + # t- + # \ \ + # \ v + # \| + # s + # via_angle is always lower than target_angle, if direction is left. + # If they are same, it means they are on a same line of convex hull. + if target_angle > via_angle: + return Direction.left + if target_angle == via_angle: + return Direction.straight + if target_angle < via_angle: + return Direction.right + + stack = deque() + stack.append(sorted_points[0]) + stack.append(sorted_points[1]) + stack.append(sorted_points[2]) + # In any ways, the first 3 points line are towards left. + # Because we sort them the angle from minx, miny. + current_direction = Direction.left + + for i in range(3, len(sorted_points)): + while True: + starting = stack[-2] + via = stack[-1] + target = sorted_points[i] + next_direction = check_direction(starting, via, target) + + if next_direction == Direction.left: + current_direction = Direction.left + break + if next_direction == Direction.straight: + if current_direction == Direction.left: + # We keep current_direction as left. + # Because if the straight line keeps as straight, + # we want to know if this straight line is towards left. + break + elif current_direction == Direction.right: + # If the straight line is towards right, + # every previous points on those straigh line is not convex hull. + stack.pop() + if next_direction == Direction.right: + stack.pop() + stack.append(sorted_points[i]) + return list(stack)