From 45a23c7b3ba2321c62186b0863c012d8f90b261a Mon Sep 17 00:00:00 2001 From: Akshat Singh Kushwaha Date: Sun, 5 Oct 2025 15:36:21 +0530 Subject: [PATCH 1/8] added powersort in sorts/power_sort.py --- .gitignore | 1 + sorts/power_sort.py | 353 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 sorts/power_sort.py diff --git a/.gitignore b/.gitignore index baea84b8d1f1..f0642f128f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ venv.bak/ .try .vscode/ .vs/ +.cursor/ \ No newline at end of file diff --git a/sorts/power_sort.py b/sorts/power_sort.py new file mode 100644 index 000000000000..998eff7c2fc8 --- /dev/null +++ b/sorts/power_sort.py @@ -0,0 +1,353 @@ +""" +PowerSort - An adaptive merge sort algorithm. + +PowerSort is an adaptive, stable sorting algorithm that efficiently handles +partially ordered data by optimally merging existing runs (consecutive sequences +of sorted elements) in the input. It was developed by J. Ian Munro and Sebastian +Wild and has been integrated into Python's standard library since version 3.11. + +The algorithm works by: +1. Detecting naturally occurring runs (ascending or descending sequences) +2. Using a power-based merge strategy to determine optimal merge order +3. Maintaining a stack of runs and merging based on calculated node powers + +Time Complexity: O(n log n) worst case, O(n) for nearly sorted data +Space Complexity: O(n) for merge buffer + +For doctests run: +python -m doctest -v power_sort.py + +For manual testing run: +python power_sort.py +""" + +from __future__ import annotations + +from typing import Any, Callable + + +def _find_run( + arr: list, start: int, end: int, key: Callable[[Any], Any] | None = None +) -> int: + """ + Detect a run (ascending or descending sequence) starting at 'start'. + + If the run is descending, reverse it in-place to make it ascending. + Returns the end index (exclusive) of the detected run. + + Args: + arr: The list to search in + start: Starting index of the run + end: End index (exclusive) of the search range + key: Optional key function for comparisons + + Returns: + End index (exclusive) of the detected run + + >>> arr = [3, 2, 1, 4, 5, 6] + >>> _find_run(arr, 0, 6) + 3 + >>> arr + [1, 2, 3, 4, 5, 6] + >>> arr = [1, 2, 3, 2, 1] + >>> _find_run(arr, 0, 5) + 3 + >>> arr + [1, 2, 3, 2, 1] + """ + if start >= end - 1: + return start + 1 + + key_func = key if key else lambda x: x + run_end = start + 1 + + # Check if run is ascending or descending + if key_func(arr[run_end]) < key_func(arr[start]): + # Descending run + while run_end < end and key_func(arr[run_end]) < key_func(arr[run_end - 1]): + run_end += 1 + # Reverse the descending run to make it ascending + arr[start:run_end] = reversed(arr[start:run_end]) + else: + # Ascending run + while run_end < end and key_func(arr[run_end]) >= key_func(arr[run_end - 1]): + run_end += 1 + + return run_end + + +def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: + """ + Calculate the node power for two adjacent runs. + + This determines the merge priority in the stack. The power is the smallest + integer p such that floor(a * 2^p) != floor(b * 2^p), where: + - a = (b1 + n1/2) / n + - b = (b2 + n2/2) / n + + Args: + n: Total length of the array + b1: Start index of first run + n1: Length of first run + b2: Start index of second run + n2: Length of second run + + Returns: + The calculated node power + + >>> _node_power(100, 0, 25, 25, 25) + 2 + >>> _node_power(100, 0, 50, 50, 50) + 1 + """ + # Calculate midpoints: a = (b1 + n1/2) / n, b = (b2 + n2/2) / n + # To avoid floating point, we work with a = (2*b1 + n1) / (2*n) and b = (2*b2 + n2) / (2*n) + # We want smallest p where floor(a * 2^p) != floor(b * 2^p) + # This is floor((2*b1 + n1) * 2^p / (2*n)) != floor((2*b2 + n2) * 2^p / (2*n)) + + a = 2 * b1 + n1 + b = 2 * b2 + n2 + two_n = 2 * n + + # Find smallest power p where floor(a * 2^p / two_n) != floor(b * 2^p / two_n) + power = 0 + while (a * (1 << power)) // two_n == (b * (1 << power)) // two_n: + power += 1 + + return power + + +def _merge( + arr: list, + start1: int, + end1: int, + end2: int, + key: Callable[[Any], Any] | None = None, +) -> None: + """ + Merge two adjacent sorted runs in-place using auxiliary space. + + Merges arr[start1:end1] with arr[end1:end2]. + + Args: + arr: The list containing the runs + start1: Start index of first run + end1: End index of first run (start of second run) + end2: End index of second run + key: Optional key function for comparisons + + >>> arr = [1, 3, 5, 2, 4, 6] + >>> _merge(arr, 0, 3, 6) + >>> arr + [1, 2, 3, 4, 5, 6] + >>> arr = [5, 6, 7, 1, 2, 3] + >>> _merge(arr, 0, 3, 6) + >>> arr + [1, 2, 3, 5, 6, 7] + """ + key_func = key if key else lambda x: x + + # Copy the runs to temporary storage + left = arr[start1:end1] + right = arr[end1:end2] + + i = j = 0 + k = start1 + + # Merge the two runs + while i < len(left) and j < len(right): + if key_func(left[i]) <= key_func(right[j]): + arr[k] = left[i] + i += 1 + else: + arr[k] = right[j] + j += 1 + k += 1 + + # Copy remaining elements + while i < len(left): + arr[k] = left[i] + i += 1 + k += 1 + + while j < len(right): + arr[k] = right[j] + j += 1 + k += 1 + + +def power_sort( + collection: list, + *, + key: Callable[[Any], Any] | None = None, + reverse: bool = False, +) -> list: + """ + Sort a list using the PowerSort algorithm. + + PowerSort is an adaptive merge sort that detects existing runs in the data + and uses a power-based merging strategy for optimal performance. + + Args: + collection: A mutable ordered collection with comparable items + key: Optional function to extract comparison key from each element + reverse: If True, sort in descending order + + Returns: + The same collection ordered according to the parameters + + Time Complexity: O(n log n) worst case, O(n) for nearly sorted data + Space Complexity: O(n) + + Examples: + >>> power_sort([0, 5, 3, 2, 2]) + [0, 2, 2, 3, 5] + >>> power_sort([]) + [] + >>> power_sort([1]) + [1] + >>> power_sort([-2, -5, -45]) + [-45, -5, -2] + >>> power_sort([1, 2, 3, 4, 5]) + [1, 2, 3, 4, 5] + >>> power_sort([5, 4, 3, 2, 1]) + [1, 2, 3, 4, 5] + >>> power_sort([3, 1, 4, 1, 5, 9, 2, 6, 5]) + [1, 1, 2, 3, 4, 5, 5, 6, 9] + >>> power_sort(['banana', 'apple', 'cherry']) + ['apple', 'banana', 'cherry'] + >>> power_sort([3.14, 2.71, 1.41, 1.73]) + [1.41, 1.73, 2.71, 3.14] + >>> power_sort([5, 2, 8, 1, 9], reverse=True) + [9, 8, 5, 2, 1] + >>> power_sort(['apple', 'pie', 'a', 'longer'], key=len) + ['a', 'pie', 'apple', 'longer'] + >>> power_sort([(1, 'b'), (2, 'a'), (1, 'a')], key=lambda x: x[0]) + [(1, 'b'), (1, 'a'), (2, 'a')] + >>> power_sort([1, 2, 3, 2, 1, 2, 3, 4]) + [1, 1, 2, 2, 2, 3, 3, 4] + >>> power_sort(list(range(100))) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99] + >>> power_sort(list(reversed(range(50)))) + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49] + """ + if len(collection) <= 1: + return collection + + # Make a copy to avoid modifying the original if it's immutable + arr = list(collection) + n = len(arr) + + # Adjust key function for reverse sorting + if reverse: + if key: + original_key = key + key = lambda x: -original_key(x) if isinstance(original_key(x), (int, float)) else original_key(x) + # For non-numeric types, we'll need a different approach + # Store original key and use negation wrapper + def reverse_key(x): + val = original_key(x) + # For comparable types, we can't negate, so we'll reverse at the end + return val + key = reverse_key + needs_final_reverse = True + else: + key = lambda x: -x if isinstance(x, (int, float)) else x + needs_final_reverse = True + else: + needs_final_reverse = False + + # Stack to hold runs: each entry is (start_index, length, power) + # Capacity is ceil(log2(n)) + 1 + import math + stack_capacity = math.ceil(math.log2(n)) + 1 if n > 1 else 2 + stack: list[tuple[int, int, int]] = [] + + start = 0 + while start < n: + # Find the next run + run_end = _find_run(arr, start, n, key) + run_length = run_end - start + + # Calculate power for this run + if len(stack) == 0: + power = 0 + else: + prev_start, prev_length, _ = stack[-1] + power = _node_power(n, prev_start, prev_length, start, run_length) + + # Merge runs from stack based on power comparison + while len(stack) > 0 and stack[-1][2] >= power: + # Merge the top run with the current run + prev_start, prev_length, prev_power = stack.pop() + _merge(arr, prev_start, prev_start + prev_length, run_end, key) + + # Update current run to include the merged run + start = prev_start + run_length = run_end - start + + # Recalculate power + if len(stack) == 0: + power = 0 + else: + prev_prev_start, prev_prev_length, _ = stack[-1] + power = _node_power( + n, prev_prev_start, prev_prev_length, start, run_length + ) + + # Push current run onto stack + stack.append((start, run_length, power)) + start = run_end + + # Merge all remaining runs on the stack + while len(stack) > 1: + start2, length2, _ = stack.pop() + start1, length1, power1 = stack.pop() + _merge(arr, start1, start1 + length1, start2 + length2, key) + + # Recalculate power for merged run + if len(stack) == 0: + power = 0 + else: + prev_start, prev_length, _ = stack[-1] + power = _node_power(n, prev_start, prev_length, start1, start2 + length2 - start1) + + stack.append((start1, start2 + length2 - start1, power)) + + # Handle reverse sorting for non-numeric types + if reverse and needs_final_reverse: + # For non-numeric types, we need to reverse the final result + # Check if we used numeric negation or not + if key and not isinstance(arr[0], (int, float)): + arr.reverse() + + return arr + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print("\nPowerSort Interactive Testing") + print("=" * 40) + + try: + user_input = input("Enter numbers separated by a comma:\n").strip() + if user_input == "": + unsorted = [] + else: + unsorted = [int(item.strip()) for item in user_input.split(",")] + + print(f"\nOriginal: {unsorted}") + sorted_list = power_sort(unsorted) + print(f"Sorted: {sorted_list}") + + # Test reverse + sorted_reverse = power_sort(unsorted, reverse=True) + print(f"Reverse: {sorted_reverse}") + + except ValueError: + print("Invalid input. Please enter valid integers separated by commas.") + except KeyboardInterrupt: + print("\n\nGoodbye!") \ No newline at end of file From 6c46eaf7689988cc55be4cf8a53fd8007ee39d70 Mon Sep 17 00:00:00 2001 From: Akshat Singh Kushwaha Date: Sun, 5 Oct 2025 15:38:58 +0530 Subject: [PATCH 2/8] Added references to the Power Sort algorithm in power_sort.py, including links to the Wikipedia page and the original paper by Munro and Wild. --- sorts/power_sort.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sorts/power_sort.py b/sorts/power_sort.py index 998eff7c2fc8..801c60eb9b43 100644 --- a/sorts/power_sort.py +++ b/sorts/power_sort.py @@ -14,6 +14,10 @@ Time Complexity: O(n log n) worst case, O(n) for nearly sorted data Space Complexity: O(n) for merge buffer +References: +- https://en.wikipedia.org/wiki/Powersort +- https://arxiv.org/abs/1805.04154 (Original paper by Munro and Wild) + For doctests run: python -m doctest -v power_sort.py From 3ab11af4547c743939bb64ea0e0f4111932ff871 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 10:10:51 +0000 Subject: [PATCH 3/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- sorts/power_sort.py | 109 ++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/sorts/power_sort.py b/sorts/power_sort.py index 801c60eb9b43..b5cdc92739e8 100644 --- a/sorts/power_sort.py +++ b/sorts/power_sort.py @@ -35,19 +35,19 @@ def _find_run( ) -> int: """ Detect a run (ascending or descending sequence) starting at 'start'. - + If the run is descending, reverse it in-place to make it ascending. Returns the end index (exclusive) of the detected run. - + Args: arr: The list to search in start: Starting index of the run end: End index (exclusive) of the search range key: Optional key function for comparisons - + Returns: End index (exclusive) of the detected run - + >>> arr = [3, 2, 1, 4, 5, 6] >>> _find_run(arr, 0, 6) 3 @@ -61,10 +61,10 @@ def _find_run( """ if start >= end - 1: return start + 1 - + key_func = key if key else lambda x: x run_end = start + 1 - + # Check if run is ascending or descending if key_func(arr[run_end]) < key_func(arr[start]): # Descending run @@ -76,29 +76,29 @@ def _find_run( # Ascending run while run_end < end and key_func(arr[run_end]) >= key_func(arr[run_end - 1]): run_end += 1 - + return run_end def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: """ Calculate the node power for two adjacent runs. - + This determines the merge priority in the stack. The power is the smallest integer p such that floor(a * 2^p) != floor(b * 2^p), where: - a = (b1 + n1/2) / n - b = (b2 + n2/2) / n - + Args: n: Total length of the array b1: Start index of first run n1: Length of first run b2: Start index of second run n2: Length of second run - + Returns: The calculated node power - + >>> _node_power(100, 0, 25, 25, 25) 2 >>> _node_power(100, 0, 50, 50, 50) @@ -108,16 +108,16 @@ def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: # To avoid floating point, we work with a = (2*b1 + n1) / (2*n) and b = (2*b2 + n2) / (2*n) # We want smallest p where floor(a * 2^p) != floor(b * 2^p) # This is floor((2*b1 + n1) * 2^p / (2*n)) != floor((2*b2 + n2) * 2^p / (2*n)) - + a = 2 * b1 + n1 b = 2 * b2 + n2 two_n = 2 * n - + # Find smallest power p where floor(a * 2^p / two_n) != floor(b * 2^p / two_n) power = 0 while (a * (1 << power)) // two_n == (b * (1 << power)) // two_n: power += 1 - + return power @@ -130,16 +130,16 @@ def _merge( ) -> None: """ Merge two adjacent sorted runs in-place using auxiliary space. - + Merges arr[start1:end1] with arr[end1:end2]. - + Args: arr: The list containing the runs start1: Start index of first run end1: End index of first run (start of second run) end2: End index of second run key: Optional key function for comparisons - + >>> arr = [1, 3, 5, 2, 4, 6] >>> _merge(arr, 0, 3, 6) >>> arr @@ -150,14 +150,14 @@ def _merge( [1, 2, 3, 5, 6, 7] """ key_func = key if key else lambda x: x - + # Copy the runs to temporary storage left = arr[start1:end1] right = arr[end1:end2] - + i = j = 0 k = start1 - + # Merge the two runs while i < len(left) and j < len(right): if key_func(left[i]) <= key_func(right[j]): @@ -167,13 +167,13 @@ def _merge( arr[k] = right[j] j += 1 k += 1 - + # Copy remaining elements while i < len(left): arr[k] = left[i] i += 1 k += 1 - + while j < len(right): arr[k] = right[j] j += 1 @@ -188,21 +188,21 @@ def power_sort( ) -> list: """ Sort a list using the PowerSort algorithm. - + PowerSort is an adaptive merge sort that detects existing runs in the data and uses a power-based merging strategy for optimal performance. - + Args: collection: A mutable ordered collection with comparable items key: Optional function to extract comparison key from each element reverse: If True, sort in descending order - + Returns: The same collection ordered according to the parameters - + Time Complexity: O(n log n) worst case, O(n) for nearly sorted data Space Complexity: O(n) - + Examples: >>> power_sort([0, 5, 3, 2, 2]) [0, 2, 2, 3, 5] @@ -237,22 +237,28 @@ def power_sort( """ if len(collection) <= 1: return collection - + # Make a copy to avoid modifying the original if it's immutable arr = list(collection) n = len(arr) - + # Adjust key function for reverse sorting if reverse: if key: original_key = key - key = lambda x: -original_key(x) if isinstance(original_key(x), (int, float)) else original_key(x) + key = ( + lambda x: -original_key(x) + if isinstance(original_key(x), (int, float)) + else original_key(x) + ) + # For non-numeric types, we'll need a different approach # Store original key and use negation wrapper def reverse_key(x): val = original_key(x) # For comparable types, we can't negate, so we'll reverse at the end return val + key = reverse_key needs_final_reverse = True else: @@ -260,36 +266,37 @@ def reverse_key(x): needs_final_reverse = True else: needs_final_reverse = False - + # Stack to hold runs: each entry is (start_index, length, power) # Capacity is ceil(log2(n)) + 1 import math + stack_capacity = math.ceil(math.log2(n)) + 1 if n > 1 else 2 stack: list[tuple[int, int, int]] = [] - + start = 0 while start < n: # Find the next run run_end = _find_run(arr, start, n, key) run_length = run_end - start - + # Calculate power for this run if len(stack) == 0: power = 0 else: prev_start, prev_length, _ = stack[-1] power = _node_power(n, prev_start, prev_length, start, run_length) - + # Merge runs from stack based on power comparison while len(stack) > 0 and stack[-1][2] >= power: # Merge the top run with the current run prev_start, prev_length, prev_power = stack.pop() _merge(arr, prev_start, prev_start + prev_length, run_end, key) - + # Update current run to include the merged run start = prev_start run_length = run_end - start - + # Recalculate power if len(stack) == 0: power = 0 @@ -298,60 +305,62 @@ def reverse_key(x): power = _node_power( n, prev_prev_start, prev_prev_length, start, run_length ) - + # Push current run onto stack stack.append((start, run_length, power)) start = run_end - + # Merge all remaining runs on the stack while len(stack) > 1: start2, length2, _ = stack.pop() start1, length1, power1 = stack.pop() _merge(arr, start1, start1 + length1, start2 + length2, key) - + # Recalculate power for merged run if len(stack) == 0: power = 0 else: prev_start, prev_length, _ = stack[-1] - power = _node_power(n, prev_start, prev_length, start1, start2 + length2 - start1) - + power = _node_power( + n, prev_start, prev_length, start1, start2 + length2 - start1 + ) + stack.append((start1, start2 + length2 - start1, power)) - + # Handle reverse sorting for non-numeric types if reverse and needs_final_reverse: # For non-numeric types, we need to reverse the final result # Check if we used numeric negation or not if key and not isinstance(arr[0], (int, float)): arr.reverse() - + return arr if __name__ == "__main__": import doctest - + doctest.testmod() - + print("\nPowerSort Interactive Testing") print("=" * 40) - + try: user_input = input("Enter numbers separated by a comma:\n").strip() if user_input == "": unsorted = [] else: unsorted = [int(item.strip()) for item in user_input.split(",")] - + print(f"\nOriginal: {unsorted}") sorted_list = power_sort(unsorted) print(f"Sorted: {sorted_list}") - + # Test reverse sorted_reverse = power_sort(unsorted, reverse=True) print(f"Reverse: {sorted_reverse}") - + except ValueError: print("Invalid input. Please enter valid integers separated by commas.") except KeyboardInterrupt: - print("\n\nGoodbye!") \ No newline at end of file + print("\n\nGoodbye!") From 4c6bd9dbf9e40c2f3d8152ba1323aad134f2d263 Mon Sep 17 00:00:00 2001 From: Akshat Singh Kushwaha Date: Sun, 5 Oct 2025 15:45:38 +0530 Subject: [PATCH 4/8] Refactor power_sort.py to improve code clarity and organization. Updated import statements, removed unnecessary whitespace, and enhanced comments for better readability. Adjusted key function handling for reverse sorting and ensured proper handling of numeric and non-numeric types. --- sorts/power_sort.py | 158 ++++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 72 deletions(-) diff --git a/sorts/power_sort.py b/sorts/power_sort.py index 801c60eb9b43..d336605c7691 100644 --- a/sorts/power_sort.py +++ b/sorts/power_sort.py @@ -27,7 +27,8 @@ from __future__ import annotations -from typing import Any, Callable +from collections.abc import Callable +from typing import Any def _find_run( @@ -35,19 +36,19 @@ def _find_run( ) -> int: """ Detect a run (ascending or descending sequence) starting at 'start'. - + If the run is descending, reverse it in-place to make it ascending. Returns the end index (exclusive) of the detected run. - + Args: arr: The list to search in start: Starting index of the run end: End index (exclusive) of the search range key: Optional key function for comparisons - + Returns: End index (exclusive) of the detected run - + >>> arr = [3, 2, 1, 4, 5, 6] >>> _find_run(arr, 0, 6) 3 @@ -61,10 +62,10 @@ def _find_run( """ if start >= end - 1: return start + 1 - + key_func = key if key else lambda x: x run_end = start + 1 - + # Check if run is ascending or descending if key_func(arr[run_end]) < key_func(arr[start]): # Descending run @@ -76,48 +77,51 @@ def _find_run( # Ascending run while run_end < end and key_func(arr[run_end]) >= key_func(arr[run_end - 1]): run_end += 1 - + return run_end def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: """ Calculate the node power for two adjacent runs. - + This determines the merge priority in the stack. The power is the smallest integer p such that floor(a * 2^p) != floor(b * 2^p), where: - a = (b1 + n1/2) / n - b = (b2 + n2/2) / n - + Args: n: Total length of the array b1: Start index of first run n1: Length of first run b2: Start index of second run n2: Length of second run - + Returns: The calculated node power - + >>> _node_power(100, 0, 25, 25, 25) 2 >>> _node_power(100, 0, 50, 50, 50) 1 """ # Calculate midpoints: a = (b1 + n1/2) / n, b = (b2 + n2/2) / n - # To avoid floating point, we work with a = (2*b1 + n1) / (2*n) and b = (2*b2 + n2) / (2*n) + # To avoid floating point, we work with a = (2*b1 + n1) / (2*n) and + # b = (2*b2 + n2) / (2*n) # We want smallest p where floor(a * 2^p) != floor(b * 2^p) - # This is floor((2*b1 + n1) * 2^p / (2*n)) != floor((2*b2 + n2) * 2^p / (2*n)) - + # This is floor((2*b1 + n1) * 2^p / (2*n)) != + # floor((2*b2 + n2) * 2^p / (2*n)) + a = 2 * b1 + n1 b = 2 * b2 + n2 two_n = 2 * n - - # Find smallest power p where floor(a * 2^p / two_n) != floor(b * 2^p / two_n) + + # Find smallest power p where floor(a * 2^p / two_n) != + # floor(b * 2^p / two_n) power = 0 while (a * (1 << power)) // two_n == (b * (1 << power)) // two_n: power += 1 - + return power @@ -130,16 +134,16 @@ def _merge( ) -> None: """ Merge two adjacent sorted runs in-place using auxiliary space. - + Merges arr[start1:end1] with arr[end1:end2]. - + Args: arr: The list containing the runs start1: Start index of first run end1: End index of first run (start of second run) end2: End index of second run key: Optional key function for comparisons - + >>> arr = [1, 3, 5, 2, 4, 6] >>> _merge(arr, 0, 3, 6) >>> arr @@ -150,14 +154,14 @@ def _merge( [1, 2, 3, 5, 6, 7] """ key_func = key if key else lambda x: x - + # Copy the runs to temporary storage left = arr[start1:end1] right = arr[end1:end2] - + i = j = 0 k = start1 - + # Merge the two runs while i < len(left) and j < len(right): if key_func(left[i]) <= key_func(right[j]): @@ -167,13 +171,13 @@ def _merge( arr[k] = right[j] j += 1 k += 1 - + # Copy remaining elements while i < len(left): arr[k] = left[i] i += 1 k += 1 - + while j < len(right): arr[k] = right[j] j += 1 @@ -188,21 +192,21 @@ def power_sort( ) -> list: """ Sort a list using the PowerSort algorithm. - + PowerSort is an adaptive merge sort that detects existing runs in the data and uses a power-based merging strategy for optimal performance. - + Args: collection: A mutable ordered collection with comparable items key: Optional function to extract comparison key from each element reverse: If True, sort in descending order - + Returns: The same collection ordered according to the parameters - + Time Complexity: O(n log n) worst case, O(n) for nearly sorted data Space Complexity: O(n) - + Examples: >>> power_sort([0, 5, 3, 2, 2]) [0, 2, 2, 3, 5] @@ -230,66 +234,70 @@ def power_sort( [(1, 'b'), (1, 'a'), (2, 'a')] >>> power_sort([1, 2, 3, 2, 1, 2, 3, 4]) [1, 1, 2, 2, 2, 3, 3, 4] - >>> power_sort(list(range(100))) - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99] - >>> power_sort(list(reversed(range(50)))) - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49] + >>> result = power_sort(list(range(100))) + >>> result == list(range(100)) + True + >>> result = power_sort(list(reversed(range(50)))) + >>> result == list(range(50)) + True """ if len(collection) <= 1: return collection - + # Make a copy to avoid modifying the original if it's immutable arr = list(collection) n = len(arr) - + # Adjust key function for reverse sorting + needs_final_reverse = False if reverse: if key: original_key = key - key = lambda x: -original_key(x) if isinstance(original_key(x), (int, float)) else original_key(x) - # For non-numeric types, we'll need a different approach - # Store original key and use negation wrapper + def reverse_key(x): val = original_key(x) - # For comparable types, we can't negate, so we'll reverse at the end + if isinstance(val, int | float): + return -val return val + key = reverse_key needs_final_reverse = True else: - key = lambda x: -x if isinstance(x, (int, float)) else x + + def reverse_cmp(x): + if isinstance(x, int | float): + return -x + return x + + key = reverse_cmp needs_final_reverse = True - else: - needs_final_reverse = False - + # Stack to hold runs: each entry is (start_index, length, power) - # Capacity is ceil(log2(n)) + 1 - import math - stack_capacity = math.ceil(math.log2(n)) + 1 if n > 1 else 2 stack: list[tuple[int, int, int]] = [] - + start = 0 while start < n: # Find the next run run_end = _find_run(arr, start, n, key) run_length = run_end - start - + # Calculate power for this run if len(stack) == 0: power = 0 else: prev_start, prev_length, _ = stack[-1] power = _node_power(n, prev_start, prev_length, start, run_length) - + # Merge runs from stack based on power comparison while len(stack) > 0 and stack[-1][2] >= power: # Merge the top run with the current run - prev_start, prev_length, prev_power = stack.pop() + prev_start, prev_length, _ = stack.pop() _merge(arr, prev_start, prev_start + prev_length, run_end, key) - + # Update current run to include the merged run start = prev_start run_length = run_end - start - + # Recalculate power if len(stack) == 0: power = 0 @@ -298,60 +306,66 @@ def reverse_key(x): power = _node_power( n, prev_prev_start, prev_prev_length, start, run_length ) - + # Push current run onto stack stack.append((start, run_length, power)) start = run_end - + # Merge all remaining runs on the stack while len(stack) > 1: start2, length2, _ = stack.pop() - start1, length1, power1 = stack.pop() + start1, length1, _ = stack.pop() _merge(arr, start1, start1 + length1, start2 + length2, key) - + # Recalculate power for merged run if len(stack) == 0: power = 0 else: prev_start, prev_length, _ = stack[-1] - power = _node_power(n, prev_start, prev_length, start1, start2 + length2 - start1) - + merged_length = start2 + length2 - start1 + power = _node_power(n, prev_start, prev_length, start1, merged_length) + stack.append((start1, start2 + length2 - start1, power)) - + # Handle reverse sorting for non-numeric types - if reverse and needs_final_reverse: + if ( + reverse + and needs_final_reverse + and key + and len(arr) > 0 + and not isinstance(arr[0], int | float) + ): # For non-numeric types, we need to reverse the final result # Check if we used numeric negation or not - if key and not isinstance(arr[0], (int, float)): - arr.reverse() - + arr.reverse() + return arr if __name__ == "__main__": import doctest - + doctest.testmod() - + print("\nPowerSort Interactive Testing") print("=" * 40) - + try: user_input = input("Enter numbers separated by a comma:\n").strip() if user_input == "": unsorted = [] else: unsorted = [int(item.strip()) for item in user_input.split(",")] - + print(f"\nOriginal: {unsorted}") sorted_list = power_sort(unsorted) print(f"Sorted: {sorted_list}") - + # Test reverse sorted_reverse = power_sort(unsorted, reverse=True) print(f"Reverse: {sorted_reverse}") - + except ValueError: print("Invalid input. Please enter valid integers separated by commas.") except KeyboardInterrupt: - print("\n\nGoodbye!") \ No newline at end of file + print("\n\nGoodbye!") From b8a0c4ea23410b1bb2d012842b4c86eba5ff764d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 10:17:42 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- sorts/power_sort.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/sorts/power_sort.py b/sorts/power_sort.py index 9d7fe91566cc..cc5df4e9d22a 100644 --- a/sorts/power_sort.py +++ b/sorts/power_sort.py @@ -67,11 +67,9 @@ def _find_run( if start >= end - 1: return start + 1 - key_func = key if key else lambda x: x run_end = start + 1 - # Check if run is ascending or descending if key_func(arr[run_end]) < key_func(arr[start]): # Descending run @@ -84,7 +82,6 @@ def _find_run( while run_end < end and key_func(arr[run_end]) >= key_func(arr[run_end - 1]): run_end += 1 - return run_end @@ -133,7 +130,6 @@ def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: while (a * (1 << power)) // two_n == (b * (1 << power)) // two_n: power += 1 - return power @@ -170,16 +166,13 @@ def _merge( """ key_func = key if key else lambda x: x - # Copy the runs to temporary storage left = arr[start1:end1] right = arr[end1:end2] - i = j = 0 k = start1 - # Merge the two runs while i < len(left) and j < len(right): if key_func(left[i]) <= key_func(right[j]): @@ -190,14 +183,12 @@ def _merge( j += 1 k += 1 - # Copy remaining elements while i < len(left): arr[k] = left[i] i += 1 k += 1 - while j < len(right): arr[k] = right[j] j += 1 @@ -269,12 +260,10 @@ def power_sort( if len(collection) <= 1: return collection - # Make a copy to avoid modifying the original if it's immutable arr = list(collection) n = len(arr) - # Adjust key function for reverse sorting needs_final_reverse = False if reverse: @@ -287,7 +276,6 @@ def reverse_key(x): return -val return val - key = reverse_key needs_final_reverse = True else: @@ -303,14 +291,12 @@ def reverse_cmp(x): # Stack to hold runs: each entry is (start_index, length, power) stack: list[tuple[int, int, int]] = [] - start = 0 while start < n: # Find the next run run_end = _find_run(arr, start, n, key) run_length = run_end - start - # Calculate power for this run if len(stack) == 0: power = 0 @@ -318,19 +304,16 @@ def reverse_cmp(x): prev_start, prev_length, _ = stack[-1] power = _node_power(n, prev_start, prev_length, start, run_length) - # Merge runs from stack based on power comparison while len(stack) > 0 and stack[-1][2] >= power: # Merge the top run with the current run prev_start, prev_length, _ = stack.pop() _merge(arr, prev_start, prev_start + prev_length, run_end, key) - # Update current run to include the merged run start = prev_start run_length = run_end - start - # Recalculate power if len(stack) == 0: power = 0 @@ -340,19 +323,16 @@ def reverse_cmp(x): n, prev_prev_start, prev_prev_length, start, run_length ) - # Push current run onto stack stack.append((start, run_length, power)) start = run_end - # Merge all remaining runs on the stack while len(stack) > 1: start2, length2, _ = stack.pop() start1, length1, _ = stack.pop() _merge(arr, start1, start1 + length1, start2 + length2, key) - # Recalculate power for merged run if len(stack) == 0: power = 0 @@ -363,7 +343,6 @@ def reverse_cmp(x): stack.append((start1, start2 + length2 - start1, power)) - # Handle reverse sorting for non-numeric types if ( reverse @@ -382,14 +361,11 @@ def reverse_cmp(x): if __name__ == "__main__": import doctest - doctest.testmod() - print("\nPowerSort Interactive Testing") print("=" * 40) - try: user_input = input("Enter numbers separated by a comma:\n").strip() if user_input == "": @@ -397,19 +373,15 @@ def reverse_cmp(x): else: unsorted = [int(item.strip()) for item in user_input.split(",")] - print(f"\nOriginal: {unsorted}") sorted_list = power_sort(unsorted) print(f"Sorted: {sorted_list}") - # Test reverse sorted_reverse = power_sort(unsorted, reverse=True) print(f"Reverse: {sorted_reverse}") - except ValueError: print("Invalid input. Please enter valid integers separated by commas.") except KeyboardInterrupt: print("\n\nGoodbye!") - From 7690063f29a8549578706319d50f653794a989e1 Mon Sep 17 00:00:00 2001 From: Akshat Singh Kushwaha Date: Sun, 5 Oct 2025 15:56:06 +0530 Subject: [PATCH 6/8] Refactor power_sort.py to enhance clarity and consistency. Updated parameter names for better understanding, improved comments for key functions, and ensured consistent handling of total length in calculations. This improves readability and maintainability of the code. --- sorts/power_sort.py | 72 +++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/sorts/power_sort.py b/sorts/power_sort.py index cc5df4e9d22a..7f02f820fab4 100644 --- a/sorts/power_sort.py +++ b/sorts/power_sort.py @@ -67,7 +67,7 @@ def _find_run( if start >= end - 1: return start + 1 - key_func = key if key else lambda x: x + key_func = key if key else lambda element: element run_end = start + 1 # Check if run is ascending or descending @@ -85,7 +85,7 @@ def _find_run( return run_end -def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: +def _node_power(total_length: int, b1: int, n1: int, b2: int, n2: int) -> int: """ Calculate the node power for two adjacent runs. @@ -97,7 +97,7 @@ def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: Args: - n: Total length of the array + total_length: Total length of the array b1: Start index of first run n1: Length of first run b2: Start index of second run @@ -113,16 +113,16 @@ def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int: >>> _node_power(100, 0, 50, 50, 50) 1 """ - # Calculate midpoints: a = (b1 + n1/2) / n, b = (b2 + n2/2) / n - # To avoid floating point, we work with a = (2*b1 + n1) / (2*n) and - # b = (2*b2 + n2) / (2*n) + # Calculate midpoints: a = (b1 + n1/2) / total_length, b = (b2 + n2/2) / total_length + # To avoid floating point, we work with a = (2*b1 + n1) / (2*total_length) and + # b = (2*b2 + n2) / (2*total_length) # We want smallest p where floor(a * 2^p) != floor(b * 2^p) - # This is floor((2*b1 + n1) * 2^p / (2*n)) != - # floor((2*b2 + n2) * 2^p / (2*n)) + # This is floor((2*b1 + n1) * 2^p / (2*total_length)) != + # floor((2*b2 + n2) * 2^p / (2*total_length)) a = 2 * b1 + n1 b = 2 * b2 + n2 - two_n = 2 * n + two_n = 2 * total_length # Find smallest power p where floor(a * 2^p / two_n) != # floor(b * 2^p / two_n) @@ -164,7 +164,7 @@ def _merge( >>> arr [1, 2, 3, 5, 6, 7] """ - key_func = key if key else lambda x: x + key_func = key if key else lambda element: element # Copy the runs to temporary storage left = arr[start1:end1] @@ -262,7 +262,7 @@ def power_sort( # Make a copy to avoid modifying the original if it's immutable arr = list(collection) - n = len(arr) + total_length = len(arr) # Adjust key function for reverse sorting needs_final_reverse = False @@ -270,8 +270,22 @@ def power_sort( if key: original_key = key - def reverse_key(x): - val = original_key(x) + def reverse_key(element: Any) -> Any: + """ + Reverse key function for numeric values. + + Args: + element: The element to process + + Returns: + Negated value for numeric types, original value otherwise + + >>> reverse_key(5) + -5 + >>> reverse_key('hello') + 'hello' + """ + val = original_key(element) if isinstance(val, int | float): return -val return val @@ -280,10 +294,24 @@ def reverse_key(x): needs_final_reverse = True else: - def reverse_cmp(x): - if isinstance(x, int | float): - return -x - return x + def reverse_cmp(element: Any) -> Any: + """ + Reverse comparison function for numeric values. + + Args: + element: The element to process + + Returns: + Negated value for numeric types, original value otherwise + + >>> reverse_cmp(10) + -10 + >>> reverse_cmp('test') + 'test' + """ + if isinstance(element, int | float): + return -element + return element key = reverse_cmp needs_final_reverse = True @@ -292,9 +320,9 @@ def reverse_cmp(x): stack: list[tuple[int, int, int]] = [] start = 0 - while start < n: + while start < total_length: # Find the next run - run_end = _find_run(arr, start, n, key) + run_end = _find_run(arr, start, total_length, key) run_length = run_end - start # Calculate power for this run @@ -302,7 +330,7 @@ def reverse_cmp(x): power = 0 else: prev_start, prev_length, _ = stack[-1] - power = _node_power(n, prev_start, prev_length, start, run_length) + power = _node_power(total_length, prev_start, prev_length, start, run_length) # Merge runs from stack based on power comparison while len(stack) > 0 and stack[-1][2] >= power: @@ -320,7 +348,7 @@ def reverse_cmp(x): else: prev_prev_start, prev_prev_length, _ = stack[-1] power = _node_power( - n, prev_prev_start, prev_prev_length, start, run_length + total_length, prev_prev_start, prev_prev_length, start, run_length ) # Push current run onto stack @@ -339,7 +367,7 @@ def reverse_cmp(x): else: prev_start, prev_length, _ = stack[-1] merged_length = start2 + length2 - start1 - power = _node_power(n, prev_start, prev_length, start1, merged_length) + power = _node_power(total_length, prev_start, prev_length, start1, merged_length) stack.append((start1, start2 + length2 - start1, power)) From 77d0e873ac27877203e4ff81075a43b5b4dec74c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 10:26:40 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- sorts/power_sort.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/sorts/power_sort.py b/sorts/power_sort.py index 7f02f820fab4..31d353284420 100644 --- a/sorts/power_sort.py +++ b/sorts/power_sort.py @@ -273,13 +273,13 @@ def power_sort( def reverse_key(element: Any) -> Any: """ Reverse key function for numeric values. - + Args: element: The element to process - + Returns: Negated value for numeric types, original value otherwise - + >>> reverse_key(5) -5 >>> reverse_key('hello') @@ -297,13 +297,13 @@ def reverse_key(element: Any) -> Any: def reverse_cmp(element: Any) -> Any: """ Reverse comparison function for numeric values. - + Args: element: The element to process - + Returns: Negated value for numeric types, original value otherwise - + >>> reverse_cmp(10) -10 >>> reverse_cmp('test') @@ -330,7 +330,9 @@ def reverse_cmp(element: Any) -> Any: power = 0 else: prev_start, prev_length, _ = stack[-1] - power = _node_power(total_length, prev_start, prev_length, start, run_length) + power = _node_power( + total_length, prev_start, prev_length, start, run_length + ) # Merge runs from stack based on power comparison while len(stack) > 0 and stack[-1][2] >= power: @@ -367,7 +369,9 @@ def reverse_cmp(element: Any) -> Any: else: prev_start, prev_length, _ = stack[-1] merged_length = start2 + length2 - start1 - power = _node_power(total_length, prev_start, prev_length, start1, merged_length) + power = _node_power( + total_length, prev_start, prev_length, start1, merged_length + ) stack.append((start1, start2 + length2 - start1, power)) From e24aef5240f1ee28d76a287f2823f4d677c522a1 Mon Sep 17 00:00:00 2001 From: Akshat Singh Kushwaha Date: Sun, 5 Oct 2025 15:59:55 +0530 Subject: [PATCH 8/8] Refactor comments and formatting in power_sort.py for improved readability. Adjusted whitespace and line breaks in function definitions and comments to enhance clarity without altering functionality. --- sorts/power_sort.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/sorts/power_sort.py b/sorts/power_sort.py index 7f02f820fab4..da25e43d63fd 100644 --- a/sorts/power_sort.py +++ b/sorts/power_sort.py @@ -113,7 +113,8 @@ def _node_power(total_length: int, b1: int, n1: int, b2: int, n2: int) -> int: >>> _node_power(100, 0, 50, 50, 50) 1 """ - # Calculate midpoints: a = (b1 + n1/2) / total_length, b = (b2 + n2/2) / total_length + # Calculate midpoints: a = (b1 + n1/2) / total_length, + # b = (b2 + n2/2) / total_length # To avoid floating point, we work with a = (2*b1 + n1) / (2*total_length) and # b = (2*b2 + n2) / (2*total_length) # We want smallest p where floor(a * 2^p) != floor(b * 2^p) @@ -273,13 +274,13 @@ def power_sort( def reverse_key(element: Any) -> Any: """ Reverse key function for numeric values. - + Args: element: The element to process - + Returns: Negated value for numeric types, original value otherwise - + >>> reverse_key(5) -5 >>> reverse_key('hello') @@ -297,13 +298,13 @@ def reverse_key(element: Any) -> Any: def reverse_cmp(element: Any) -> Any: """ Reverse comparison function for numeric values. - + Args: element: The element to process - + Returns: Negated value for numeric types, original value otherwise - + >>> reverse_cmp(10) -10 >>> reverse_cmp('test') @@ -330,7 +331,9 @@ def reverse_cmp(element: Any) -> Any: power = 0 else: prev_start, prev_length, _ = stack[-1] - power = _node_power(total_length, prev_start, prev_length, start, run_length) + power = _node_power( + total_length, prev_start, prev_length, start, run_length + ) # Merge runs from stack based on power comparison while len(stack) > 0 and stack[-1][2] >= power: @@ -367,7 +370,9 @@ def reverse_cmp(element: Any) -> Any: else: prev_start, prev_length, _ = stack[-1] merged_length = start2 + length2 - start1 - power = _node_power(total_length, prev_start, prev_length, start1, merged_length) + power = _node_power( + total_length, prev_start, prev_length, start1, merged_length + ) stack.append((start1, start2 + length2 - start1, power))