Skip to content

Commit 2faf470

Browse files
authored
Merge branch 'master' into fix/binary-insertion-comparable
2 parents bf627dc + 3f4ce7a commit 2faf470

20 files changed

Lines changed: 1444 additions & 30 deletions

.github/pull_request_template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@
1717
* [ ] All function parameters and return values are annotated with Python [type hints](https://docs.python.org/3/library/typing.html).
1818
* [ ] All functions have [doctests](https://docs.python.org/3/library/doctest.html) that pass the automated testing.
1919
* [ ] All new algorithms include at least one URL that points to Wikipedia or another similar explanation.
20-
* [ ] If this pull request resolves one or more open issues then the description above includes the issue number(s) with a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue): "Fixes #ISSUE-NUMBER".
20+
* [ ] If this pull request resolves one or more open issues, then the description above includes the issue number(s) with a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue): "Fixes #ISSUE-NUMBER".

.github/skills/new-pull-request/SKILL.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ hand. A hand-modified `uv.lock` makes the `algorithms-keeper` bot close the pull
1919
request as invalid, and even a repo maintainer cannot undo that.
2020

2121
Always check at least one Markdown checkbox in the pull request description (the "Describe your change" section), or the
22-
`algorithms-keeper` bot will close the pull request as invalid. Any repo maintainer can undo this if you @mention them on the closed pull request.
22+
`algorithms-keeper` bot will close the pull request as invalid — and it does
23+
this *before* a human reads the PR, so a genuinely good change gets closed for a
24+
formatting reason. This applies to **every** pull request, including CI, docs,
25+
and tooling changes that are not algorithms: tick the boxes that genuinely apply
26+
so the body is never submitted with all boxes empty. Any repo maintainer can
27+
undo this if you @mention them on the closed pull request, but re-opening is
28+
often unreliable, so it is far better to get it right the first time.
2329

2430
### 1. Before contributing / Is this an algorithm?
2531

