-
-
Notifications
You must be signed in to change notification settings - Fork 51k
Expand file tree
/
Copy pathsmoothsort.py
More file actions
213 lines (175 loc) · 6.8 KB
/
Copy pathsmoothsort.py
File metadata and controls
213 lines (175 loc) · 6.8 KB
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"""
Smoothsort algorithm implementation.
Smoothsort is an adaptive, in-place comparison sort invented by Edsger W. Dijkstra.
It runs in O(n log n) worst-case and degrades gracefully to O(n) for nearly sorted data.
It uses a forest of Leonardo heaps to achieve this adaptive behaviour.
Reference:
https://en.wikipedia.org/wiki/Smoothsort
https://www.cs.utexas.edu/~EWD/ewd07xx/EWD796a.PDF
"""
# Precomputed Leonardo numbers: L(0)=1, L(1)=1, L(k)=L(k-1)+L(k-2)+1.
# 46 values comfortably cover all practical list sizes.
_LEONARDO: list[int] = [1, 1]
while _LEONARDO[-1] < 2**31:
_LEONARDO.append(_LEONARDO[-1] + _LEONARDO[-2] + 1)
def _sift(seq: list[int], root: int, order: int) -> None:
"""
Restore the max-heap property within a Leonardo tree of the given ``order``.
Sifts ``seq[root]`` downward until the subtree satisfies the Leonardo
max-heap invariant: every node is >= both of its children.
Trees of order 0 or 1 are single nodes and already satisfy the invariant.
In a Leonardo tree of order k rooted at index ``root``:
- the right child root is at ``root - 1``
- the left child root is at ``root - 1 - L(k-2)``
Args:
seq: The list being sorted (mutated in-place).
root: Index of the root of the Leonardo tree to fix.
order: Leonardo order of the tree rooted at ``root``.
Examples:
>>> data = [3, 5, 4]
>>> _sift(data, 2, 2)
>>> data
[3, 4, 5]
>>> data = [1, 2, 3]
>>> _sift(data, 2, 2)
>>> data
[1, 2, 3]
>>> data = [7]
>>> _sift(data, 0, 1)
>>> data
[7]
>>> data = [9, 1, 8, 5, 3]
>>> _sift(data, 4, 3)
>>> data
[3, 1, 9, 5, 8]
"""
while order > 1:
right = root - 1 # right child root
left = root - 1 - _LEONARDO[order - 2] # left child root
if seq[left] >= seq[right] and seq[left] > seq[root]:
seq[root], seq[left] = seq[left], seq[root]
root = left
order -= 1
elif seq[right] > seq[left] and seq[right] > seq[root]:
seq[root], seq[right] = seq[right], seq[root]
root = right
order -= 2
else:
break
def _trinkle(
seq: list[int],
pos: int,
heap_sizes: list[int],
idx: int,
) -> None:
"""
Restore both the inter-heap root ordering and the intra-heap ordering.
Walks the value at ``pos`` leftwards through the forest-root chain as
long as the left-neighbour root is larger, then calls ``_sift`` to fix
the heap at the final resting position.
Args:
seq: The list being sorted (mutated in-place).
pos: Index of the root being inserted or newly exposed.
heap_sizes: List of Leonardo orders for the current forest (left to
right); ``heap_sizes[idx]`` is the order of the tree
whose root is at ``pos``.
idx: Position in ``heap_sizes`` for the tree rooted at ``pos``.
Examples:
>>> data = [1, 5, 3]
>>> _trinkle(data, 2, [1, 1], 1)
>>> data
[1, 3, 5]
>>> data = [3, 5, 4]
>>> _trinkle(data, 2, [2], 0)
>>> data
[3, 4, 5]
"""
while idx > 0:
prev_root = pos - _LEONARDO[heap_sizes[idx]]
if seq[pos] >= seq[prev_root]:
break
# Only swap if prev_root is also >= its own children; otherwise
# moving it would break the heap on the left side.
if heap_sizes[idx] > 1:
right = pos - 1
left = pos - 1 - _LEONARDO[heap_sizes[idx] - 2]
if seq[prev_root] <= seq[right] or seq[prev_root] <= seq[left]:
break
seq[pos], seq[prev_root] = seq[prev_root], seq[pos]
pos = prev_root
idx -= 1
_sift(seq, pos, heap_sizes[idx])
def smoothsort(seq: list[int]) -> list[int]:
"""
Sort a list in-place using the Smoothsort algorithm and return it.
Smoothsort (Edsger W. Dijkstra, 1981) is an adaptive, in-place sort
with O(n log n) worst-case time and O(n) best-case time on already-sorted
input. It improves on Heapsort by maintaining a forest of Leonardo heaps
whose structure mirrors the sorted prefix of the sequence.
Args:
seq: A list of integers to sort.
Returns:
The same list object, sorted in ascending order.
Examples:
>>> smoothsort([4, 1, 3, 9, 7])
[1, 3, 4, 7, 9]
>>> smoothsort([])
[]
>>> smoothsort([1])
[1]
>>> smoothsort([5, 4, 3, 2, 1])
[1, 2, 3, 4, 5]
>>> smoothsort([3, 3, 2, 1, 2])
[1, 2, 2, 3, 3]
>>> smoothsort([1, 2, 3, 4, 5])
[1, 2, 3, 4, 5]
>>> smoothsort([-3, 0, -1, 5, 2])
[-3, -1, 0, 2, 5]
"""
n = len(seq)
if n < 2:
return seq
# ``heap_sizes[i]`` is the Leonardo order of the i-th tree (left to right).
heap_sizes: list[int] = []
# ------------------------------------------------------------------
# Phase 1 : Build the Leonardo heap forest over seq[0..n-1].
# ------------------------------------------------------------------
for i in range(n):
# If the two rightmost trees have consecutive orders, merge them.
if len(heap_sizes) >= 2 and heap_sizes[-2] == heap_sizes[-1] + 1:
heap_sizes.pop()
heap_sizes[-1] += 1
elif heap_sizes and heap_sizes[-1] == 1:
heap_sizes.append(0)
else:
heap_sizes.append(1)
_trinkle(seq, i, heap_sizes, len(heap_sizes) - 1)
# ------------------------------------------------------------------
# Phase 2 : Extract maximum elements right-to-left.
# ------------------------------------------------------------------
for i in range(n - 1, -1, -1):
order = heap_sizes.pop()
if order > 1:
# Expose the two child roots and re-trinkle each.
right_order = order - 2
left_order = order - 1
right_pos = i - 1
left_pos = i - 1 - _LEONARDO[right_order]
heap_sizes.append(left_order)
_trinkle(seq, left_pos, heap_sizes, len(heap_sizes) - 1)
heap_sizes.append(right_order)
_trinkle(seq, right_pos, heap_sizes, len(heap_sizes) - 1)
return seq
if __name__ == "__main__":
import doctest
import random
results = doctest.testmod(verbose=False)
assert results.failed == 0, f"{results.failed} doctest(s) failed"
for trial in range(5000):
sample = random.choices(range(-50, 50), k=random.randint(0, 30))
got = smoothsort(sample[:])
assert got == sorted(sample), (
f"Trial {trial}: smoothsort({sample!r}) -> {got!r}, "
f"expected {sorted(sample)!r}"
)
print("All doctests and 5 000 random trials passed.")