A while ago, I wrote a new sort algorithm.
If you haven't read it, please check it out before reading. New sort algorithm?
Read it? OK. Let's go.
Review of the original algorithm
Today, I upgraded it.
The original algorithm is a bit slow, a lot of
pop
,
insert
operations, so I tried to speed it up.
The original algorithm is like this (I added copy of the input list to avoid modifying it, and debug flag to activate printing, but it's not important for the algorithm itself.):
def sort(arr: list, debug=False):
iterables_list = [copy.copy(arr),]
while len(iterables_list[-1]) > 1:
iterables_list.append([])
if debug:
print("a", iterables_list)
i = 0
while i < len(iterables_list[-2])-1:
if iterables_list[-2][i] > iterables_list[-2][i+1]:
iterables_list[-1].append(iterables_list[-2][i+1])
iterables_list[-2].pop(i+1)
if debug:
print("b", iterables_list)
else:
i += 1
if debug:
print("c", iterables_list)
if len(iterables_list[-1]) == 0:
iterables_list.pop()
while len(iterables_list) > 1:
i = 0
while len(iterables_list[-1]) > 0:
if iterables_list[-1][0] < iterables_list[-2][i]:
iterables_list[-2].insert(i, iterables_list[-1][0])
iterables_list[-1].pop(0)
if debug:
print("d", iterables_list)
else:
i += 1
iterables_list.pop()
if debug:
print("e", iterables_list)
return iterables_list[0]
While this algorithm uses
pop
and
insert
a lot, I tried to avoid them in the upgraded version.
Upgraded version
The upgraded version is this:
def new_sort(arr: list, debug=False):
iterable = copy.copy(arr)
layer = [0 for _ in range(len(iterable))]
layer_count = [len(iterable)]
while layer_count[-1] > 1:
current_layer_index = len(layer_count) - 1
layer_count.append(0)
if debug:
print("a", layer, layer_count)
i = 0
while i < len(iterable) and layer[i] != current_layer_index:
i += 1
j = i + 1
while j < len(iterable):
if layer[j] == current_layer_index:
if iterable[i] > iterable[j]:
layer[j] += 1
layer_count[current_layer_index + 1] += 1
layer_count[current_layer_index] -= 1
if debug:
print("b", layer, layer_count)
else:
i = j
j += 1
if debug:
print("c", layer, layer_count)
if layer_count[-1] == 0:
layer_count.pop()
result = []
while len(layer_count) > 1:
i = 0
j = 0
result = []
new_layer = []
while layer_count[-1] > 0:
while not layer[i] == len(layer_count) - 1:
i += 1
while not layer[j] == len(layer_count) - 2:
j += 1
if layer[i] == len(layer_count) - 1 and layer[j] == len(layer_count) - 2:
if iterable[i] < iterable[j]:
result.append(iterable[i])
new_layer.append(layer[i] - 1)
layer_count[-1] -= 1
layer_count[-2] += 1
if debug:
print("d", new_layer, layer_count, result)
i += 1
else:
result.append(iterable[j])
new_layer.append(layer[j])
if debug:
print("d", new_layer, layer_count, result)
j += 1
while j < len(iterable):
if layer[j] == len(layer_count) - 2:
result.append(iterable[j])
new_layer.append(layer[j])
if debug:
print("d", new_layer, layer_count, result)
j += 1
for i in range(len(layer)):
if layer[i] < len(layer_count) - 2:
result.append(iterable[i])
new_layer.append(layer[i])
if debug:
print("e", new_layer, layer_count, result)
iterable = result
layer = new_layer
layer_count.pop()
return result
The main idea is to virtually split the list into layers with
layer
list, and increment/decrement layer indices instead of popping and inserting them.
This way, we can avoid the overhead of
pop
and
insert
, and the algorithm should be faster.
Instead of this, the code became more complex and longer, but I think it's worth it.
Benchmark
I benchmarked the original and upgraded versions with random lists of different sizes.
def benchmark(sort_func, n=200, trials=10):
times = []
for _ in range(trials):
target = [random.randint(0, 1000) for _ in range(n)]
start = time.perf_counter()
sort_func(target.copy())
end = time.perf_counter()
times.append(end - start)
avg = statistics.mean(times)
stdev = statistics.stdev(times) if len(times) > 1 else 0
return avg, stdev
if __name__ == "__main__":
N_SIZE = 30000
TRIALS = 10
print(f"--- Start Benchmark (Size:{N_SIZE}, Trials:{TRIALS}) ---")
avg1, dev1 = benchmark(sort, N_SIZE, TRIALS)
print(f"Old Version: Average {avg1:.5f}s (±{dev1:.5f})")
avg2, dev2 = benchmark(new_sort, N_SIZE, TRIALS)
print(f"Upgraded Version: Average {avg2:.5f}s (±{dev2:.5f})")
improvement = (avg1 / avg2) if avg2 > 0 else 0
print(f"\nResult: The upgraded version is approximately {improvement:.1f} times faster than the old version.")
The results are below.
| Size | Times | Old Version | Upgraded Version | Improvement |
|---|---|---|---|---|
| 1000 | 10 | 0.01483s (±0.00237) | 0.01812s (±0.00160) | 0.8 times |
| 5000 | 10 | 0.25094s (±0.03296) | 0.20472s (±0.01751) | 1.2 times |
| 10000 | 10 | 0.95736s (±0.08883) | 0.59673s (±0.03286) | 1.6 times |
| 30000 | 10 | 10.53698s (±0.67754) | 2.99876s (±0.22899) | 3.5 times |
As you can see, the upgraded version is faster and more consistent than the old version, especially as the size of the list increases.
The improvement is significant, especially for larger lists, where the upgraded version is approximately 3.5 times faster than the old version.
Conclusion
In this article, I upgraded a new sort algorithm that I wrote a while ago.
The upgraded version is faster and more consistent than the old version, especially as the size of the list increases.
The main idea is still the same, compare and drop layers because this idea is what makes this algorithm (maybe) unique.
I hope you enjoyed this article, and if you have any questions or suggestions, please let me know through form or twitter.
print("see you next time!")