@@ -55,3 +61,15 @@ Always check at least one Markdown checkbox in the pull request description (the
5561
- [ ] At least one **Wikipedia (or equivalent) URL** documenting the algorithm.
5662
- [ ] Docstring explains what the function does and its parameters/returns.
5763
- [ ] No unnecessary third-party dependencies.
64+
65+
## Before you click "Create pull request"
66+
67+
This is the final gate. Do not open the pull request until every item here is true:
68+
69+
- [ ] At least one Markdown checkbox in the PR description is checked. **Verify
70+
this by re-reading the rendered body** — if every box is still `- [ ]`, the
71+
`algorithms-keeper` bot will auto-close the PR before any human sees it.
72+
Check the boxes that genuinely apply to this change; never submit an
73+
all-empty checklist, even for a CI, docs, or tooling PR.
74+
- [ ] The branch is not `master`, and `master` is synced with `upstream/master`.
75+
- [ ] `uv.lock` was not hand-edited.

.github/zizmor.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ rules:
22
excessive-permissions:
33
disable: true
44
unpinned-uses:
5-
disable: true
6-
# config:
7-
# policies:
8-
# actions/*: ref-pin
5+
config:
6+
policies:
7+
"*": ref-pin

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,7 @@
13701370
* [Dutch National Flag Sort](sorts/dutch_national_flag_sort.py)
13711371
* [Exchange Sort](sorts/exchange_sort.py)
13721372
* [External Sort](sorts/external_sort.py)
1373+
* [Flash Sort](sorts/flash_sort.py)
13731374
* [Gnome Sort](sorts/gnome_sort.py)
13741375
* [Heap Sort](sorts/heap_sort.py)
13751376
* [Insertion Sort](sorts/insertion_sort.py)

sorts/adaptive_merge_sort.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
def adaptive_merge_sort(sequence: list) -> list:
2+
if len(sequence) < 2:
3+
return sequence
4+
5+
aux = sequence[:]
6+
print(f"Initial sequence: {sequence}")
7+
adaptive_merge_sort_helper(sequence, aux, 0, len(sequence) - 1)
8+
print(f"Sorted sequence: {sequence}")
9+
return sequence
10+
11+
12+
def adaptive_merge_sort_helper(array: list, aux: list, low: int, high: int) -> None:
13+
if high <= low:
14+
return
15+
16+
mid = (low + high) // 2
17+
print(f"Sorting: array[{low}:{mid + 1}] and array[{mid + 1}:{high + 1}]")
18+
19+
adaptive_merge_sort_helper(aux, array, low, mid)
20+
adaptive_merge_sort_helper(aux, array, mid + 1, high)
21+
22+
if array[mid] <= array[mid + 1]:
23+
print(f"Skipping merge as array[{mid}] <= array[{mid + 1}]")
24+
array[low : high + 1] = aux[low : high + 1]
25+
return
26+
27+
merge(array, aux, low, mid, high)
28+
29+
30+
def merge(array: list, aux: list, low: int, mid: int, high: int) -> None:
31+
print(f"Merging: array[{low}:{mid + 1}] and array[{mid + 1}:{high + 1}]")
32+
33+
i, j = low, mid + 1
34+
for k in range(low, high + 1):
35+
if i > mid or j > high:
36+
if i > mid:
37+
aux[k] = array[j]
38+
j += 1
39+
else:
40+
aux[k] = array[i]
41+
i += 1
42+
elif array[i] <= array[j]:
43+
aux[k] = array[i]
44+
i += 1
45+
else:
46+
aux[k] = array[j]
47+
j += 1
48+
49+
for k in range(low, high + 1):
50+
array[k] = aux[k]
51+
52+
print(f"After merge: {array[low:high + 1]}")
53+
54+
55+
# Example usage
56+
if __name__ == "__main__":
57+
print(adaptive_merge_sort([4, 3, 1, 2]))

sorts/bubble_sort_recursive.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
def bubble_sort_recursive(arr: list[int]) -> list[int]:
2+
"""
3+
Sorts a list of integers using the recursive Bubble Sort algorithm.
4+
5+
>>> bubble_sort_recursive([5, 1, 4, 2, 8])
6+
[1, 2, 4, 5, 8]
7+
>>> bubble_sort_recursive([])
8+
[]
9+
>>> bubble_sort_recursive([1])
10+
[1]
11+
>>> bubble_sort_recursive([3, 3, 2, 1])
12+
[1, 2, 3, 3]
13+
>>> bubble_sort_recursive([-1, 5, 0, -2])
14+
[-2, -1, 0, 5]
15+
"""
16+
n = len(arr)
17+
if n <= 1:
18+
return arr
19+
20+
swapped = False
21+
for i in range(n - 1):
22+
if arr[i] > arr[i + 1]:
23+
arr[i], arr[i + 1] = arr[i + 1], arr[i]
24+
swapped = True
25+
26+
if not swapped:
27+
return arr
28+
29+
return [*bubble_sort_recursive(arr[:-1]), arr[-1]]
30+
31+
32+
if __name__ == "__main__":
33+
import doctest
34+
35+
doctest.testmod()

sorts/comb_sort.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,19 @@ def comb_sort(data: list) -> list:
3030
[]
3131
>>> comb_sort([99, 45, -7, 8, 2, 0, -15, 3])
3232
[-15, -7, 0, 2, 3, 8, 45, 99]
33+
>>> comb_sort([2, 0, 3, 4, 5, 6, 1])
34+
[0, 1, 2, 3, 4, 5, 6]
3335
"""
3436
shrink_factor = 1.3
3537
gap = len(data)
3638
completed = False
3739

3840
while not completed:
39-
# Update the gap value for a next comb
40-
gap = int(gap / shrink_factor)
41-
if gap <= 1:
41+
# Update the gap value for a next comb. The gap is never allowed to drop
42+
# below 1: a gap of 0 compares each element with itself, so no swap can
43+
# ever happen and the loop would exit while the data is still unsorted.
44+
gap = max(int(gap / shrink_factor), 1)
45+
if gap == 1:
4246
completed = True
4347

4448
index = 0

sorts/cyclic_sort.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
python -m doctest -v cyclic_sort.py
66
or
77
python3 -m doctest -v cyclic_sort.py
8+
89
For manual testing run:
910
python cyclic_sort.py
1011
or
@@ -27,20 +28,42 @@ def cyclic_sort(nums: list[int]) -> list[int]:
2728
[]
2829
>>> cyclic_sort([3, 5, 2, 1, 4])
2930
[1, 2, 3, 4, 5]
31+
32+
>>> cyclic_sort([1, 2, 2])
33+
Traceback (most recent call last):
34+
...
35+
ValueError: All numbers must be unique, got 2
36+
37+
>>> cyclic_sort([1, 5])
38+
Traceback (most recent call last):
39+
...
40+
ValueError: All numbers must be in range 1 to 2, got 5
3041
"""
3142

43+
# Input validation
44+
seen = set()
45+
n = len(nums)
46+
47+
for num in nums:
48+
if num in seen:
49+
message = f"All numbers must be unique, got {num}"
50+
raise ValueError(message)
51+
52+
if num < 1 or num > n:
53+
message = f"All numbers must be in range 1 to {n}, got {num}"
54+
raise ValueError(message)
55+
56+
seen.add(num)
57+
3258
# Perform cyclic sort
3359
index = 0
3460
while index < len(nums):
35-
# Calculate the correct index for the current element
3661
correct_index = nums[index] - 1
37-
# If the current element is not at its correct position,
38-
# swap it with the element at its correct index
62+
3963
if index != correct_index:
4064
nums[index], nums[correct_index] = nums[correct_index], nums[index]
65+
4166
else:
42-
# If the current element is already in its correct position,
43-
# move to the next element
4467
index += 1
4568

4669
return nums
@@ -50,6 +73,7 @@ def cyclic_sort(nums: list[int]) -> list[int]:
5073
import doctest
5174

5275
doctest.testmod()
76+
5377
user_input = input("Enter numbers separated by a comma:\n").strip()
5478
unsorted = [int(item) for item in user_input.split(",")]
5579
print(*cyclic_sort(unsorted), sep=",")

sorts/flash_sort.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Flash Sort Algorithm Implementation
4+
5+
Flash sort is a distribution sorting algorithm showing linear computational
6+
complexity O(n) for uniformly distributed datasets and relatively little
7+
additional memory requirement. The basic idea is to use the distribution
8+
of the values to be sorted to determine their approximate final positions
9+
directly, without comparing and moving each element through many intermediate
10+
positions as done by other algorithms.
11+
12+
The algorithm was developed by Karl-Dietrich Neubert in 1998 and builds upon
13+
the idea of bucket sort. It works by classifying elements into classes and
14+
then sorting each class.
15+
16+
Time Complexity:
17+
- Best Case: O(n) when data is uniformly distributed
18+
- Average Case: O(n + k) where k is the number of classes
19+
- Worst Case: O(n²) when data is not uniformly distributed
20+
21+
Space Complexity: O(k) where k is the number of classes
22+
23+
Source: https://en.wikipedia.org/wiki/Flashsort
24+
"""
25+
26+
from __future__ import annotations
27+
28+
29+
def flash_sort(arr: list[int | float]) -> list[int | float]:
30+
"""
31+
Sorts a list using the Flash Sort algorithm.
32+
33+
Flash sort is particularly efficient for uniformly distributed data.
34+
It uses the distribution of values to determine approximate positions.
35+
36+
Args:
37+
arr: List of integers or floats to be sorted
38+
39+
Returns:
40+
Sorted list in ascending order
41+
42+
Examples:
43+
>>> flash_sort([4, 2, 7, 1, 9, 3])
44+
[1, 2, 3, 4, 7, 9]
45+
>>> flash_sort([])
46+
[]
47+
>>> flash_sort([5])
48+
[5]
49+
>>> flash_sort([3, 3, 3, 3])
50+
[3, 3, 3, 3]
51+
>>> flash_sort([-1, -5, 0, 3, 2])
52+
[-5, -1, 0, 2, 3]
53+
>>> flash_sort([1.5, 2.3, 0.1, 3.7, 1.2])
54+
[0.1, 1.2, 1.5, 2.3, 3.7]
55+
>>> flash_sort([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])
56+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
57+
>>> import random
58+
>>> data = random.sample(range(100), 20)
59+
>>> flash_sort(data) == sorted(data)
60+
True
61+
>>> flash_sort([42])
62+
[42]
63+
>>> flash_sort([2.5, 1.1, 3.3, 2.5, 1.1])
64+
[1.1, 1.1, 2.5, 2.5, 3.3]
65+
"""
66+
if len(arr) <= 1:
67+
return arr.copy()
68+
69+
# Create a copy to avoid modifying the original array
70+
result = arr.copy()
71+
n = len(result)
72+
73+
# Find min and max values
74+
min_val = min(result)
75+
max_val = max(result)
76+
77+
# If all elements are the same, return the array
78+
if min_val == max_val:
79+
return result
80+
81+
# Number of classes (buckets) - typically n/10 to n/5 works well
82+
m = max(1, int(0.45 * n))
83+
84+
# Initialize class sizes array
85+
class_sizes = [0] * m
86+
87+
# Calculate class sizes
88+
c1 = (m - 1) / (max_val - min_val)
89+
90+
for value in result:
91+
class_index = int(c1 * (value - min_val))
92+
if class_index >= m:
93+
class_index = m - 1
94+
class_sizes[class_index] += 1
95+
96+
# Calculate cumulative class sizes (positions)
97+
for i in range(1, m):
98+
class_sizes[i] += class_sizes[i - 1]
99+
100+
# Permutation phase
101+
hold = result[0]
102+
j = 0
103+
k = m - 1
104+
105+
while j < n - 1:
106+
while j >= class_sizes[k]:
107+
k -= 1
108+
109+
flash = int(c1 * (hold - min_val))
110+
if flash >= m:
111+
flash = m - 1
112+
113+
while j < class_sizes[flash]:
114+
k = flash
115+
class_sizes[k] -= 1
116+
result[j], result[class_sizes[k]] = result[class_sizes[k]], result[j]
117+
hold = result[j]
118+
j += 1
119+
flash = int(c1 * (hold - min_val))
120+
if flash >= m:
121+
flash = m - 1
122+
123+
j += 1
124+
if j < n:
125+
hold = result[j]
126+
127+
# Insertion sort for final sorting within classes
128+
for i in range(1, n):
129+
key = result[i]
130+
j = i - 1
131+
while j >= 0 and result[j] > key:
132+
result[j + 1] = result[j]
133+
j -= 1
134+
result[j + 1] = key
135+
136+
return result
137+
138+
139+
if __name__ == "__main__":
140+
from doctest import testmod
141+
142+
testmod()
143+
144+
# Additional test cases
145+
test_cases: list[list[int | float]] = [
146+
[64, 34, 25, 12, 22, 11, 90],
147+
[5, 2, 4, 6, 1, 3],
148+
[1],
149+
[],
150+
[3, 3, 3, 3],
151+
[-1, -3, 2, 0, -5],
152+
[1.1, 2.2, 0.5, 3.3, 1.5],
153+
]
154+
155+
for test_case in test_cases:
156+
sorted_result = flash_sort(test_case)
157+
expected = sorted(test_case)
158+
assert sorted_result == expected, f"Failed for {test_case}"
159+
print(f"✓ {test_case} -> {sorted_result}")
160+
161+
print("All tests passed!")

0 commit comments

Comments
 (0)