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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2f62e8f149a2fed7b2f3d2b9e2eb69a1944559122dd4012972fd2f2bc029dd5
|
|
4
|
+
data.tar.gz: 20498925f9a617fc59913a762e5d7997d09a765dfadfbf7e8583a2ad89c85d98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/array-sort)
|
|
4
|
+
[](https://github.com/foxhatleo/array-sort/actions/workflows/ci.yml)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
51
|
-
|
|
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
|
|
58
|
-
also run `bin/console` for an interactive prompt that will allow you to
|
|
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](
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|