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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -0
  3. data/README.md +228 -29
  4. data/lib/array/sort.rb +5 -8
  5. data/lib/array_sort/algorithms/binary_insertion_sort.rb +41 -0
  6. data/lib/array_sort/algorithms/bubble_sort.rb +31 -0
  7. data/lib/array_sort/algorithms/bucket_sort.rb +67 -0
  8. data/lib/array_sort/algorithms/cocktail_shaker_sort.rb +42 -0
  9. data/lib/array_sort/algorithms/comb_sort.rb +33 -0
  10. data/lib/array_sort/algorithms/counting_sort.rb +50 -0
  11. data/lib/array_sort/algorithms/cycle_sort.rb +46 -0
  12. data/lib/array_sort/algorithms/gnome_sort.rb +28 -0
  13. data/lib/array_sort/algorithms/heap_sort.rb +43 -0
  14. data/lib/array_sort/algorithms/insertion_sort.rb +35 -0
  15. data/lib/array_sort/algorithms/intro_sort.rb +80 -0
  16. data/lib/array_sort/algorithms/merge_sort.rb +57 -0
  17. data/lib/array_sort/algorithms/odd_even_sort.rb +34 -0
  18. data/lib/array_sort/algorithms/pancake_sort.rb +39 -0
  19. data/lib/array_sort/algorithms/quick_sort.rb +68 -0
  20. data/lib/array_sort/algorithms/radix_sort.rb +58 -0
  21. data/lib/array_sort/algorithms/selection_sort.rb +25 -0
  22. data/lib/array_sort/algorithms/shell_sort.rb +41 -0
  23. data/lib/array_sort/algorithms/smooth_sort.rb +106 -0
  24. data/lib/array_sort/algorithms/tim_sort.rb +335 -0
  25. data/lib/array_sort/algorithms/tournament_sort.rb +53 -0
  26. data/lib/array_sort/algorithms/tree_sort.rb +96 -0
  27. data/lib/array_sort/array_methods.rb +67 -0
  28. data/lib/array_sort/core.rb +124 -0
  29. data/lib/array_sort/functions.rb +23 -0
  30. data/lib/array_sort/refinements.rb +16 -0
  31. data/lib/array_sort/registry.rb +68 -0
  32. data/lib/array_sort/trace.rb +111 -0
  33. data/lib/{array/sort → array_sort}/version.rb +1 -1
  34. data/lib/array_sort.rb +43 -0
  35. data/sig/array_sort.rbs +368 -0
  36. metadata +47 -71
  37. data/.gitignore +0 -9
  38. data/.rubocop.yml +0 -11
  39. data/.travis.yml +0 -6
  40. data/Gemfile +0 -6
  41. data/Gemfile.lock +0 -22
  42. data/Rakefile +0 -10
  43. data/array-sort.gemspec +0 -37
  44. data/bin/console +0 -14
  45. data/bin/setup +0 -8
  46. data/lib/array/sort/bubble_sort.rb +0 -91
  47. data/lib/array/sort/heap_sort.rb +0 -111
  48. data/lib/array/sort/helper.rb +0 -34
  49. data/lib/array/sort/insertion_sort.rb +0 -78
  50. data/lib/array/sort/merge_sort.rb +0 -94
  51. 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