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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91c86c745cc5568df53cec34a27f8da0a3b3b24899d0016150b794998b3da1c5
4
- data.tar.gz: d63474b2f370ef3454ea7978acb838bf8901e51caf30f668dc9f2392f600befc
3
+ metadata.gz: b2f62e8f149a2fed7b2f3d2b9e2eb69a1944559122dd4012972fd2f2bc029dd5
4
+ data.tar.gz: 20498925f9a617fc59913a762e5d7997d09a765dfadfbf7e8583a2ad89c85d98
5
5
  SHA512:
6
- metadata.gz: af69d008cb08440a894f1ee58583e565dcf4994a5825fd0f57415cbd8aea1fc90bc493c47d73be7e0c5304a8651fa7cd003fbda1a22d97ac3c16e658193ebdc4
7
- data.tar.gz: 4dc3acb9501663a583d921c9fef4a50074eace63b1b67ebdc1eead7507e0ffef05c78ed60047fa796c4538e937e8776c71ecd7cd52093815f80d17fb5405af9c
6
+ metadata.gz: 050e9660a9586a8dff0b7a5bee3e565d5bc1aee445e519cb512e2d9e9c6b476a5f2ffbb171abbd238595cee1f2d3039c2c8ccc1e7e62acc4aade725cd210f9c0
7
+ data.tar.gz: 36d984fc22fe1fc955ec9bfe74cc951f6b4593f88a8fa6af2a959784fbf493d1f33c67d5d6e8762c11738d138aa4e4787c66e18ab3b2c65fd16ccb07b0a3bde4
data/CHANGELOG.md ADDED
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0] - 2026-10-10
8
+
9
+ The five original algorithms keep their public API (`*_sort`, `*_sort!`, `*_sort_by`, `*_sort_by!` and `Array#swap`),
10
+ and `require 'array/sort'` still adds them to every Array. But several behaviors were wrong and have been fixed, and
11
+ the gem grows from 5 algorithms to 22, which is why this is a major release. If you relied on any of the old behavior
12
+ listed under "Changed", please check your code.
13
+
14
+ ### Added
15
+
16
+ - 17 new algorithms, each with the same four methods as the originals:
17
+ - Simple sorts: binary insertion, cocktail shaker, comb, cycle, gnome, odd-even, pancake and selection sort.
18
+ - Efficient comparison sorts: shell sort, smoothsort, tree sort (AVL) and tournament sort.
19
+ - Hybrid sorts: introsort and Timsort.
20
+ - Distribution sorts, which sort by Integer (or, for bucket sort, real number) keys without comparing: counting,
21
+ radix (LSD) and bucket sort.
22
+ - `require 'array_sort'`, which loads the gem without changing Array. From there, use `using ArraySort::Refinements`
23
+ to add the methods to Array in one file only, or module functions such as `ArraySort.merge_sort(array)`.
24
+ - `ArraySort.trace(:algorithm, array)`, which records every comparison and write an algorithm makes.
25
+ - `ArraySort.algorithms` and `ArraySort.algorithm(name)`, describing each algorithm's stability and complexity.
26
+ - RBS type signatures in `sig/`, generated from the algorithm registry.
27
+ - `rake bench`, which benchmarks every algorithm against `Array#sort`.
28
+ - GitHub Actions CI testing Ruby 3.3, 3.4 and 4.0, and running RuboCop and RBS validation.
29
+ - A Release workflow that publishes to RubyGems.org with trusted publishing, and Dependabot for dependency updates.
30
+ - RubyGems metadata (source, changelog and issue tracker links) and MFA requirement for publishing.
31
+
32
+ ### Fixed
33
+
34
+ - `merge_sort` and `quick_sort` (and their `!`/`_by` variants) only applied a custom comparison block at the top level
35
+ of the recursion, so sorting with a block, e.g. in descending order, usually returned a wrongly ordered array.
36
+ - `insertion_sort` and `heap_sort` treated only exactly `1`/`-1` as "greater"/"less", so a block returning other
37
+ numbers (like `a - b`) produced unsorted output.
38
+ - `quick_sort` raised `SystemStackError` on large sorted arrays (about 10,000 elements) and took quadratic time on
39
+ sorted or reverse-sorted input.
40
+ - Comparing incomparable elements raised `NoMethodError` instead of `ArgumentError`, as `Array#sort` does.
41
+ - `Array#swap` with an out-of-range index silently grew the array and filled it with `nil`s. It now raises
42
+ `IndexError`.
43
+
44
+ ### Changed
45
+
46
+ - Ruby 3.3 or later is required.
47
+ - Bang methods check for a frozen receiver before doing any work, so they raise `FrozenError` even if the array is
48
+ already sorted, and without calling the block.
49
+ - Bang methods leave the receiver untouched if the comparison raises partway through, like `Array#sort!`.
50
+ - The `_by` methods call their block exactly once per element (previously up to twice per comparison).
51
+ - Non-bang methods always return a plain `Array`, even for subclasses of `Array`, like `Array#sort`.
52
+ - Rewrote merge sort and quicksort to stop allocating new arrays at every level of recursion: merge sort now merges
53
+ with indices and a single scratch buffer, and quicksort sorts in place with a random pivot and three-way
54
+ partitioning. Insertion sort shifts elements instead of swapping them.
55
+ - The private helper methods the gem used to add to `Array` (`sort_compare`, `become_clone_of`, `heapify`,
56
+ `sift_down` and others) were removed. The algorithms now live in the `ArraySort` module and no longer add anything
57
+ to `Array` beyond the public sort methods and `swap`.
58
+ - The Array methods are now defined in `ArraySort::ArrayMethods`, which `require 'array/sort'` includes into Array,
59
+ instead of directly on Array. The files under `lib/array/sort/` other than `lib/array/sort.rb` itself are gone, so
60
+ `require 'array/sort'` is the only way to load the patch.
61
+ - Enumerators returned by the `_by` methods now report their `size`, and work under the refinement too.
62
+
63
+ ### Removed
64
+
65
+ - Travis CI configuration.
66
+ - `bundler` as a development dependency in the gemspec. Development dependencies now live in the `Gemfile`.
67
+
68
+ ## [0.1.1] - 2018-05-11
69
+
70
+ - Initial public release with bubble sort, heap sort, insertion sort, merge sort and quicksort.
71
+
72
+ [1.0.0]: https://github.com/foxhatleo/array-sort/compare/0c3220b...v1.0.0
73
+ [0.1.1]: https://github.com/foxhatleo/array-sort/commit/0c3220b
data/README.md CHANGED
@@ -1,20 +1,26 @@
1
1
  # ArraySort
