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,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Cycle sort: writes every element directly into its final position, following the cycles of the permutation.
|
|
5
|
+
#
|
|
6
|
+
# An element's final position is found by counting how many elements are smaller than it. Each element is written
|
|
7
|
+
# at most once, so the number of writes is the theoretical minimum, at the cost of O(n²) comparisons.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module CycleSort
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(array, compare)
|
|
14
|
+
length = array.length
|
|
15
|
+
# With a consistent comparison every element is written at most once, so the whole sort makes at most +length+
|
|
16
|
+
# writes. The budget only matters for inconsistent comparisons, where it keeps the sort O(n²); running out
|
|
17
|
+
# closes the current cycle by putting +item+ back at +start+, so no element is ever lost.
|
|
18
|
+
writes_left = length
|
|
19
|
+
(0...(length - 1)).each do |start|
|
|
20
|
+
item = array[start]
|
|
21
|
+
position = position_of(array, compare, item, start)
|
|
22
|
+
next if position == start
|
|
23
|
+
|
|
24
|
+
loop do
|
|
25
|
+
position += 1 while position < length && compare.call(item, array[position]).zero?
|
|
26
|
+
position = start if position >= length || (writes_left -= 1).negative?
|
|
27
|
+
array[position], item = item, array[position]
|
|
28
|
+
break if position == start
|
|
29
|
+
|
|
30
|
+
position = position_of(array, compare, item, start)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
array
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns where +item+ belongs: +start+ plus the number of elements after +start+ that are smaller than it.
|
|
37
|
+
def position_of(array, compare, item, start)
|
|
38
|
+
position = start
|
|
39
|
+
((start + 1)...array.length).each { |i| position += 1 if compare.call(array[i], item).negative? }
|
|
40
|
+
position
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
register :cycle, CycleSort, title: 'Cycle sort', stable: false,
|
|
45
|
+
best: 'n²', average: 'n²', worst: 'n²', space: '1'
|
|
46
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Gnome sort: a garden gnome walks along a line of flower pots, swapping each pot backwards while it is smaller than
|
|
5
|
+
# the one before it, then returning to where it left off.
|
|
6
|
+
#
|
|
7
|
+
# This is the common variant where the gnome remembers its position instead of walking back step by step, which
|
|
8
|
+
# also guarantees it finishes even if the comparison is inconsistent.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module GnomeSort
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(array, compare)
|
|
15
|
+
(1...array.length).each do |position|
|
|
16
|
+
i = position
|
|
17
|
+
while i.positive? && compare.call(array[i - 1], array[i]).positive?
|
|
18
|
+
array[i - 1], array[i] = array[i], array[i - 1]
|
|
19
|
+
i -= 1
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
array
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
register :gnome, GnomeSort, title: 'Gnome sort', stable: true,
|
|
27
|
+
best: 'n', average: 'n²', worst: 'n²', space: '1'
|
|
28
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Heap sort: builds a max-heap in place, then repeatedly swaps the largest element to the end of the unsorted region
|
|
5
|
+
# and restores the heap over what remains.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module HeapSort
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def call(array, compare)
|
|
12
|
+
sort_range(array, compare, 0, array.length - 1)
|
|
13
|
+
array
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Sorts +array[low..high]+. Heap positions are counted from +low+, so the heap's root is +array[low]+.
|
|
17
|
+
def sort_range(array, compare, low, high)
|
|
18
|
+
last = high - low
|
|
19
|
+
((last - 1) / 2).downto(0) { |root| sift_down(array, compare, low, root, last) }
|
|
20
|
+
last.downto(1) do |boundary|
|
|
21
|
+
array[low], array[low + boundary] = array[low + boundary], array[low]
|
|
22
|
+
sift_down(array, compare, low, 0, boundary - 1)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Moves the element at heap position +root+ down until neither child (up to position +last+) is larger than it.
|
|
27
|
+
def sift_down(array, compare, base, root, last)
|
|
28
|
+
loop do
|
|
29
|
+
child = (root * 2) + 1
|
|
30
|
+
break if child > last
|
|
31
|
+
|
|
32
|
+
child += 1 if child < last && compare.call(array[base + child], array[base + child + 1]).negative?
|
|
33
|
+
break unless compare.call(array[base + root], array[base + child]).negative?
|
|
34
|
+
|
|
35
|
+
array[base + root], array[base + child] = array[base + child], array[base + root]
|
|
36
|
+
root = child
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
register :heap, HeapSort, title: 'Heap sort', stable: false,
|
|
42
|
+
best: 'n log n', average: 'n log n', worst: 'n log n', space: '1'
|
|
43
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Insertion sort: grows a sorted prefix one element at a time, inserting each new element where it belongs.
|
|
5
|
+
#
|
|
6
|
+
# Larger elements are shifted one slot to the right until the gap reaches the correct position, which takes fewer
|
|
7
|
+
# writes than swapping the element into place pair by pair. Very fast on small or nearly-sorted arrays, which is why
|
|
8
|
+
# hybrid sorts like introsort use it for short ranges.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module InsertionSort
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(array, compare)
|
|
15
|
+
sort_range(array, compare, 0, array.length - 1)
|
|
16
|
+
array
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Sorts +array[low..high]+.
|
|
20
|
+
def sort_range(array, compare, low, high)
|
|
21
|
+
((low + 1)..high).each do |i|
|
|
22
|
+
element = array[i]
|
|
23
|
+
j = i
|
|
24
|
+
while j > low && compare.call(array[j - 1], element).positive?
|
|
25
|
+
array[j] = array[j - 1]
|
|
26
|
+
j -= 1
|
|
27
|
+
end
|
|
28
|
+
array[j] = element unless j == i
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
register :insertion, InsertionSort, title: 'Insertion sort', stable: true,
|
|
34
|
+
best: 'n', average: 'n²', worst: 'n²', space: '1'
|
|
35
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Introsort: quicksort that switches to heap sort if the recursion gets too deep, and to insertion sort for short
|
|
5
|
+
# ranges. This is the algorithm behind C++'s std::sort.
|
|
6
|
+
#
|
|
7
|
+
# Quicksort is fastest in practice; the heap sort fallback caps the worst case at O(n log n), and insertion sort
|
|
8
|
+
# is faster than either on a handful of elements. Sorted and reversed input partition perfectly; "organ pipe" input
|
|
9
|
+
# (ascending, then descending) is a classic case where median-of-three pivots go wrong and the fallback takes over.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module IntroSort
|
|
13
|
+
# Ranges this short are finished with insertion sort.
|
|
14
|
+
SMALL_RANGE = 16
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def call(array, compare)
|
|
19
|
+
sort_range(array, compare, 0, array.length - 1, 2 * Math.log2([array.length, 1].max).floor)
|
|
20
|
+
array
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Sorts +array[low..high]+, falling back to heap sort once +depth_limit+ partitions have been made.
|
|
24
|
+
def sort_range(array, compare, low, high, depth_limit)
|
|
25
|
+
while high - low + 1 > SMALL_RANGE
|
|
26
|
+
return HeapSort.sort_range(array, compare, low, high) if depth_limit.zero?
|
|
27
|
+
|
|
28
|
+
depth_limit -= 1
|
|
29
|
+
split = partition(array, compare, low, high)
|
|
30
|
+
# Recurse into the smaller side and loop on the larger one, keeping the stack shallow.
|
|
31
|
+
if split - low < high - split
|
|
32
|
+
sort_range(array, compare, low, split, depth_limit)
|
|
33
|
+
low = split + 1
|
|
34
|
+
else
|
|
35
|
+
sort_range(array, compare, split + 1, high, depth_limit)
|
|
36
|
+
high = split
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
InsertionSort.sort_range(array, compare, low, high)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Hoare partition around the median of the first, middle and last elements: rearranges +array[low..high]+ so that
|
|
43
|
+
# no element of +array[low..split]+ is greater than the pivot and no element of +array[split + 1..high]+ is
|
|
44
|
+
# smaller, and returns +split+.
|
|
45
|
+
#
|
|
46
|
+
# Unlike a three-way partition, this barely disturbs input that is already partly ordered, which keeps pivots good
|
|
47
|
+
# on sorted, reversed and similar inputs. The bounds checks only matter for inconsistent comparisons, where they
|
|
48
|
+
# keep both sides non-empty so that sorting still finishes.
|
|
49
|
+
def partition(array, compare, low, high)
|
|
50
|
+
pivot = array[median_of_three(array, compare, low, (low + high) / 2, high)]
|
|
51
|
+
i = low - 1
|
|
52
|
+
j = high + 1
|
|
53
|
+
loop do
|
|
54
|
+
i += 1
|
|
55
|
+
i += 1 while i < high && compare.call(array[i], pivot).negative?
|
|
56
|
+
j -= 1
|
|
57
|
+
j -= 1 while j > low && compare.call(array[j], pivot).positive?
|
|
58
|
+
return j.clamp(low, high - 1) if i >= j
|
|
59
|
+
|
|
60
|
+
array[i], array[j] = array[j], array[i]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns whichever of the indices +a+, +b+ and +c+ holds the median of the three elements.
|
|
65
|
+
def median_of_three(array, compare, a, b, c)
|
|
66
|
+
if compare.call(array[a], array[b]).negative?
|
|
67
|
+
return b if compare.call(array[b], array[c]).negative?
|
|
68
|
+
|
|
69
|
+
compare.call(array[a], array[c]).negative? ? c : a
|
|
70
|
+
else
|
|
71
|
+
return a if compare.call(array[a], array[c]).negative?
|
|
72
|
+
|
|
73
|
+
compare.call(array[b], array[c]).negative? ? c : b
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
register :intro, IntroSort, title: 'Introsort', stable: false,
|
|
79
|
+
best: 'n log n', average: 'n log n', worst: 'n log n', space: 'log n'
|
|
80
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Merge sort: sorts each half, then merges the two sorted halves.
|
|
5
|
+
#
|
|
6
|
+
# Top-down: sort each half recursively, then merge them. Merging copies the left half into a scratch buffer that
|
|
7
|
+
# is allocated once for the whole sort and writes the merged run back into the array. If the two halves are already
|
|
8
|
+
# in order the merge is skipped, so sorted input costs only O(n) comparisons.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module MergeSort
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(array, compare)
|
|
15
|
+
sort_range(array, compare, 0, array.length, Array.new(array.length))
|
|
16
|
+
array
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Sorts +array[low...high]+.
|
|
20
|
+
def sort_range(array, compare, low, high, buffer)
|
|
21
|
+
return if high - low < 2
|
|
22
|
+
|
|
23
|
+
mid = (low + high) / 2
|
|
24
|
+
sort_range(array, compare, low, mid, buffer)
|
|
25
|
+
sort_range(array, compare, mid, high, buffer)
|
|
26
|
+
merge(array, compare, low, mid, high, buffer) if compare.call(array[mid - 1], array[mid]).positive?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Merges the sorted runs +array[low...mid]+ and +array[mid...high]+. On ties the left element wins, which is what
|
|
30
|
+
# makes the sort stable.
|
|
31
|
+
def merge(array, compare, low, mid, high, buffer)
|
|
32
|
+
(low...mid).each { |i| buffer[i] = array[i] }
|
|
33
|
+
left = low
|
|
34
|
+
right = mid
|
|
35
|
+
out = low
|
|
36
|
+
while left < mid && right < high
|
|
37
|
+
if compare.call(buffer[left], array[right]).positive?
|
|
38
|
+
array[out] = array[right]
|
|
39
|
+
right += 1
|
|
40
|
+
else
|
|
41
|
+
array[out] = buffer[left]
|
|
42
|
+
left += 1
|
|
43
|
+
end
|
|
44
|
+
out += 1
|
|
45
|
+
end
|
|
46
|
+
# Anything left over on the right is already in place; only leftovers from the left need copying back.
|
|
47
|
+
while left < mid
|
|
48
|
+
array[out] = buffer[left]
|
|
49
|
+
left += 1
|
|
50
|
+
out += 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
register :merge, MergeSort, title: 'Merge sort', stable: true,
|
|
56
|
+
best: 'n', average: 'n log n', worst: 'n log n', space: 'n'
|
|
57
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Odd-even sort (brick sort): alternates between comparing every odd/even pair of neighbours and every even/odd
|
|
5
|
+
# pair, swapping those that are out of order.
|
|
6
|
+
#
|
|
7
|
+
# All the comparisons within a phase are independent, which is why this sort is used on parallel hardware. n rounds
|
|
8
|
+
# are always enough, so the loop stops there even if the comparison is inconsistent.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module OddEvenSort
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(array, compare)
|
|
15
|
+
length = array.length
|
|
16
|
+
length.times do
|
|
17
|
+
sorted = true
|
|
18
|
+
[1, 0].each do |start|
|
|
19
|
+
start.step(length - 2, 2) do |i|
|
|
20
|
+
next unless compare.call(array[i], array[i + 1]).positive?
|
|
21
|
+
|
|
22
|
+
array[i], array[i + 1] = array[i + 1], array[i]
|
|
23
|
+
sorted = false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
break if sorted
|
|
27
|
+
end
|
|
28
|
+
array
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
register :odd_even, OddEvenSort, title: 'Odd-even sort', stable: true,
|
|
33
|
+
best: 'n', average: 'n²', worst: 'n²', space: '1'
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Pancake sort: sorts using only "flips", which reverse a prefix of the array, like a spatula flipping the top of a
|
|
5
|
+
# stack of pancakes.
|
|
6
|
+
#
|
|
7
|
+
# For each position from the end, flip the largest remaining element to the front, then flip it down into place.
|
|
8
|
+
# That takes at most 2n flips.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module PancakeSort
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(array, compare)
|
|
15
|
+
(array.length - 1).downto(1) do |last|
|
|
16
|
+
largest = 0
|
|
17
|
+
(1..last).each { |i| largest = i if compare.call(array[i], array[largest]).positive? }
|
|
18
|
+
next if largest == last
|
|
19
|
+
|
|
20
|
+
flip(array, largest) unless largest.zero?
|
|
21
|
+
flip(array, last)
|
|
22
|
+
end
|
|
23
|
+
array
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Reverses +array[0..last]+.
|
|
27
|
+
def flip(array, last)
|
|
28
|
+
first = 0
|
|
29
|
+
while first < last
|
|
30
|
+
array[first], array[last] = array[last], array[first]
|
|
31
|
+
first += 1
|
|
32
|
+
last -= 1
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
register :pancake, PancakeSort, title: 'Pancake sort', stable: false,
|
|
38
|
+
best: 'n²', average: 'n²', worst: 'n²', space: '1'
|
|
39
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Quicksort: picks a pivot, partitions the array around it, and sorts each side.
|
|
5
|
+
#
|
|
6
|
+
# In place, with a randomly chosen pivot so that no particular input (sorted, reversed, organ-pipe...) reliably
|
|
7
|
+
# triggers the quadratic worst case, and a three-way (Dutch national flag) partition so that runs of equal elements
|
|
8
|
+
# are handled in a single pass. Only the smaller partition is sorted recursively and the larger one is handled by the
|
|
9
|
+
# loop, which keeps the recursion depth at O(log n) no matter how unlucky the pivots are.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module QuickSort
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def call(array, compare)
|
|
16
|
+
sort_range(array, compare, 0, array.length - 1)
|
|
17
|
+
array
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Sorts +array[low..high]+.
|
|
21
|
+
def sort_range(array, compare, low, high)
|
|
22
|
+
while low < high
|
|
23
|
+
less_end, greater_start = partition(array, compare, low, high, rand(low..high))
|
|
24
|
+
if less_end - low < high - greater_start
|
|
25
|
+
sort_range(array, compare, low, less_end)
|
|
26
|
+
low = greater_start
|
|
27
|
+
else
|
|
28
|
+
sort_range(array, compare, greater_start, high)
|
|
29
|
+
high = less_end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Rearranges +array[low..high]+ into elements less than, equal to, and greater than the pivot +array[pivot_index]+.
|
|
35
|
+
# Returns the index of the last "less than" element and the first "greater than" element.
|
|
36
|
+
#
|
|
37
|
+
# The pivot itself always ends up in the "equal" region, so every call makes progress even if the comparison is
|
|
38
|
+
# inconsistent (for example, a block that always returns 1).
|
|
39
|
+
def partition(array, compare, low, high, pivot_index)
|
|
40
|
+
array[low], array[pivot_index] = array[pivot_index], array[low]
|
|
41
|
+
pivot = array[low]
|
|
42
|
+
|
|
43
|
+
less = low + 1
|
|
44
|
+
greater = high
|
|
45
|
+
i = low + 1
|
|
46
|
+
while i <= greater
|
|
47
|
+
comparison = compare.call(array[i], pivot)
|
|
48
|
+
if comparison.negative?
|
|
49
|
+
array[less], array[i] = array[i], array[less]
|
|
50
|
+
less += 1
|
|
51
|
+
i += 1
|
|
52
|
+
elsif comparison.positive?
|
|
53
|
+
array[i], array[greater] = array[greater], array[i]
|
|
54
|
+
greater -= 1
|
|
55
|
+
else
|
|
56
|
+
i += 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Move the pivot from the front to the end of the "less than" region, i.e. the start of the "equal" region.
|
|
61
|
+
array[low], array[less - 1] = array[less - 1], array[low]
|
|
62
|
+
[less - 2, greater + 1]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
register :quick, QuickSort, title: 'Quicksort', stable: false,
|
|
67
|
+
best: 'n log n', average: 'n log n', worst: 'n²', space: 'log n'
|
|
68
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Radix sort (least significant digit first): sorts Integer keys one byte at a time, starting with the lowest byte,
|
|
5
|
+
# using a stable counting pass for each. It never compares elements.
|
|
6
|
+
#
|
|
7
|
+
# Takes one pass per byte of the widest key, i.e. O(n·w) time where w is the number of bytes needed for the range
|
|
8
|
+
# of the keys. Negative keys work because every key is measured from the smallest one.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module RadixSort
|
|
12
|
+
DIGIT_BITS = 8
|
|
13
|
+
BASE = 1 << DIGIT_BITS
|
|
14
|
+
DIGIT_MASK = BASE - 1
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def call(array, key)
|
|
19
|
+
keys = Core.integer_keys(array, key, 'radix sort')
|
|
20
|
+
return array if keys.length < 2
|
|
21
|
+
|
|
22
|
+
min = keys.min
|
|
23
|
+
keys.map! { |k| k - min } # now every key is >= 0
|
|
24
|
+
max = keys.max
|
|
25
|
+
shift = 0
|
|
26
|
+
while (max >> shift).positive?
|
|
27
|
+
keys = counting_pass(array, keys, shift)
|
|
28
|
+
shift += DIGIT_BITS
|
|
29
|
+
end
|
|
30
|
+
array
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Stably reorders +array+ (and returns +keys+ in the new order) by the digit of each key at bit offset +shift+.
|
|
34
|
+
def counting_pass(array, keys, shift)
|
|
35
|
+
starts = Array.new(BASE, 0)
|
|
36
|
+
keys.each { |k| starts[(k >> shift) & DIGIT_MASK] += 1 }
|
|
37
|
+
total = 0
|
|
38
|
+
starts.each_index do |digit|
|
|
39
|
+
count = starts[digit]
|
|
40
|
+
starts[digit] = total
|
|
41
|
+
total += count
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
elements = Array.new(array)
|
|
45
|
+
reordered_keys = Array.new(keys.length)
|
|
46
|
+
keys.each_with_index do |k, i|
|
|
47
|
+
digit = (k >> shift) & DIGIT_MASK
|
|
48
|
+
array[starts[digit]] = elements[i]
|
|
49
|
+
reordered_keys[starts[digit]] = k
|
|
50
|
+
starts[digit] += 1
|
|
51
|
+
end
|
|
52
|
+
reordered_keys
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
register :radix, RadixSort, title: 'Radix sort (LSD)', stable: true, kind: :distribution,
|
|
57
|
+
best: 'n·w', average: 'n·w', worst: 'n·w', space: 'n'
|
|
58
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Selection sort: repeatedly finds the smallest remaining element and swaps it to the front.
|
|
5
|
+
#
|
|
6
|
+
# Always makes n(n-1)/2 comparisons, but at most n-1 swaps, so it is a reasonable choice when writes are expensive.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module SelectionSort
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(array, compare)
|
|
13
|
+
length = array.length
|
|
14
|
+
(0...(length - 1)).each do |i|
|
|
15
|
+
smallest = i
|
|
16
|
+
((i + 1)...length).each { |j| smallest = j if compare.call(array[j], array[smallest]).negative? }
|
|
17
|
+
array[i], array[smallest] = array[smallest], array[i] unless smallest == i
|
|
18
|
+
end
|
|
19
|
+
array
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
register :selection, SelectionSort, title: 'Selection sort', stable: false,
|
|
24
|
+
best: 'n²', average: 'n²', worst: 'n²', space: '1'
|
|
25
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Shell sort: insertion sort over elements a gap apart, for a shrinking sequence of gaps ending in 1.
|
|
5
|
+
#
|
|
6
|
+
# Uses Marcin Ciura's gap sequence (1, 4, 10, 23, 57, 132, 301, 701), extended by a factor of 2.25 for longer
|
|
7
|
+
# arrays. Its exact complexity is an open problem; in practice it grows a little faster than n log n.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module ShellSort
|
|
11
|
+
CIURA_GAPS = [1, 4, 10, 23, 57, 132, 301, 701].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def call(array, compare)
|
|
16
|
+
length = array.length
|
|
17
|
+
gaps(length).each do |gap|
|
|
18
|
+
(gap...length).each do |i|
|
|
19
|
+
element = array[i]
|
|
20
|
+
j = i
|
|
21
|
+
while j >= gap && compare.call(array[j - gap], element).positive?
|
|
22
|
+
array[j] = array[j - gap]
|
|
23
|
+
j -= gap
|
|
24
|
+
end
|
|
25
|
+
array[j] = element unless j == i
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
array
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns the gaps to use for an array of +length+ elements, largest first.
|
|
32
|
+
def gaps(length)
|
|
33
|
+
gaps = CIURA_GAPS.dup
|
|
34
|
+
gaps << (gaps.last * 2.25).floor while gaps.last < length
|
|
35
|
+
gaps.select { |gap| gap < length }.reverse
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
register :shell, ShellSort, title: 'Shell sort', stable: false,
|
|
40
|
+
best: 'n log n', average: '≈ n^1.3', worst: 'unknown', space: '1'
|
|
41
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArraySort
|
|
4
|
+
# Smoothsort: Edsger Dijkstra's heap sort variant that runs in O(n) on already-sorted input while keeping heap sort's
|
|
5
|
+
# O(n log n) worst case and O(1) extra space.
|
|
6
|
+
#
|
|
7
|
+
# Instead of one binary heap, it keeps a row of max-heaps whose sizes are Leonardo numbers (1, 1, 3, 5, 9, 15, ...,
|
|
8
|
+
# each the sum of the previous two plus one), with their roots in ascending order from left to right. The largest
|
|
9
|
+
# element is therefore always the rightmost root. Growing the row one element at a time builds it; shrinking it one
|
|
10
|
+
# element at a time, while repairing the row, leaves the array sorted.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module SmoothSort
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def call(array, compare)
|
|
17
|
+
length = array.length
|
|
18
|
+
return array if length < 2
|
|
19
|
+
|
|
20
|
+
sizes = leonardo_numbers(length)
|
|
21
|
+
orders = [] # the order (index into sizes) of each heap in the row, left to right
|
|
22
|
+
|
|
23
|
+
# Build: add each element as the root of a new heap, merging the last two heaps when their orders are
|
|
24
|
+
# consecutive, then move it to its place among the roots.
|
|
25
|
+
length.times do |root|
|
|
26
|
+
if orders.length >= 2 && orders[-2] == orders[-1] + 1
|
|
27
|
+
orders.pop
|
|
28
|
+
orders[-1] += 1
|
|
29
|
+
elsif orders.last == 1
|
|
30
|
+
orders.push(0)
|
|
31
|
+
else
|
|
32
|
+
orders.push(1)
|
|
33
|
+
end
|
|
34
|
+
restore_roots(array, compare, sizes, orders, orders.length - 1, root)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Take apart: the last element is the largest, so it is in place. Removing it splits its heap into its two
|
|
38
|
+
# subheaps, whose roots then need to find their place in the row.
|
|
39
|
+
(length - 1).downto(1) do |root|
|
|
40
|
+
order = orders.pop
|
|
41
|
+
next if order < 2
|
|
42
|
+
|
|
43
|
+
left_root = root - 1 - sizes[order - 2]
|
|
44
|
+
orders.push(order - 1)
|
|
45
|
+
restore_roots(array, compare, sizes, orders, orders.length - 1, left_root)
|
|
46
|
+
orders.push(order - 2)
|
|
47
|
+
restore_roots(array, compare, sizes, orders, orders.length - 1, root - 1)
|
|
48
|
+
end
|
|
49
|
+
array
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns the Leonardo numbers up to the first one that is at least +length+.
|
|
53
|
+
def leonardo_numbers(length)
|
|
54
|
+
numbers = [1, 1]
|
|
55
|
+
numbers << (numbers[-1] + numbers[-2] + 1) while numbers.last < length
|
|
56
|
+
numbers
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Moves the value at +root+ (the root of heap +heap+ in the row) leftwards past larger roots, then sifts it down
|
|
60
|
+
# into whichever heap it ends up in.
|
|
61
|
+
def restore_roots(array, compare, sizes, orders, heap, root)
|
|
62
|
+
while heap.positive?
|
|
63
|
+
previous_root = root - sizes[orders[heap]]
|
|
64
|
+
break unless compare.call(array[previous_root], array[root]).positive?
|
|
65
|
+
|
|
66
|
+
# The previous root may only move into this heap if it is also larger than this root's children.
|
|
67
|
+
order = orders[heap]
|
|
68
|
+
if order >= 2
|
|
69
|
+
right_child = root - 1
|
|
70
|
+
left_child = right_child - sizes[order - 2]
|
|
71
|
+
break unless compare.call(array[previous_root], array[left_child]).positive? &&
|
|
72
|
+
compare.call(array[previous_root], array[right_child]).positive?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
array[previous_root], array[root] = array[root], array[previous_root]
|
|
76
|
+
root = previous_root
|
|
77
|
+
heap -= 1
|
|
78
|
+
end
|
|
79
|
+
sift_down(array, compare, sizes, root, orders[heap])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Restores the max-heap property of the Leonardo heap of the given +order+ rooted at +root+. Its left subheap has
|
|
83
|
+
# order - 1 and its right subheap order - 2, with the right subheap's root just before +root+.
|
|
84
|
+
def sift_down(array, compare, sizes, root, order)
|
|
85
|
+
while order >= 2
|
|
86
|
+
right_child = root - 1
|
|
87
|
+
left_child = right_child - sizes[order - 2]
|
|
88
|
+
if compare.call(array[left_child], array[right_child]).negative?
|
|
89
|
+
child = right_child
|
|
90
|
+
child_order = order - 2
|
|
91
|
+
else
|
|
92
|
+
child = left_child
|
|
93
|
+
child_order = order - 1
|
|
94
|
+
end
|
|
95
|
+
break unless compare.call(array[root], array[child]).negative?
|
|
96
|
+
|
|
97
|
+
array[root], array[child] = array[child], array[root]
|
|
98
|
+
root = child
|
|
99
|
+
order = child_order
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
register :smooth, SmoothSort, title: 'Smoothsort', stable: false,
|
|
105
|
+
best: 'n', average: 'n log n', worst: 'n log n', space: '1'
|
|
106
|
+
end
|