array-sort 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +228 -29
- data/lib/array/sort.rb +5 -8
- data/lib/array_sort/algorithms/binary_insertion_sort.rb +41 -0
- data/lib/array_sort/algorithms/bubble_sort.rb +31 -0
- data/lib/array_sort/algorithms/bucket_sort.rb +67 -0
- data/lib/array_sort/algorithms/cocktail_shaker_sort.rb +42 -0
- data/lib/array_sort/algorithms/comb_sort.rb +33 -0
- data/lib/array_sort/algorithms/counting_sort.rb +50 -0
- data/lib/array_sort/algorithms/cycle_sort.rb +46 -0
- data/lib/array_sort/algorithms/gnome_sort.rb +28 -0
- data/lib/array_sort/algorithms/heap_sort.rb +43 -0
- data/lib/array_sort/algorithms/insertion_sort.rb +35 -0
- data/lib/array_sort/algorithms/intro_sort.rb +80 -0
- data/lib/array_sort/algorithms/merge_sort.rb +57 -0
- data/lib/array_sort/algorithms/odd_even_sort.rb +34 -0
- data/lib/array_sort/algorithms/pancake_sort.rb +39 -0
- data/lib/array_sort/algorithms/quick_sort.rb +68 -0
- data/lib/array_sort/algorithms/radix_sort.rb +58 -0
- data/lib/array_sort/algorithms/selection_sort.rb +25 -0
- data/lib/array_sort/algorithms/shell_sort.rb +41 -0
- data/lib/array_sort/algorithms/smooth_sort.rb +106 -0
- data/lib/array_sort/algorithms/tim_sort.rb +335 -0
- data/lib/array_sort/algorithms/tournament_sort.rb +53 -0
- data/lib/array_sort/algorithms/tree_sort.rb +96 -0
- data/lib/array_sort/array_methods.rb +67 -0
- data/lib/array_sort/core.rb +124 -0
- data/lib/array_sort/functions.rb +23 -0
- data/lib/array_sort/refinements.rb +16 -0
- data/lib/array_sort/registry.rb +68 -0
- data/lib/array_sort/trace.rb +111 -0
- data/lib/{array/sort → array_sort}/version.rb +1 -1
- data/lib/array_sort.rb +43 -0
- data/sig/array_sort.rbs +368 -0
- metadata +47 -71
- data/.gitignore +0 -9
- data/.rubocop.yml +0 -11
- data/.travis.yml +0 -6
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -22
- data/Rakefile +0 -10
- data/array-sort.gemspec +0 -37
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/lib/array/sort/bubble_sort.rb +0 -91
- data/lib/array/sort/heap_sort.rb +0 -111
- data/lib/array/sort/helper.rb +0 -34
- data/lib/array/sort/insertion_sort.rb +0 -78
- data/lib/array/sort/merge_sort.rb +0 -94
- data/lib/array/sort/quick_sort.rb +0 -73
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Timsort: finds the runs that are already in order in the data, extends short runs with binary insertion sort, and
|
|
5
|
+
# merges runs in a carefully balanced order. This is the algorithm behind Python's sorted() and Java's
|
|
6
|
+
# Arrays.sort for objects.
|
|
7
|
+
#
|
|
8
|
+
# Real-world data is often partly sorted, and Timsort takes advantage of it: sorted or reverse-sorted input takes a
|
|
9
|
+
# single O(n) pass. Merges "gallop" (search exponentially) when one run keeps winning, so merging runs that barely
|
|
10
|
+
# overlap is cheap too.
|
|
11
|
+
#
|
|
12
|
+
# This follows the structure of the reference implementation, including the corrected merge invariants
|
|
13
|
+
# (de Gouw et al., 2015), but always merges with a temporary copy of the left run rather than choosing the smaller
|
|
14
|
+
# run, which keeps it simpler at the cost of some extra memory.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module TimSort
|
|
18
|
+
# Arrays shorter than this are sorted with binary insertion sort alone.
|
|
19
|
+
MIN_MERGE = 32
|
|
20
|
+
# How many consecutive wins by one run switch a merge into galloping mode.
|
|
21
|
+
MIN_GALLOP = 7
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def call(array, compare)
|
|
26
|
+
Sorter.new(array, compare).sort if array.length > 1
|
|
27
|
+
array
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Holds the state of one sort: the array, the stack of pending runs and the adaptive galloping threshold.
|
|
31
|
+
class Sorter
|
|
32
|
+
def initialize(array, compare)
|
|
33
|
+
@array = array
|
|
34
|
+
@compare = compare
|
|
35
|
+
@runs = [] # [start, length] pairs, waiting to be merged
|
|
36
|
+
@min_gallop = MIN_GALLOP
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sort
|
|
40
|
+
length = @array.length
|
|
41
|
+
min_run = min_run_length(length)
|
|
42
|
+
low = 0
|
|
43
|
+
while low < length
|
|
44
|
+
run_length = count_run_and_make_ascending(low, length)
|
|
45
|
+
if run_length < min_run
|
|
46
|
+
forced = [length - low, min_run].min
|
|
47
|
+
binary_insertion_sort(low, low + forced, low + run_length)
|
|
48
|
+
run_length = forced
|
|
49
|
+
end
|
|
50
|
+
@runs << [low, run_length]
|
|
51
|
+
merge_collapse
|
|
52
|
+
low += run_length
|
|
53
|
+
end
|
|
54
|
+
merge_force_collapse
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def less?(a, b) = @compare.call(a, b).negative?
|
|
60
|
+
|
|
61
|
+
# A run length in (MIN_MERGE / 2)..MIN_MERGE such that length / min_run is a power of two or just below one,
|
|
62
|
+
# which keeps the final merges balanced.
|
|
63
|
+
def min_run_length(length)
|
|
64
|
+
remainder = 0
|
|
65
|
+
while length >= MIN_MERGE
|
|
66
|
+
remainder |= length & 1
|
|
67
|
+
length >>= 1
|
|
68
|
+
end
|
|
69
|
+
length + remainder
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns the length of the run starting at +low+, reversing it first if it is strictly descending. (Only
|
|
73
|
+
# strictly descending runs are reversed, so equal elements never swap places.)
|
|
74
|
+
def count_run_and_make_ascending(low, high)
|
|
75
|
+
run_high = low + 1
|
|
76
|
+
return 1 if run_high == high
|
|
77
|
+
|
|
78
|
+
if less?(@array[run_high], @array[low])
|
|
79
|
+
run_high += 1 while run_high < high && less?(@array[run_high], @array[run_high - 1])
|
|
80
|
+
reverse_range(low, run_high - 1)
|
|
81
|
+
else
|
|
82
|
+
run_high += 1 while run_high < high && !less?(@array[run_high], @array[run_high - 1])
|
|
83
|
+
end
|
|
84
|
+
run_high - low
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def reverse_range(low, high)
|
|
88
|
+
while low < high
|
|
89
|
+
@array[low], @array[high] = @array[high], @array[low]
|
|
90
|
+
low += 1
|
|
91
|
+
high -= 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Sorts +@array[low...high]+, given that +@array[low...start]+ is already sorted.
|
|
96
|
+
def binary_insertion_sort(low, high, start)
|
|
97
|
+
(start...high).each do |i|
|
|
98
|
+
element = @array[i]
|
|
99
|
+
position = BinaryInsertionSort.upper_bound(@array, @compare, element, low, i)
|
|
100
|
+
next if position == i
|
|
101
|
+
|
|
102
|
+
i.downto(position + 1) { |j| @array[j] = @array[j - 1] }
|
|
103
|
+
@array[position] = element
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Merges runs at the top of the stack until these hold for the top three run lengths z, y, x:
|
|
108
|
+
# z > y + x and y > x. That keeps the stack O(log n) deep and the merges balanced.
|
|
109
|
+
def merge_collapse
|
|
110
|
+
while @runs.size > 1
|
|
111
|
+
n = @runs.size - 2
|
|
112
|
+
if (n.positive? && run_length(n - 1) <= run_length(n) + run_length(n + 1)) ||
|
|
113
|
+
(n > 1 && run_length(n - 2) <= run_length(n - 1) + run_length(n))
|
|
114
|
+
n -= 1 if run_length(n - 1) < run_length(n + 1)
|
|
115
|
+
elsif run_length(n) > run_length(n + 1)
|
|
116
|
+
break
|
|
117
|
+
end
|
|
118
|
+
merge_at(n)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def merge_force_collapse
|
|
123
|
+
while @runs.size > 1
|
|
124
|
+
n = @runs.size - 2
|
|
125
|
+
n -= 1 if n.positive? && run_length(n - 1) < run_length(n + 1)
|
|
126
|
+
merge_at(n)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def run_length(index) = @runs[index][1]
|
|
131
|
+
|
|
132
|
+
# Merges the runs at stack positions +index+ and +index + 1+.
|
|
133
|
+
def merge_at(index)
|
|
134
|
+
base1, length1 = @runs[index]
|
|
135
|
+
base2, length2 = @runs[index + 1]
|
|
136
|
+
@runs[index] = [base1, length1 + length2]
|
|
137
|
+
@runs.delete_at(index + 1)
|
|
138
|
+
|
|
139
|
+
# Elements of run 1 that are no larger than run 2's first element are already in place.
|
|
140
|
+
skipped = gallop_right(@array[base2], @array, base1, length1, 0)
|
|
141
|
+
base1 += skipped
|
|
142
|
+
length1 -= skipped
|
|
143
|
+
return if length1.zero?
|
|
144
|
+
|
|
145
|
+
# Likewise, elements of run 2 that are no smaller than run 1's last element are already in place.
|
|
146
|
+
length2 = gallop_left(@array[base1 + length1 - 1], @array, base2, length2, length2 - 1)
|
|
147
|
+
return if length2.zero?
|
|
148
|
+
|
|
149
|
+
merge_low(base1, length1, base2, length2)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Merges two adjacent runs by copying the first into a temporary array and merging back from the front.
|
|
153
|
+
def merge_low(base1, length1, base2, length2)
|
|
154
|
+
temp = @array[base1, length1]
|
|
155
|
+
cursor1 = 0 # into temp
|
|
156
|
+
cursor2 = base2 # into @array
|
|
157
|
+
dest = base1
|
|
158
|
+
min_gallop = @min_gallop
|
|
159
|
+
|
|
160
|
+
catch(:done) do
|
|
161
|
+
@array[dest] = @array[cursor2]
|
|
162
|
+
dest += 1
|
|
163
|
+
cursor2 += 1
|
|
164
|
+
length2 -= 1
|
|
165
|
+
throw :done if length2.zero? || length1 == 1
|
|
166
|
+
|
|
167
|
+
loop do
|
|
168
|
+
count1 = 0 # consecutive wins by run 1
|
|
169
|
+
count2 = 0 # consecutive wins by run 2
|
|
170
|
+
|
|
171
|
+
# One element at a time, until one run wins MIN_GALLOP times in a row.
|
|
172
|
+
loop do
|
|
173
|
+
if less?(@array[cursor2], temp[cursor1])
|
|
174
|
+
@array[dest] = @array[cursor2]
|
|
175
|
+
dest += 1
|
|
176
|
+
cursor2 += 1
|
|
177
|
+
count2 += 1
|
|
178
|
+
count1 = 0
|
|
179
|
+
length2 -= 1
|
|
180
|
+
throw :done if length2.zero?
|
|
181
|
+
else
|
|
182
|
+
@array[dest] = temp[cursor1]
|
|
183
|
+
dest += 1
|
|
184
|
+
cursor1 += 1
|
|
185
|
+
count1 += 1
|
|
186
|
+
count2 = 0
|
|
187
|
+
length1 -= 1
|
|
188
|
+
throw :done if length1 == 1
|
|
189
|
+
end
|
|
190
|
+
break if count1 >= min_gallop || count2 >= min_gallop
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Galloping: find how far each run's winning streak goes with an exponential search, and copy it in bulk.
|
|
194
|
+
loop do
|
|
195
|
+
count1 = gallop_right(@array[cursor2], temp, cursor1, length1, 0)
|
|
196
|
+
count1.times do
|
|
197
|
+
@array[dest] = temp[cursor1]
|
|
198
|
+
dest += 1
|
|
199
|
+
cursor1 += 1
|
|
200
|
+
end
|
|
201
|
+
length1 -= count1
|
|
202
|
+
throw :done if length1 <= 1
|
|
203
|
+
|
|
204
|
+
@array[dest] = @array[cursor2]
|
|
205
|
+
dest += 1
|
|
206
|
+
cursor2 += 1
|
|
207
|
+
length2 -= 1
|
|
208
|
+
throw :done if length2.zero?
|
|
209
|
+
|
|
210
|
+
count2 = gallop_left(temp[cursor1], @array, cursor2, length2, 0)
|
|
211
|
+
count2.times do
|
|
212
|
+
@array[dest] = @array[cursor2]
|
|
213
|
+
dest += 1
|
|
214
|
+
cursor2 += 1
|
|
215
|
+
end
|
|
216
|
+
length2 -= count2
|
|
217
|
+
throw :done if length2.zero?
|
|
218
|
+
|
|
219
|
+
@array[dest] = temp[cursor1]
|
|
220
|
+
dest += 1
|
|
221
|
+
cursor1 += 1
|
|
222
|
+
length1 -= 1
|
|
223
|
+
throw :done if length1 == 1
|
|
224
|
+
|
|
225
|
+
min_gallop -= 1
|
|
226
|
+
break if count1 < MIN_GALLOP && count2 < MIN_GALLOP
|
|
227
|
+
end
|
|
228
|
+
# Leaving galloping mode makes it a little harder to re-enter.
|
|
229
|
+
min_gallop = [min_gallop, 0].max + 2
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
@min_gallop = [min_gallop, 1].max
|
|
233
|
+
|
|
234
|
+
if length1 == 1
|
|
235
|
+
# Run 1's last element is larger than everything left in run 2, which slides down to make room for it.
|
|
236
|
+
length2.times do
|
|
237
|
+
@array[dest] = @array[cursor2]
|
|
238
|
+
dest += 1
|
|
239
|
+
cursor2 += 1
|
|
240
|
+
end
|
|
241
|
+
@array[dest] = temp[cursor1]
|
|
242
|
+
else
|
|
243
|
+
# Run 2 is exhausted, so the rest of run 1 fills the gap. (Nothing is left at all only if the comparison is
|
|
244
|
+
# inconsistent, in which case every element is still in the array.)
|
|
245
|
+
length1.times do
|
|
246
|
+
@array[dest] = temp[cursor1]
|
|
247
|
+
dest += 1
|
|
248
|
+
cursor1 += 1
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Returns the leftmost position in +array[base, length]+ where +key+ could be inserted keeping it sorted, i.e. the
|
|
254
|
+
# number of elements less than +key+. The search starts at +base + hint+ and gallops outwards from there.
|
|
255
|
+
def gallop_left(key, array, base, length, hint)
|
|
256
|
+
last_offset = 0
|
|
257
|
+
offset = 1
|
|
258
|
+
if less?(array[base + hint], key)
|
|
259
|
+
# Gallop right until array[base + hint + last_offset] < key <= array[base + hint + offset].
|
|
260
|
+
max_offset = length - hint
|
|
261
|
+
while offset < max_offset && less?(array[base + hint + offset], key)
|
|
262
|
+
last_offset = offset
|
|
263
|
+
offset = (offset * 2) + 1
|
|
264
|
+
end
|
|
265
|
+
offset = [offset, max_offset].min
|
|
266
|
+
last_offset += hint
|
|
267
|
+
offset += hint
|
|
268
|
+
else
|
|
269
|
+
# Gallop left until array[base + hint - offset] < key <= array[base + hint - last_offset].
|
|
270
|
+
max_offset = hint + 1
|
|
271
|
+
while offset < max_offset && !less?(array[base + hint - offset], key)
|
|
272
|
+
last_offset = offset
|
|
273
|
+
offset = (offset * 2) + 1
|
|
274
|
+
end
|
|
275
|
+
offset = [offset, max_offset].min
|
|
276
|
+
last_offset, offset = hint - offset, hint - last_offset
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Binary search: array[base + last_offset] < key <= array[base + offset].
|
|
280
|
+
last_offset += 1
|
|
281
|
+
while last_offset < offset
|
|
282
|
+
mid = last_offset + ((offset - last_offset) / 2)
|
|
283
|
+
if less?(array[base + mid], key)
|
|
284
|
+
last_offset = mid + 1
|
|
285
|
+
else
|
|
286
|
+
offset = mid
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
offset
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Like #gallop_left, but returns the rightmost position, i.e. the number of elements less than or equal to
|
|
293
|
+
# +key+. Placing a run-2 element after equal run-1 elements is what keeps merges stable.
|
|
294
|
+
def gallop_right(key, array, base, length, hint)
|
|
295
|
+
last_offset = 0
|
|
296
|
+
offset = 1
|
|
297
|
+
if less?(key, array[base + hint])
|
|
298
|
+
# Gallop left until array[base + hint - offset] <= key < array[base + hint - last_offset].
|
|
299
|
+
max_offset = hint + 1
|
|
300
|
+
while offset < max_offset && less?(key, array[base + hint - offset])
|
|
301
|
+
last_offset = offset
|
|
302
|
+
offset = (offset * 2) + 1
|
|
303
|
+
end
|
|
304
|
+
offset = [offset, max_offset].min
|
|
305
|
+
last_offset, offset = hint - offset, hint - last_offset
|
|
306
|
+
else
|
|
307
|
+
# Gallop right until array[base + hint + last_offset] <= key < array[base + hint + offset].
|
|
308
|
+
max_offset = length - hint
|
|
309
|
+
while offset < max_offset && !less?(key, array[base + hint + offset])
|
|
310
|
+
last_offset = offset
|
|
311
|
+
offset = (offset * 2) + 1
|
|
312
|
+
end
|
|
313
|
+
offset = [offset, max_offset].min
|
|
314
|
+
last_offset += hint
|
|
315
|
+
offset += hint
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Binary search: array[base + last_offset] <= key < array[base + offset].
|
|
319
|
+
last_offset += 1
|
|
320
|
+
while last_offset < offset
|
|
321
|
+
mid = last_offset + ((offset - last_offset) / 2)
|
|
322
|
+
if less?(key, array[base + mid])
|
|
323
|
+
offset = mid
|
|
324
|
+
else
|
|
325
|
+
last_offset = mid + 1
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
offset
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
register :tim, TimSort, title: 'Timsort', stable: true,
|
|
334
|
+
best: 'n', average: 'n log n', worst: 'n log n', space: 'n'
|
|
335
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Tournament sort: runs a knockout tournament between the elements, where each match is won by the smaller one. The
|
|
5
|
+
# champion is the smallest element; it is removed and only the matches it played are replayed to find the next.
|
|
6
|
+
#
|
|
7
|
+
# Each replay takes log n comparisons. Ties are won by the element that came first, which makes the sort stable.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module TournamentSort
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(array, compare)
|
|
14
|
+
count = array.length
|
|
15
|
+
return array if count < 2
|
|
16
|
+
|
|
17
|
+
elements = Array.new(array)
|
|
18
|
+
leaves = 1
|
|
19
|
+
leaves <<= 1 while leaves < count
|
|
20
|
+
|
|
21
|
+
# A complete binary tree stored in an array: node i has children 2i and 2i + 1, leaves are leaves...2 * leaves,
|
|
22
|
+
# and every node holds the index of the element that won there (nil once that element has been taken).
|
|
23
|
+
tree = Array.new(2 * leaves)
|
|
24
|
+
count.times { |i| tree[leaves + i] = i }
|
|
25
|
+
(leaves - 1).downto(1) { |node| tree[node] = winner(elements, compare, tree[2 * node], tree[(2 * node) + 1]) }
|
|
26
|
+
|
|
27
|
+
count.times do |position|
|
|
28
|
+
champion = tree[1]
|
|
29
|
+
array[position] = elements[champion]
|
|
30
|
+
|
|
31
|
+
node = leaves + champion
|
|
32
|
+
tree[node] = nil
|
|
33
|
+
while node > 1
|
|
34
|
+
node /= 2
|
|
35
|
+
tree[node] = winner(elements, compare, tree[2 * node], tree[(2 * node) + 1])
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
array
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns whichever of the element indices +left+ and +right+ wins (is smaller). +left+ always comes from earlier
|
|
42
|
+
# in the array, so it wins ties.
|
|
43
|
+
def winner(elements, compare, left, right)
|
|
44
|
+
return right if left.nil?
|
|
45
|
+
return left if right.nil?
|
|
46
|
+
|
|
47
|
+
compare.call(elements[right], elements[left]).negative? ? right : left
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
register :tournament, TournamentSort, title: 'Tournament sort', stable: true,
|
|
52
|
+
best: 'n log n', average: 'n log n', worst: 'n log n', space: 'n'
|
|
53
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Tree sort: inserts every element into a binary search tree, then reads them back in order.
|
|
5
|
+
#
|
|
6
|
+
# The tree is an AVL tree, which rebalances itself on every insertion so that sorted input doesn't degrade it into a
|
|
7
|
+
# linked list (the classic O(n²) trap of tree sort). Equal elements are inserted to the right of each other, so an
|
|
8
|
+
# in-order walk returns them in their original order and the sort is stable.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module TreeSort
|
|
12
|
+
Node = Struct.new(:value, :left, :right, :height)
|
|
13
|
+
private_constant :Node
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def call(array, compare)
|
|
18
|
+
root = nil
|
|
19
|
+
array.each { |element| root = insert(root, element, compare) }
|
|
20
|
+
|
|
21
|
+
index = 0
|
|
22
|
+
each_in_order(root) do |value|
|
|
23
|
+
array[index] = value
|
|
24
|
+
index += 1
|
|
25
|
+
end
|
|
26
|
+
array
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def insert(node, value, compare)
|
|
30
|
+
return Node.new(value, nil, nil, 1) if node.nil?
|
|
31
|
+
|
|
32
|
+
if compare.call(value, node.value).negative?
|
|
33
|
+
node.left = insert(node.left, value, compare)
|
|
34
|
+
else
|
|
35
|
+
node.right = insert(node.right, value, compare)
|
|
36
|
+
end
|
|
37
|
+
rebalance(node)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Walks the tree in order without recursion.
|
|
41
|
+
def each_in_order(node)
|
|
42
|
+
stack = []
|
|
43
|
+
while node || !stack.empty?
|
|
44
|
+
while node
|
|
45
|
+
stack.push(node)
|
|
46
|
+
node = node.left
|
|
47
|
+
end
|
|
48
|
+
node = stack.pop
|
|
49
|
+
yield node.value
|
|
50
|
+
node = node.right
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def height(node) = node ? node.height : 0
|
|
55
|
+
|
|
56
|
+
def balance(node) = height(node.left) - height(node.right)
|
|
57
|
+
|
|
58
|
+
def update_height(node)
|
|
59
|
+
node.height = [height(node.left), height(node.right)].max + 1
|
|
60
|
+
node
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Restores the AVL property (subtree heights differ by at most one) at +node+ and returns the subtree's new root.
|
|
64
|
+
# Rotations never change the in-order sequence, so they preserve both sorting and stability.
|
|
65
|
+
def rebalance(node)
|
|
66
|
+
update_height(node)
|
|
67
|
+
case balance(node)
|
|
68
|
+
when 2
|
|
69
|
+
node.left = rotate_left(node.left) if balance(node.left).negative?
|
|
70
|
+
rotate_right(node)
|
|
71
|
+
when -2
|
|
72
|
+
node.right = rotate_right(node.right) if balance(node.right).positive?
|
|
73
|
+
rotate_left(node)
|
|
74
|
+
else
|
|
75
|
+
node
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def rotate_right(node)
|
|
80
|
+
pivot = node.left
|
|
81
|
+
node.left = pivot.right
|
|
82
|
+
pivot.right = update_height(node)
|
|
83
|
+
update_height(pivot)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rotate_left(node)
|
|
87
|
+
pivot = node.right
|
|
88
|
+
node.right = pivot.left
|
|
89
|
+
pivot.left = update_height(node)
|
|
90
|
+
update_height(pivot)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
register :tree, TreeSort, title: 'Tree sort (AVL)', stable: true,
|
|
95
|
+
best: 'n log n', average: 'n log n', worst: 'n log n', space: 'n'
|
|
96
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# The methods ArraySort adds to arrays. +require 'array/sort'+ includes this module into +Array+; +using
|
|
5
|
+
# ArraySort::Refinements+ adds the same methods to +Array+ in one file only.
|
|
6
|
+
#
|
|
7
|
+
# For every algorithm name +x+ in {ArraySort.algorithms} there are four methods with the same contract as their
|
|
8
|
+
# native counterparts:
|
|
9
|
+
#
|
|
10
|
+
# [+x_sort(&block)+] Returns a new sorted array. Comparison sorts accept an optional comparison block.
|
|
11
|
+
# [+x_sort!(&block)+] Sorts +self+ in place and returns it.
|
|
12
|
+
# [+x_sort_by(&block)+] Returns a new array sorted by the keys the block returns, calling the block once per
|
|
13
|
+
# element. Returns an Enumerator without a block.
|
|
14
|
+
# [+x_sort_by!(&block)+] Sorts +self+ in place by the keys the block returns. Returns an Enumerator without a block.
|
|
15
|
+
#
|
|
16
|
+
# Distribution sorts (counting, radix and bucket sort) don't compare elements, so +x_sort+ and +x_sort!+ raise
|
|
17
|
+
# ArgumentError when given a block, and every element (or key, for the +_by+ methods) must be an Integer (or for
|
|
18
|
+
# bucket sort, any finite real number).
|
|
19
|
+
#
|
|
20
|
+
# See {ArraySort.algorithm} for each algorithm's stability and complexity.
|
|
21
|
+
module ArrayMethods
|
|
22
|
+
# Swap two elements in the array, and returns +self+.
|
|
23
|
+
#
|
|
24
|
+
# Negative indices count from the end of the array, as with +Array#[]+.
|
|
25
|
+
#
|
|
26
|
+
# @param index1 [Integer] The index of the first element.
|
|
27
|
+
# @param index2 [Integer] The index of the second element.
|
|
28
|
+
# @return [Array] +self+
|
|
29
|
+
# @raise [ArgumentError] If either of the two parameters is not an Integer.
|
|
30
|
+
# @raise [IndexError] If either index is outside the bounds of the array.
|
|
31
|
+
def swap(index1, index2)
|
|
32
|
+
raise ArgumentError, 'Index must be an integer.' unless index1.is_a?(Integer) && index2.is_a?(Integer)
|
|
33
|
+
|
|
34
|
+
fetch(index1)
|
|
35
|
+
fetch(index2)
|
|
36
|
+
self[index1], self[index2] = self[index2], self[index1] if index1 != index2
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The methods are compiled from source rather than created with define_method so that Refinements can import
|
|
41
|
+
# them (Refinement#import_methods only accepts methods written in Ruby source). For +name = :merge+ this defines
|
|
42
|
+
# merge_sort, merge_sort!, merge_sort_by and merge_sort_by!.
|
|
43
|
+
ArraySort.algorithms.each do |name|
|
|
44
|
+
module_eval <<~RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
45
|
+
def #{name}_sort(&)
|
|
46
|
+
ArraySort::Core.sort(self, :#{name}, &)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def #{name}_sort!(&)
|
|
50
|
+
ArraySort::Core.sort!(self, :#{name}, &)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def #{name}_sort_by(&)
|
|
54
|
+
return ArraySort::Core.sort_by_enumerator(self, :#{name}, bang: false) unless block_given?
|
|
55
|
+
|
|
56
|
+
ArraySort::Core.sort_by(self, :#{name}, &)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def #{name}_sort_by!(&)
|
|
60
|
+
return ArraySort::Core.sort_by_enumerator(self, :#{name}, bang: true) unless block_given?
|
|
61
|
+
|
|
62
|
+
ArraySort::Core.sort_by!(self, :#{name}, &)
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Shared plumbing behind every public sort method.
|
|
5
|
+
#
|
|
6
|
+
# Algorithms come in two kinds (see {Algorithm#kind}):
|
|
7
|
+
#
|
|
8
|
+
# * Comparison algorithms respond to +call(buffer, compare)+ and sort +buffer+ in place, where +compare+ is a lambda
|
|
9
|
+
# returning a negative, zero or positive Integer.
|
|
10
|
+
# * Distribution algorithms respond to +call(buffer, key)+ and sort +buffer+ in place by the numeric key that the
|
|
11
|
+
# lambda +key+ returns for each element.
|
|
12
|
+
#
|
|
13
|
+
# Algorithms only ever change +buffer+ through single-element assignment (+buffer[i] = value+), which is what lets
|
|
14
|
+
# {ArraySort.trace} record every write. Everything else (copying, frozen checks, key caching for the +_by+ variants,
|
|
15
|
+
# enumerators) lives here so the algorithms stay focused on the algorithm.
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
module Core
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
IDENTITY = ->(element) { element }
|
|
22
|
+
FIRST = ->(pair) { pair[0] }
|
|
23
|
+
private_constant :IDENTITY, :FIRST
|
|
24
|
+
|
|
25
|
+
# Returns a new, sorted copy of +array+.
|
|
26
|
+
def sort(array, name, &block)
|
|
27
|
+
algorithm = ArraySort.algorithm(name)
|
|
28
|
+
buffer = Array.new(array)
|
|
29
|
+
if algorithm.comparison?
|
|
30
|
+
algorithm.implementation.call(buffer, comparator(block))
|
|
31
|
+
else
|
|
32
|
+
if block
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"#{name}_sort sorts by numeric key and does not take a comparison block; use #{name}_sort_by instead"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
algorithm.implementation.call(buffer, IDENTITY)
|
|
38
|
+
end
|
|
39
|
+
buffer
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Sorts +array+ in place and returns it.
|
|
43
|
+
#
|
|
44
|
+
# The work happens on a private copy that replaces the contents of +array+ only once sorting has finished, so if
|
|
45
|
+
# the comparison raises, +array+ is left exactly as it was (matching +Array#sort!+).
|
|
46
|
+
def sort!(array, name, &)
|
|
47
|
+
ensure_mutable!(array)
|
|
48
|
+
array.replace(sort(array, name, &))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns a new copy of +array+ sorted by the keys the block produces.
|
|
52
|
+
#
|
|
53
|
+
# Like +Array#sort_by+, the block is called exactly once per element.
|
|
54
|
+
def sort_by(array, name, &block)
|
|
55
|
+
algorithm = ArraySort.algorithm(name)
|
|
56
|
+
keyed = array.map { |element| [block.call(element), element] }
|
|
57
|
+
by_key = algorithm.comparison? ? ->(a, b) { normalize(a[0] <=> b[0], a[0], b[0]) } : FIRST
|
|
58
|
+
algorithm.implementation.call(keyed, by_key)
|
|
59
|
+
keyed.map!(&:last)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Sorts +array+ in place by the keys the block produces and returns it.
|
|
63
|
+
def sort_by!(array, name, &)
|
|
64
|
+
ensure_mutable!(array)
|
|
65
|
+
array.replace(sort_by(array, name, &))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the Enumerator that +_sort_by+ and +_sort_by!+ return when called without a block.
|
|
69
|
+
#
|
|
70
|
+
# This is built by hand rather than with +to_enum+ so that it also works under {ArraySort::Refinements}: an
|
|
71
|
+
# Enumerator made by +to_enum+ calls the method from outside the refined scope, where it doesn't exist.
|
|
72
|
+
def sort_by_enumerator(array, name, bang:)
|
|
73
|
+
method = bang ? :sort_by! : :sort_by
|
|
74
|
+
Enumerator.new(-> { array.size }) do |yielder|
|
|
75
|
+
public_send(method, array, name) { |element| yielder.yield(element) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Wraps a comparison (the user's block, or +<=>+) so that it always returns an Integer, raising the same
|
|
80
|
+
# +ArgumentError+ as +Array#sort+ when two elements cannot be compared.
|
|
81
|
+
def comparator(block)
|
|
82
|
+
if block
|
|
83
|
+
->(a, b) { normalize(block.call(a, b), a, b) }
|
|
84
|
+
else
|
|
85
|
+
->(a, b) { normalize(a <=> b, a, b) }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalize(result, element1, element2)
|
|
90
|
+
return result if result.is_a?(Integer)
|
|
91
|
+
|
|
92
|
+
sign = result <=> 0 unless result.nil?
|
|
93
|
+
raise ArgumentError, "comparison of #{element1.class} with #{element2.class} failed" if sign.nil?
|
|
94
|
+
|
|
95
|
+
sign
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns the keys of +array+ under +key+, checking that each one is an Integer.
|
|
99
|
+
def integer_keys(array, key, algorithm_title)
|
|
100
|
+
array.map do |element|
|
|
101
|
+
value = key.call(element)
|
|
102
|
+
raise ArgumentError, "#{algorithm_title} needs Integer keys, got #{value.class}" unless value.is_a?(Integer)
|
|
103
|
+
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns the keys of +array+ under +key+, checking that each one is a finite real number.
|
|
109
|
+
def real_keys(array, key, algorithm_title)
|
|
110
|
+
array.map do |element|
|
|
111
|
+
value = key.call(element)
|
|
112
|
+
unless value.is_a?(Numeric) && value.real? && value.finite?
|
|
113
|
+
raise ArgumentError, "#{algorithm_title} needs finite real number keys, got #{value.inspect}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
value
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ensure_mutable!(array)
|
|
121
|
+
raise FrozenError.new("can't modify frozen #{array.class}: #{array.inspect}", receiver: array) if array.frozen?
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# For every algorithm name +x+ in {ArraySort.algorithms}, ArraySort responds to +x_sort(array, &block)+,
|
|
5
|
+
# +x_sort!(array, &block)+, +x_sort_by(array, &block)+ and +x_sort_by!(array, &block)+. They behave exactly like
|
|
6
|
+
# the Array methods of the same name (see {ArrayMethods}) but take the array as an argument, so they work without
|
|
7
|
+
# patching Array.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# require 'array_sort'
|
|
11
|
+
# ArraySort.merge_sort([3, 1, 2]) # => [1, 2, 3]
|
|
12
|
+
# ArraySort.quick_sort_by(%w[ccc a bb], &:size) # => ["a", "bb", "ccc"]
|
|
13
|
+
ArraySort.algorithms.each do |name|
|
|
14
|
+
define_singleton_method(:"#{name}_sort") { |array, &block| Core.sort(array, name, &block) }
|
|
15
|
+
define_singleton_method(:"#{name}_sort!") { |array, &block| Core.sort!(array, name, &block) }
|
|
16
|
+
define_singleton_method(:"#{name}_sort_by") do |array, &block|
|
|
17
|
+
block ? Core.sort_by(array, name, &block) : Core.sort_by_enumerator(array, name, bang: false)
|
|
18
|
+
end
|
|
19
|
+
define_singleton_method(:"#{name}_sort_by!") do |array, &block|
|
|
20
|
+
block ? Core.sort_by!(array, name, &block) : Core.sort_by_enumerator(array, name, bang: true)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|