2
2
 
3
- ArraySort adds methods in the Ruby Array class that provides sorting methods using popular sorting algorithms.
3
+ [![Gem Version](https://badge.fury.io/rb/array-sort.svg)](https://badge.fury.io/rb/array-sort)
4
+ [![CI](https://github.com/foxhatleo/array-sort/actions/workflows/ci.yml/badge.svg)](https://github.com/foxhatleo/array-sort/actions/workflows/ci.yml)
4
5
 
5
- Currently, the following sorting algorithms are implemented:
6
- * Bubble sort _(stable)_
7
- * Heap sort _(unstable)_
8
- * Insertion sort _(stable)_
9
- * Merge sort _(stable)_
10
- * Quicksort _(unstable)_
6
+ ArraySort implements 22 sorting algorithms for Ruby arrays, from bubble sort to Timsort, all with the same interface
7
+ as `Array#sort`, `#sort!`, `#sort_by` and `#sort_by!`. Use them as `Array` methods, through a refinement, or as plain
8
+ module functions, and trace any of them to watch every comparison and write it makes.
11
9
 
12
- Note that this gem does not overwrite `Array#sort`, `Array#sort!`, `Array#sort_by`, or `Array#sort_by!`. Calling those
13
- methods will invoke the native sorting methods, which use in-place quicksort algorithm and are unstable.
10
+ ```ruby
11
+ [3, 1, 2].tim_sort # => [1, 2, 3]
12
+ [3, 1, 2].merge_sort { |a, b| b <=> a } # => [3, 2, 1]
13
+ %w[pear fig apple kiwi].merge_sort_by(&:length) # => ["fig", "pear", "kiwi", "apple"]
14
+ ArraySort.trace(:insertion, [3, 1, 2]).comparisons # => 3
15
+ ```
16
+
17
+ This gem does not override `Array#sort` and friends. Ruby's native sort is written in C and is much faster than
18
+ anything here, but it is not stable. Reach for ArraySort when you need a stable sort, want a specific algorithm's
19
+ guarantees, or want to learn and compare how the algorithms behave.
14
20
 
15
21
  ## Installation
16
22
 
17
- Add this line to your application's Gemfile:
23
+ Requires Ruby 3.3 or later. Add this line to your application's Gemfile:
18
24
 
19
25
  ```ruby
20
26
  gem 'array-sort'
@@ -22,47 +28,240 @@ gem 'array-sort'
22
28
 
23
29
  And then execute:
24
30
 
25
- $ bundle
31
+ $ bundle install
26
32
 
27
33
  Or install it yourself as:
28
34
 
29
35
  $ gem install array-sort
30
36
 
37
+ ## Loading it
38
+
39
+ There are three ways to use ArraySort, depending on whether you want it to change `Array`.
40
+
41
+ **Patch `Array` everywhere.** This is what Bundler does by default for `gem 'array-sort'`, and what
42
+ `require 'array/sort'` does. Every array gets the sort methods.
43
+
44
+ ```ruby
45
+ require 'array/sort'
46
+
47
+ [3, 1, 2].heap_sort # => [1, 2, 3]
48
+ ```
49
+
50
+ **Refine `Array` in one file.** Load the gem without patching (in a Gemfile, `gem 'array-sort', require: 'array_sort'`),
51
+ then opt in per file with `using`. Code outside that file never sees the methods.
52
+
53
+ ```ruby
54
+ require 'array_sort'
55
+ using ArraySort::Refinements
56
+
57
+ [3, 1, 2].heap_sort # => [1, 2, 3]
58
+ ```
59
+
60
+ **Module functions.** Also available after `require 'array_sort'`, without touching `Array` at all. Each takes the
61
+ array as its first argument.
62
+
63
+ ```ruby
64
+ require 'array_sort'
65
+
66
+ ArraySort.merge_sort([3, 1, 2]) # => [1, 2, 3]
67
+ ArraySort.quick_sort_by(%w[ccc a bb], &:size) # => ["a", "bb", "ccc"]
68
+ ArraySort.insertion_sort!(array) # sorts array in place
69
+ ```
70
+
71
+ ## Algorithms
72
+
73
+ Every algorithm `x` comes with `x_sort`, `x_sort!`, `x_sort_by` and `x_sort_by!`. Complexities are in big-O notation.
74
+ In the distribution sorts, k is the range of the keys (largest minus smallest) and w is the number of bytes needed for
75
+ that range.
76
+
77
+ ### Simple sorts
78
+
79
+ | Algorithm | Method | Best | Average | Worst | Extra space | Stable |
80
+ | --------------------- | ----------------------- | ------- | ------- | ----- | ----------- | ------ |
81
+ | Bubble sort | `bubble_sort` | n | n² | n² | 1 | Yes |
82
+ | Cocktail shaker sort | `cocktail_shaker_sort` | n | n² | n² | 1 | Yes |
83
+ | Comb sort | `comb_sort` | n log n | n²/2ᵖ | n² | 1 | No |
84
+ | Gnome sort | `gnome_sort` | n | n² | n² | 1 | Yes |
85
+ | Odd-even sort | `odd_even_sort` | n | n² | n² | 1 | Yes |
86
+ | Insertion sort | `insertion_sort` | n | n² | n² | 1 | Yes |
87
+ | Binary insertion sort | `binary_insertion_sort` | n log n | n² | n² | 1 | Yes |
88
+ | Selection sort | `selection_sort` | n² | n² | n² | 1 | No |
89
+ | Cycle sort | `cycle_sort` | n² | n² | n² | 1 | No |
90
+ | Pancake sort | `pancake_sort` | n² | n² | n² | 1 | No |
91
+
92
+ ### Efficient comparison sorts
93
+
94
+ | Algorithm | Method | Best | Average | Worst | Extra space | Stable |
95
+ | --------------- | ----------------- | ------- | ------- | ------- | ----------- | ------ |
96
+ | Merge sort | `merge_sort` | n | n log n | n log n | n | Yes |
97
+ | Quicksort | `quick_sort` | n log n | n log n | n² | log n | No |
98
+ | Heap sort | `heap_sort` | n log n | n log n | n log n | 1 | No |
99
+ | Shell sort | `shell_sort` | n log n | ≈ n^1.3 | unknown | 1 | No |
100
+ | Smoothsort | `smooth_sort` | n | n log n | n log n | 1 | No |
101
+ | Tree sort (AVL) | `tree_sort` | n log n | n log n | n log n | n | Yes |
102
+ | Tournament sort | `tournament_sort` | n log n | n log n | n log n | n | Yes |
103
+
104
+ ### Hybrid sorts
105
+
106
+ | Algorithm | Method | Best | Average | Worst | Extra space | Stable |
107
+ | --------- | ------------ | ------- | ------- | ------- | ----------- | ------ |
108
+ | Introsort | `intro_sort` | n log n | n log n | n log n | log n | No |
109
+ | Timsort | `tim_sort` | n | n log n | n log n | n | Yes |
110
+
111
+ ### Distribution sorts
112
+
113
+ | Algorithm | Method | Best | Average | Worst | Extra space | Stable |
114
+ | ---------------- | --------------- | ----- | ------- | ----- | ----------- | ------ |
115
+ | Counting sort | `counting_sort` | n + k | n + k | n + k | n + k | Yes |
116
+ | Radix sort (LSD) | `radix_sort` | n·w | n·w | n·w | n | Yes |
117
+ | Bucket sort | `bucket_sort` | n | n | n² | n | Yes |
118
+
119
+ A few notes worth knowing:
120
+
121
+ * **Quicksort** picks a random pivot, so its O(n²) worst case is vanishingly unlikely on any input, sorted and
122
+ reversed arrays included.
123
+ * **Introsort** (C++'s `std::sort`) is quicksort that falls back to heap sort when partitioning goes badly, and uses
124
+ insertion sort for short ranges, so its worst case is O(n log n).
125
+ * **Timsort** (Python's `sorted`, Java's object sort) finds runs that are already in order and merges them, so sorted,
126
+ reversed and partly sorted data sort in close to linear time.
127
+ * **Smoothsort** is a heap sort that takes linear time on sorted input, without extra memory.
128
+ * **Comb sort's** average case depends on the number of passes p; in practice it is close to n log n. Shell sort's
129
+ complexity with the gap sequence used here (Ciura's) is an open problem.
130
+ * **Cycle sort** writes each element at most once, the minimum possible, at the cost of many comparisons.
131
+ * **Gnome sort** uses the common variant where the gnome remembers where it left off.
132
+
133
+ You can also ask the gem about its algorithms:
134
+
135
+ ```ruby
136
+ ArraySort.algorithms # => [:binary_insertion, :bubble, :bucket, :cocktail_shaker, ...]
137
+ ArraySort.algorithm(:tim).stable? # => true
138
+ ArraySort.algorithm(:tim).worst # => "n log n"
139
+ ```
140
+
31
141
  ## Usage
32
142
 
33
- All sort methods share the same signature of `Array#sort`, `Array#sort!`, `Array#sort_by` and `Array#sort_by!`. Simply
34
- change the names of the sorting methods to the following corresponding methods of a specific sorting algorithm:
143
+ The methods behave like their native counterparts (see the
144
+ [Ruby documentation](https://docs.ruby-lang.org/en/master/Array.html#method-i-sort)):
145
+
146
+ ```ruby
147
+ [3, 1, 2].insertion_sort # => [1, 2, 3]
148
+ [3, 1, 2].merge_sort { |a, b| b <=> a } # => [3, 2, 1]
149
+ [[3, 3], [1, 4], [2, 6]].insertion_sort_by { |e| e[0] } # => [[1, 4], [2, 6], [3, 3]]
150
+
151
+ array = [3, 1, 2]
152
+ array.smooth_sort! # => [1, 2, 3], and array is now sorted
153
+ ```
154
+
155
+ * Without a bang, a new `Array` is returned and the receiver is left alone.
156
+ * With a bang, the receiver is sorted in place and returned. If the comparison raises partway through, the receiver
157
+ is left unchanged. Calling a bang method on a frozen array raises `FrozenError`.
158
+ * A comparison block may return any number; only its sign matters. If two elements can't be compared (`<=>` or the
159
+ block returns `nil`), an `ArgumentError` is raised.
160
+ * The `_by` methods call their block exactly once per element, and return an `Enumerator` when called without one.
161
+
162
+ ### Distribution sorts
163
+
164
+ Counting, radix and bucket sort never compare elements. Instead they place each element by a numeric key, which is
165
+ the element itself for `x_sort`, or whatever the block returns for `x_sort_by`. Counting and radix sort need Integer
166
+ keys (negative and arbitrarily large ones are fine); bucket sort takes any finite real number. Because they don't
167
+ compare, `x_sort` and `x_sort!` raise `ArgumentError` when given a comparison block.
168
+
169
+ ```ruby
170
+ [10, -3, 2**70, 0].radix_sort # => [-3, 0, 10, 1180591620717411303424]
171
+ [0.42, 0.07, 0.99, 0.5].bucket_sort # => [0.07, 0.42, 0.5, 0.99]
172
+
173
+ people.radix_sort_by { |person| person[:age] } # stable, so people of the same age keep their order
174
+ ```
175
+
176
+ Counting sort allocates a table as large as the range of the keys, so it raises `RangeError` for ranges above
177
+ max(2²⁰, 4n) rather than running out of memory. Radix sort is the better choice there.
178
+
179
+ ### `Array#swap`
35
180
 
36
- * Bubble sort: `bubble_sort`, `bubble_sort!`, `bubble_sort_by`, `bubble_sort_by!`
37
- * Heap sort: `heap_sort`, `heap_sort!`, `heap_sort_by`, `heap_sort_by!`
38
- * Insertion sort: `insertion_sort`, `insertion_sort!`, `insertion_sort_by`, `insertion_sort_by!`
39
- * Merge sort: `merge_sort`, `merge_sort!`, `merge_sort_by`, `merge_sort_by!`
40
- * Quicksort: `quick_sort`, `quick_sort!`, `quick_sort_by`, `quick_sort_by!`
41
-
42
- See the official [Ruby documentation](https://ruby-doc.org/core-2.5.0/Array.html#method-i-sort) on how to use the native
43
- sorting methods of Array.
181
+ The gem also adds `swap`, which swaps two elements in place and returns the array:
44
182
 
45
- For example, to sort an array using insertion sort:
183
+ ```ruby
184
+ [1, 2, 3].swap(0, -1) # => [3, 2, 1]
185
+ ```
186
+
187
+ ## Tracing
188
+
189
+ `ArraySort.trace` sorts a copy of an array and records every comparison and every write the algorithm makes, which is
190
+ handy for visualizing algorithms or seeing how they differ.
46
191
 
47
192
  ```ruby
48
- [3, 1, 2].insertion_sort # => [1, 2, 3]
193
+ trace = ArraySort.trace(:insertion, [3, 1, 2])
194
+ trace.result # => [1, 2, 3]
195
+ trace.comparisons # => 3
196
+ trace.writes # => 4
197
+ trace.events
198
+ # => [#<data ArraySort::Trace::Comparison left=3, right=1, result=1>,
199
+ # #<data ArraySort::Trace::Write index=1, value=3>,
200
+ # #<data ArraySort::Trace::Write index=0, value=1>,
201
+ # #<data ArraySort::Trace::Comparison left=3, right=2, result=1>,
202
+ # #<data ArraySort::Trace::Write index=2, value=3>,
203
+ # #<data ArraySort::Trace::Comparison left=1, right=2, result=-1>,
204
+ # #<data ArraySort::Trace::Write index=1, value=2>]
49
205
 
50
- # The following two lines both produce [[1, 4], [2, 6], [3, 3]]
51
- [[3, 3], [1, 4], [2, 6]].insertion_sort { |a, b| a[0] <=> b[0] }
52
- [[3, 3], [1, 4], [2, 6]].insertion_sort_by { |e| e[0] }
206
+ # Stream events as they happen
207
+ ArraySort.trace(:heap, [5, 1, 4, 2, 3]) { |event| puts event.inspect }
53
208
  ```
54
209
 
210
+ Replaying the writes onto a copy of `trace.input` reproduces the sort step by step. A swap shows up as two writes.
211
+ For example, sorting `[5, 1, 4, 2, 3]`:
212
+
213
+ | Algorithm | Comparisons | Writes |
214
+ | --------- | ----------- | ------ |
215
+ | Bubble | 9 | 12 |
216
+ | Insertion | 9 | 10 |
217
+ | Selection | 10 | 8 |
218
+ | Cycle | 31 | 5 |
219
+ | Merge | 11 | 10 |
220
+ | Heap | 9 | 16 |
221
+
222
+ Elements are compared with `<=>`. Distribution sorts make no comparisons at all.
223
+
224
+ ## Benchmarks
225
+
226
+ `bundle exec rake bench` compares every algorithm with `Array#sort`. `SIZE`, `SHAPE` (`random`, `sorted`,
227
+ `reversed` or `few`) and `ALGORITHMS` (comma-separated names) are optional. For example:
228
+
229
+ $ SIZE=10000 SHAPE=sorted ALGORITHMS=merge,tim,smooth bundle exec rake bench
230
+
231
+ ## Type signatures
232
+
233
+ RBS signatures for the whole public API ship with the gem in `sig/array_sort.rbs`. They are generated from the
234
+ algorithm registry with `bundle exec rake sig`.
235
+
55
236
  ## Development
56
237
 
57
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
58
- also run `bin/console` for an interactive prompt that will allow you to experiment.
238
+ After checking out the repo, run `bin/setup` to install dependencies, then `bundle exec rake` to run the tests,
239
+ RuboCop and the RBS validation. You can also run `bin/console` for an interactive prompt that will allow you to
240
+ experiment.
241
+
242
+ Each algorithm lives in its own file in `lib/array_sort/algorithms/` and registers itself with its name, stability
243
+ and complexity. Registering it is all it takes for it to get its four `Array` methods, module functions, refinement,
244
+ tracing, benchmark and type signatures, and to run against the full shared test suite. To add one:
245
+
246
+ 1. Add `lib/array_sort/algorithms/x_sort.rb` and require it from `lib/array_sort.rb`. A comparison sort implements
247
+ `call(array, compare)`, a distribution sort `call(array, key)`; both sort `array` in place and may only change it
248
+ with single-element assignment (`array[i] = value`), which is what makes tracing work.
249
+ 2. Add its row to the tables above (a test checks that they match the registry).
250
+ 3. Run `bundle exec rake sig` to regenerate the type signatures.
251
+
252
+ ### Releasing
253
+
254
+ Releases are published from GitHub Actions with RubyGems trusted publishing, so no API key is needed. Once, on
255
+ rubygems.org, add a trusted publisher for this gem (repository `foxhatleo/array-sort`, workflow `release.yml`,
256
+ environment `release`). Then, for each release, bump `ArraySort::VERSION` and the CHANGELOG, merge to `master`, and
257
+ run the **Release** workflow from the Actions tab. It runs the tests, pushes the gem and tags the release.
59
258
 
60
259
  ## Contributing
61
260
 
62
261
  Bug reports and pull requests are welcome on GitHub at
63
262
  [https://github.com/foxhatleo/array-sort](https://github.com/foxhatleo/array-sort). This project is intended to be a
64
263
  safe, welcoming space for collaboration, and contributors are expected to adhere to the
65
- [Contributor Covenant](http://contributor-covenant.org) code of conduct.
264
+ [Contributor Covenant](https://www.contributor-covenant.org) code of conduct.
66
265
 
67
266
  ## License
68
267
 
data/lib/array/sort.rb CHANGED
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
- require 'array/sort/version'
5
- require 'array/sort/helper'
6
- require 'array/sort/merge_sort'
7
- require 'array/sort/insertion_sort'
8
- require 'array/sort/heap_sort'
9
- require 'array/sort/bubble_sort'
10
- require 'array/sort/quick_sort'
3
+ # Loads ArraySort and adds its sort methods to every Array. Use +require 'array_sort'+ instead to load it without
4
+ # changing Array.
5
+ require_relative '../array_sort'
6
+
7
+ Array.include(ArraySort::ArrayMethods)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Binary insertion sort: insertion sort that finds each element's position with a binary search.
5
+ #
6
+ # Needs only O(n log n) comparisons, which helps when comparing is expensive, but still moves O(n²) elements. The
7
+ # search finds the position after any equal elements, which keeps the sort stable.
8
+ #
9
+ # @api private
10
+ module BinaryInsertionSort
11
+ module_function
12
+
13
+ def call(array, compare)
14
+ (1...array.length).each do |i|
15
+ element = array[i]
16
+ position = upper_bound(array, compare, element, 0, i)
17
+ next if position == i
18
+
19
+ i.downto(position + 1) { |j| array[j] = array[j - 1] }
20
+ array[position] = element
21
+ end
22
+ array
23
+ end
24
+
25
+ # Returns the first index in +array[low...high]+ whose element is greater than +element+.
26
+ def upper_bound(array, compare, element, low, high)
27
+ while low < high
28
+ mid = (low + high) / 2
29
+ if compare.call(element, array[mid]).negative?
30
+ high = mid
31
+ else
32
+ low = mid + 1
33
+ end
34
+ end
35
+ low
36
+ end
37
+ end
38
+
39
+ register :binary_insertion, BinaryInsertionSort, title: 'Binary insertion sort', stable: true,
40
+ best: 'n log n', average: 'n²', worst: 'n²', space: '1'
41
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Bubble sort: repeatedly walks the array swapping adjacent elements that are out of order.
5
+ #
6
+ # Each pass remembers where its last swap happened; everything after that point is already in place, so the next
7
+ # pass stops there. An already-sorted array therefore takes a single pass.
8
+ #
9
+ # @api private
10
+ module BubbleSort
11
+ module_function
12
+
13
+ def call(array, compare)
14
+ unsorted_length = array.length
15
+ while unsorted_length > 1
16
+ last_swap = 0
17
+ (1...unsorted_length).each do |i|
18
+ next unless compare.call(array[i - 1], array[i]).positive?
19
+
20
+ array[i - 1], array[i] = array[i], array[i - 1]
21
+ last_swap = i
22
+ end
23
+ unsorted_length = last_swap
24
+ end
25
+ array
26
+ end
27
+ end
28
+
29
+ register :bubble, BubbleSort, title: 'Bubble sort', stable: true,
30
+ best: 'n', average: 'n²', worst: 'n²', space: '1'
31
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Bucket sort: spreads the elements over n buckets by where their key falls between the smallest and largest key,
5
+ # sorts each bucket with insertion sort, then concatenates the buckets.
6
+ #
7
+ # Keys can be any finite real numbers (Integer, Float, Rational, BigDecimal...). When they are spread evenly most
8
+ # buckets hold one or two elements and the sort takes O(n); when they cluster, a bucket can fill up and insertion
9
+ # sort makes it O(n²).
10
+ #
11
+ # @api private
12
+ module BucketSort
13
+ module_function
14
+
15
+ def call(array, key)
16
+ keys = Core.real_keys(array, key, 'bucket sort')
17
+ count = keys.length
18
+ return array if count < 2
19
+
20
+ min, max = keys.minmax
21
+ return array if min == max
22
+
23
+ buckets = Array.new(count) { [] }
24
+ keys.each_with_index do |k, i|
25
+ index = (fraction(k, min, max) * (count - 1)).floor.clamp(0, count - 1)
26
+ buckets[index] << i
27
+ end
28
+
29
+ elements = Array.new(array)
30
+ position = 0
31
+ buckets.each do |bucket|
32
+ insertion_sort_by_key(bucket, keys)
33
+ bucket.each do |i|
34
+ array[position] = elements[i]
35
+ position += 1
36
+ end
37
+ end
38
+ array
39
+ end
40
+
41
+ # Where +k+ falls between +min+ (0.0) and +max+ (1.0). Every step only ever rounds in the same direction for larger
42
+ # keys, so a larger key never lands in an earlier bucket.
43
+ def fraction(k, min, max)
44
+ span = max - min
45
+ return (k - min).fdiv(span) if span.finite?
46
+
47
+ # Only floats near Float::MAX get here: halving everything first keeps the arithmetic from overflowing.
48
+ ((k / 2) - (min / 2)) / ((max / 2) - (min / 2))
49
+ end
50
+
51
+ # Sorts a bucket (a list of element indices) by key. Only strictly larger keys move, so the sort is stable.
52
+ def insertion_sort_by_key(bucket, keys)
53
+ (1...bucket.length).each do |i|
54
+ index = bucket[i]
55
+ j = i
56
+ while j.positive? && keys[bucket[j - 1]] > keys[index]
57
+ bucket[j] = bucket[j - 1]
58
+ j -= 1
59
+ end
60
+ bucket[j] = index
61
+ end
62
+ end
63
+ end
64
+
65
+ register :bucket, BucketSort, title: 'Bucket sort', stable: true, kind: :distribution,
66
+ best: 'n', average: 'n', worst: 'n²', space: 'n'
67
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Cocktail shaker sort: bubble sort that alternates direction, carrying the largest element to the end on the way
5
+ # up and the smallest to the front on the way down.
6
+ #
7
+ # Moving in both directions fixes bubble sort's "turtles" (small elements near the end, which bubble sort moves only
8
+ # one step per pass). Each pass also narrows the unsorted range to where its last swap happened.
9
+ #
10
+ # @api private
11
+ module CocktailShakerSort
12
+ module_function
13
+
14
+ def call(array, compare)
15
+ low = 0
16
+ high = array.length - 1
17
+ while low < high
18
+ last_swap = low
19
+ (low...high).each 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
+ last_swap = i
24
+ end
25
+ high = last_swap
26
+
27
+ last_swap = high
28
+ (high - 1).downto(low) do |i|
29
+ next unless compare.call(array[i], array[i + 1]).positive?
30
+
31
+ array[i], array[i + 1] = array[i + 1], array[i]
32
+ last_swap = i + 1
33
+ end
34
+ low = last_swap
35
+ end
36
+ array
37
+ end
38
+ end
39
+
40
+ register :cocktail_shaker, CocktailShakerSort, title: 'Cocktail shaker sort', stable: true,
41
+ best: 'n', average: 'n²', worst: 'n²', space: '1'
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Comb sort: bubble sort that first compares elements far apart, shrinking the gap by a factor of 1.3 each pass.
5
+ #
6
+ # The long-range passes move small elements near the end ("turtles") forward quickly; once the gap reaches 1, a
7
+ # final bubble sort finishes the job on an array that is already nearly sorted.
8
+ #
9
+ # @api private
10
+ module CombSort
11
+ SHRINK_FACTOR = 1.3
12
+
13
+ module_function
14
+
15
+ def call(array, compare)
16
+ gap = array.length
17
+ loop do
18
+ gap = (gap / SHRINK_FACTOR).floor
19
+ break if gap <= 1
20
+
21
+ (0...(array.length - gap)).each do |i|
22
+ next unless compare.call(array[i], array[i + gap]).positive?
23
+
24
+ array[i], array[i + gap] = array[i + gap], array[i]
25
+ end
26
+ end
27
+ BubbleSort.call(array, compare)
28
+ end
29
+ end
30
+
31
+ register :comb, CombSort, title: 'Comb sort', stable: false,
32
+ best: 'n log n', average: 'n²/2ᵖ', worst: 'n²', space: '1'
33
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArraySort
4
+ # Counting sort: counts how many times each Integer key occurs, works out from the counts where each key's elements
5
+ # start, then places every element directly. It never compares elements.
6
+ #
7
+ # Takes O(n + k) time and space, where k is the range of the keys (largest minus smallest), so it is only practical
8
+ # when that range is small. Ranges above max(2^20, 4n) raise RangeError rather than allocating a huge table; radix
9
+ # sort handles wide ranges.
10
+ #
11
+ # @api private
12
+ module CountingSort
13
+ MAX_RANGE = 1 << 20
14
+
15
+ module_function
16
+
17
+ def call(array, key)
18
+ keys = Core.integer_keys(array, key, 'counting sort')
19
+ return array if keys.length < 2
20
+
21
+ min, max = keys.minmax
22
+ range = max - min + 1
23
+ limit = [MAX_RANGE, keys.length * 4].max
24
+ if range > limit
25
+ raise RangeError, "counting sort can't handle a key range of #{range} (limit #{limit}); try radix_sort instead"
26
+ end
27
+
28
+ # starts[k] is where the next element with key (min + k) goes.
29
+ starts = Array.new(range, 0)
30
+ keys.each { |k| starts[k - min] += 1 }
31
+ total = 0
32
+ starts.each_index do |i|
33
+ count = starts[i]
34
+ starts[i] = total
35
+ total += count
36
+ end
37
+
38
+ elements = Array.new(array)
39
+ elements.each_with_index do |element, i|
40
+ slot = keys[i] - min
41
+ array[starts[slot]] = element
42
+ starts[slot] += 1
43
+ end
44
+ array
45
+ end
46
+ end
47
+
48
+ register :counting, CountingSort, title: 'Counting sort', stable: true, kind: :distribution,
49
+ best: 'n + k', average: 'n + k', worst: 'n + k', space: 'n + k'
50
+ end