Skip to content

Commit d8d3725

Browse files
Simon Lammershermanhui
Simon Lammer
authored andcommitted
Implement the melkman anlgorithm for computing convex hulls (TheAlgorithms#2916)
* Implement the melkman anlgorithm for computing convex hulls * Link melkman algorithm description * Format melkman algorithm code * Add type hints to functions * Fix build errors
1 parent 2aff9e1 commit d8d3725

File tree

1 file changed

+105
-36
lines changed

1 file changed

+105
-36
lines changed

Diff for: divide_and_conquer/convex_hull.py

+105-36
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
1414
"""
1515

16+
from typing import Iterable, List, Set, Union
17+
1618

1719
class Point:
1820
"""
@@ -81,7 +83,9 @@ def __hash__(self):
8183
return hash(self.x)
8284

8385

84-
def _construct_points(list_of_tuples):
86+
def _construct_points(
87+
list_of_tuples: Union[List[Point], List[List[float]], Iterable[List[float]]]
88+
) -> List[Point]:
8589
"""
8690
constructs a list of points from an array-like object of numbers
8791
@@ -110,20 +114,23 @@ def _construct_points(list_of_tuples):
110114
[]
111115
"""
112116

113-
points = []
117+
points: List[Point] = []
114118
if list_of_tuples:
115119
for p in list_of_tuples:
116-
try:
117-
points.append(Point(p[0], p[1]))
118-
except (IndexError, TypeError):
119-
print(
120-
f"Ignoring deformed point {p}. All points"
121-
" must have at least 2 coordinates."
122-
)
120+
if isinstance(p, Point):
121+
points.append(p)
122+
else:
123+
try:
124+
points.append(Point(p[0], p[1]))
125+
except (IndexError, TypeError):
126+
print(
127+
f"Ignoring deformed point {p}. All points"
128+
" must have at least 2 coordinates."
129+
)
123130
return points
124131

125132

126-
def _validate_input(points):
133+
def _validate_input(points: Union[List[Point], List[List[float]]]) -> List[Point]:
127134
"""
128135
validates an input instance before a convex-hull algorithms uses it
129136
@@ -165,33 +172,18 @@ def _validate_input(points):
165172
ValueError: Expecting an iterable object but got an non-iterable type 1
166173
"""
167174

175+
if not hasattr(points, "__iter__"):
176+
raise ValueError(
177+
f"Expecting an iterable object but got an non-iterable type {points}"
178+
)
179+
168180
if not points:
169181
raise ValueError(f"Expecting a list of points but got {points}")
170182

171-
if isinstance(points, set):
172-
points = list(points)
173-
174-
try:
175-
if hasattr(points, "__iter__") and not isinstance(points[0], Point):
176-
if isinstance(points[0], (list, tuple)):
177-
points = _construct_points(points)
178-
else:
179-
raise ValueError(
180-
"Expecting an iterable of type Point, list or tuple. "
181-
f"Found objects of type {type(points[0])} instead"
182-
)
183-
elif not hasattr(points, "__iter__"):
184-
raise ValueError(
185-
f"Expecting an iterable object but got an non-iterable type {points}"
186-
)
187-
except TypeError:
188-
print("Expecting an iterable of type Point, list or tuple.")
189-
raise
190-
191-
return points
183+
return _construct_points(points)
192184

193185

194-
def _det(a, b, c):
186+
def _det(a: Point, b: Point, c: Point) -> float:
195187
"""
196188
Computes the sign perpendicular distance of a 2d point c from a line segment
197189
ab. The sign indicates the direction of c relative to ab.
@@ -226,7 +218,7 @@ def _det(a, b, c):
226218
return det
227219

228220

229-
def convex_hull_bf(points):
221+
def convex_hull_bf(points: List[Point]) -> List[Point]:
230222
"""
231223
Constructs the convex hull of a set of 2D points using a brute force algorithm.
232224
The algorithm basically considers all combinations of points (i, j) and uses the
@@ -299,7 +291,7 @@ def convex_hull_bf(points):
299291
return sorted(convex_set)
300292

301293

302-
def convex_hull_recursive(points):
294+
def convex_hull_recursive(points: List[Point]) -> List[Point]:
303295
"""
304296
Constructs the convex hull of a set of 2D points using a divide-and-conquer strategy
305297
The algorithm exploits the geometric properties of the problem by repeatedly
@@ -369,7 +361,9 @@ def convex_hull_recursive(points):
369361
return sorted(convex_set)
370362

371363

372-
def _construct_hull(points, left, right, convex_set):
364+
def _construct_hull(
365+
points: List[Point], left: Point, right: Point, convex_set: Set[Point]
366+
) -> None:
373367
"""
374368
375369
Parameters
@@ -411,6 +405,77 @@ def _construct_hull(points, left, right, convex_set):
411405
_construct_hull(candidate_points, extreme_point, right, convex_set)
412406

413407

408+
def convex_hull_melkman(points: List[Point]) -> List[Point]:
409+
"""
410+
Constructs the convex hull of a set of 2D points using the melkman algorithm.
411+
The algorithm works by iteratively inserting points of a simple polygonal chain
412+
(meaning that no line segments between two consecutive points cross each other).
413+
Sorting the points yields such a polygonal chain.
414+
415+
For a detailed description, see http://cgm.cs.mcgill.ca/~athens/cs601/Melkman.html
416+
417+
Runtime: O(n log n) - O(n) if points are already sorted in the input
418+
419+
Parameters
420+
---------
421+
points: array-like of object of Points, lists or tuples.
422+
The set of 2d points for which the convex-hull is needed
423+
424+
Returns
425+
------
426+
convex_set: list, the convex-hull of points sorted in non-decreasing order.
427+
428+
See Also
429+
--------
430+
431+
Examples
432+
---------
433+
>>> convex_hull_melkman([[0, 0], [1, 0], [10, 1]])
434+
[(0.0, 0.0), (1.0, 0.0), (10.0, 1.0)]
435+
>>> convex_hull_melkman([[0, 0], [1, 0], [10, 0]])
436+
[(0.0, 0.0), (10.0, 0.0)]
437+
>>> convex_hull_melkman([[-1, 1],[-1, -1], [0, 0], [0.5, 0.5], [1, -1], [1, 1],
438+
... [-0.75, 1]])
439+
[(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)]
440+
>>> convex_hull_melkman([(0, 3), (2, 2), (1, 1), (2, 1), (3, 0), (0, 0), (3, 3),
441+
... (2, -1), (2, -4), (1, -3)])
442+
[(0.0, 0.0), (0.0, 3.0), (1.0, -3.0), (2.0, -4.0), (3.0, 0.0), (3.0, 3.0)]
443+
"""
444+
points = sorted(_validate_input(points))
445+
n = len(points)
446+
447+
convex_hull = points[:2]
448+
for i in range(2, n):
449+
det = _det(convex_hull[1], convex_hull[0], points[i])
450+
if det > 0:
451+
convex_hull.insert(0, points[i])
452+
break
453+
elif det < 0:
454+
convex_hull.append(points[i])
455+
break
456+
else:
457+
convex_hull[1] = points[i]
458+
i += 1
459+
460+
for i in range(i, n):
461+
if (
462+
_det(convex_hull[0], convex_hull[-1], points[i]) > 0
463+
and _det(convex_hull[-1], convex_hull[0], points[1]) < 0
464+
):
465+
# The point lies within the convex hull
466+
continue
467+
468+
convex_hull.insert(0, points[i])
469+
convex_hull.append(points[i])
470+
while _det(convex_hull[0], convex_hull[1], convex_hull[2]) >= 0:
471+
del convex_hull[1]
472+
while _det(convex_hull[-1], convex_hull[-2], convex_hull[-3]) <= 0:
473+
del convex_hull[-2]
474+
475+
# `convex_hull` is contains the convex hull in circular order
476+
return sorted(convex_hull[1:] if len(convex_hull) > 3 else convex_hull)
477+
478+
414479
def main():
415480
points = [
416481
(0, 3),
@@ -426,10 +491,14 @@ def main():
426491
]
427492
# the convex set of points is
428493
# [(0, 0), (0, 3), (1, -3), (2, -4), (3, 0), (3, 3)]
429-
results_recursive = convex_hull_recursive(points)
430494
results_bf = convex_hull_bf(points)
495+
496+
results_recursive = convex_hull_recursive(points)
431497
assert results_bf == results_recursive
432498

499+
results_melkman = convex_hull_melkman(points)
500+
assert results_bf == results_melkman
501+
433502
print(results_bf)
434503

435504

0 commit comments

Comments
 (0)