@@ -37,18 +37,22 @@ def _find_run(
3737 """
3838 Detect a run (ascending or descending sequence) starting at 'start'.
3939
40+
4041 If the run is descending, reverse it in-place to make it ascending.
4142 Returns the end index (exclusive) of the detected run.
4243
44+
4345 Args:
4446 arr: The list to search in
4547 start: Starting index of the run
4648 end: End index (exclusive) of the search range
4749 key: Optional key function for comparisons
4850
51+
4952 Returns:
5053 End index (exclusive) of the detected run
5154
55+
5256 >>> arr = [3, 2, 1, 4, 5, 6]
5357 >>> _find_run(arr, 0, 6)
5458 3
@@ -63,9 +67,11 @@ def _find_run(
6367 if start >= end - 1 :
6468 return start + 1
6569
70+
6671 key_func = key if key else lambda x : x
6772 run_end = start + 1
6873
74+
6975 # Check if run is ascending or descending
7076 if key_func (arr [run_end ]) < key_func (arr [start ]):
7177 # Descending run
@@ -78,28 +84,33 @@ def _find_run(
7884 while run_end < end and key_func (arr [run_end ]) >= key_func (arr [run_end - 1 ]):
7985 run_end += 1
8086
87+
8188 return run_end
8289
8390
8491def _node_power (n : int , b1 : int , n1 : int , b2 : int , n2 : int ) -> int :
8592 """
8693 Calculate the node power for two adjacent runs.
8794
95+
8896 This determines the merge priority in the stack. The power is the smallest
8997 integer p such that floor(a * 2^p) != floor(b * 2^p), where:
9098 - a = (b1 + n1/2) / n
9199 - b = (b2 + n2/2) / n
92100
101+
93102 Args:
94103 n: Total length of the array
95104 b1: Start index of first run
96105 n1: Length of first run
97106 b2: Start index of second run
98107 n2: Length of second run
99108
109+
100110 Returns:
101111 The calculated node power
102112
113+
103114 >>> _node_power(100, 0, 25, 25, 25)
104115 2
105116 >>> _node_power(100, 0, 50, 50, 50)
@@ -122,6 +133,7 @@ def _node_power(n: int, b1: int, n1: int, b2: int, n2: int) -> int:
122133 while (a * (1 << power )) // two_n == (b * (1 << power )) // two_n :
123134 power += 1
124135
136+
125137 return power
126138
127139
@@ -135,15 +147,18 @@ def _merge(
135147 """
136148 Merge two adjacent sorted runs in-place using auxiliary space.
137149
150+
138151 Merges arr[start1:end1] with arr[end1:end2].
139152
153+
140154 Args:
141155 arr: The list containing the runs
142156 start1: Start index of first run
143157 end1: End index of first run (start of second run)
144158 end2: End index of second run
145159 key: Optional key function for comparisons
146160
161+
147162 >>> arr = [1, 3, 5, 2, 4, 6]
148163 >>> _merge(arr, 0, 3, 6)
149164 >>> arr
@@ -155,13 +170,16 @@ def _merge(
155170 """
156171 key_func = key if key else lambda x : x
157172
173+
158174 # Copy the runs to temporary storage
159175 left = arr [start1 :end1 ]
160176 right = arr [end1 :end2 ]
161177
178+
162179 i = j = 0
163180 k = start1
164181
182+
165183 # Merge the two runs
166184 while i < len (left ) and j < len (right ):
167185 if key_func (left [i ]) <= key_func (right [j ]):
@@ -172,12 +190,14 @@ def _merge(
172190 j += 1
173191 k += 1
174192
193+
175194 # Copy remaining elements
176195 while i < len (left ):
177196 arr [k ] = left [i ]
178197 i += 1
179198 k += 1
180199
200+
181201 while j < len (right ):
182202 arr [k ] = right [j ]
183203 j += 1
@@ -193,20 +213,25 @@ def power_sort(
193213 """
194214 Sort a list using the PowerSort algorithm.
195215
216+
196217 PowerSort is an adaptive merge sort that detects existing runs in the data
197218 and uses a power-based merging strategy for optimal performance.
198219
220+
199221 Args:
200222 collection: A mutable ordered collection with comparable items
201223 key: Optional function to extract comparison key from each element
202224 reverse: If True, sort in descending order
203225
226+
204227 Returns:
205228 The same collection ordered according to the parameters
206229
230+
207231 Time Complexity: O(n log n) worst case, O(n) for nearly sorted data
208232 Space Complexity: O(n)
209233
234+
210235 Examples:
211236 >>> power_sort([0, 5, 3, 2, 2])
212237 [0, 2, 2, 3, 5]
@@ -244,10 +269,12 @@ def power_sort(
244269 if len (collection ) <= 1 :
245270 return collection
246271
272+
247273 # Make a copy to avoid modifying the original if it's immutable
248274 arr = list (collection )
249275 n = len (arr )
250276
277+
251278 # Adjust key function for reverse sorting
252279 needs_final_reverse = False
253280 if reverse :
@@ -260,6 +287,7 @@ def reverse_key(x):
260287 return - val
261288 return val
262289
290+
263291 key = reverse_key
264292 needs_final_reverse = True
265293 else :
@@ -275,29 +303,34 @@ def reverse_cmp(x):
275303 # Stack to hold runs: each entry is (start_index, length, power)
276304 stack : list [tuple [int , int , int ]] = []
277305
306+
278307 start = 0
279308 while start < n :
280309 # Find the next run
281310 run_end = _find_run (arr , start , n , key )
282311 run_length = run_end - start
283312
313+
284314 # Calculate power for this run
285315 if len (stack ) == 0 :
286316 power = 0
287317 else :
288318 prev_start , prev_length , _ = stack [- 1 ]
289319 power = _node_power (n , prev_start , prev_length , start , run_length )
290320
321+
291322 # Merge runs from stack based on power comparison
292323 while len (stack ) > 0 and stack [- 1 ][2 ] >= power :
293324 # Merge the top run with the current run
294325 prev_start , prev_length , _ = stack .pop ()
295326 _merge (arr , prev_start , prev_start + prev_length , run_end , key )
296327
328+
297329 # Update current run to include the merged run
298330 start = prev_start
299331 run_length = run_end - start
300332
333+
301334 # Recalculate power
302335 if len (stack ) == 0 :
303336 power = 0
@@ -307,16 +340,19 @@ def reverse_cmp(x):
307340 n , prev_prev_start , prev_prev_length , start , run_length
308341 )
309342
343+
310344 # Push current run onto stack
311345 stack .append ((start , run_length , power ))
312346 start = run_end
313347
348+
314349 # Merge all remaining runs on the stack
315350 while len (stack ) > 1 :
316351 start2 , length2 , _ = stack .pop ()
317352 start1 , length1 , _ = stack .pop ()
318353 _merge (arr , start1 , start1 + length1 , start2 + length2 , key )
319354
355+
320356 # Recalculate power for merged run
321357 if len (stack ) == 0 :
322358 power = 0
@@ -327,6 +363,7 @@ def reverse_cmp(x):
327363
328364 stack .append ((start1 , start2 + length2 - start1 , power ))
329365
366+
330367 # Handle reverse sorting for non-numeric types
331368 if (
332369 reverse
@@ -345,27 +382,34 @@ def reverse_cmp(x):
345382if __name__ == "__main__" :
346383 import doctest
347384
385+
348386 doctest .testmod ()
349387
388+
350389 print ("\n PowerSort Interactive Testing" )
351390 print ("=" * 40 )
352391
392+
353393 try :
354394 user_input = input ("Enter numbers separated by a comma:\n " ).strip ()
355395 if user_input == "" :
356396 unsorted = []
357397 else :
358398 unsorted = [int (item .strip ()) for item in user_input .split ("," )]
359399
400+
360401 print (f"\n Original: { unsorted } " )
361402 sorted_list = power_sort (unsorted )
362403 print (f"Sorted: { sorted_list } " )
363404
405+
364406 # Test reverse
365407 sorted_reverse = power_sort (unsorted , reverse = True )
366408 print (f"Reverse: { sorted_reverse } " )
367409
410+
368411 except ValueError :
369412 print ("Invalid input. Please enter valid integers separated by commas." )
370413 except KeyboardInterrupt :
371414 print ("\n \n Goodbye!" )
415+
0 commit comments