Skip to content

Commit 16041db

Browse files
committed
Some proofreading / tweaks / release prep
1 parent ebfae6f commit 16041db

File tree

6 files changed

+89
-56
lines changed

6 files changed

+89
-56
lines changed

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/*.tgz
66
__tests__/
77
/eslint.config.js
8+
tsconfig.json

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
Merge-Insertion Sort a.k.a. Ford-Johnson Algorithm
2-
==================================================
2+
===================================================
33

44
The Ford-Johnson algorithm[1], also known as the merge-insertion sort[2,3] uses the minimum
55
number of possible comparisons for lists of 22 items or less, and at the time of writing has
66
the fewest comparisons known for lists of 46 items or less. It is therefore very well suited
77
for cases where comparisons are expensive, such as user input, and the API is implemented to
88
take an async comparator function for this reason.
99

10+
### Example
11+
12+
```typescript
13+
import { mergeInsertionSort, Comparator } from 'merge-insertion'
14+
15+
// A Comparator should return 0 if the first item is larger, or 1 if the second item is larger.
16+
const comp :Comparator<string> = async ([a, b]) => a > b ? 0 : 1
17+
18+
// Sort five items in ascending order with a maximum of only seven comparisons:
19+
const sorted = await mergeInsertionSort(['D', 'A', 'B', 'E', 'C'], comp)
20+
```
21+
22+
### References
23+
1024
1. Ford, L. R., & Johnson, S. M. (1959). A Tournament Problem.
1125
The American Mathematical Monthly, 66(5), 387–389. <https://doi.org/10.1080/00029890.1959.11989306>
12-
2. Knuth, D. E. (1998). The Art of Computer Programming: Volume 3: Sorting and Searching (2nd ed.). Addison-Wesley.
26+
2. Knuth, D. E. (1998). The Art of Computer Programming: Volume 3: Sorting and Searching (2nd ed.).
27+
Addison-Wesley. <https://cs.stanford.edu/~knuth/taocp.html#vol3>
1328
3. <https://en.wikipedia.org/wiki/Merge-insertion_sort>
1429

1530
## Type Aliases
@@ -18,7 +33,7 @@ take an async comparator function for this reason.
1833

1934
> **Comparable** = `NonNullable`\<`unknown`\>
2035
21-
A type of object that can be compared by a `Comparator` and therefore sorted by `mergeInsertionSort`.
36+
A type of object that can be compared by a [Comparator](#comparator) and therefore sorted by [mergeInsertionSort](#mergeinsertionsort).
2237
Must have sensible support for the equality operators.
2338

2439
***
@@ -55,7 +70,7 @@ Must return a Promise resolving to 0 if the first item is ranked higher, or 1 if
5570

5671
> **mergeInsertionMaxComparisons**(`n`): `number`
5772
58-
Returns the maximum number of comparisons that `mergeInsertionSort` will perform depending on the input length `n`.
73+
Returns the maximum number of comparisons that [mergeInsertionSort](#mergeinsertionsort) will perform depending on the input length.
5974

6075
#### Parameters
6176

@@ -105,7 +120,7 @@ Async comparison function.
105120

106121
`Promise`\<`T`[]\>
107122

108-
A shallow copy of the array sorted in ascending order.
123+
A Promise resolving to a shallow copy of the array sorted in ascending order.
109124

110125
Author, Copyright and License
111126
-----------------------------

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "merge-insertion",
3-
"version": "0.1.0",
3+
"version": "1.0.0",
44
"description": "The merge-insertion sort (aka the Ford-Johnson algorithm) is optimized for using few comparisons.",
55
"author": {
6-
"name": "Hauke D",
6+
"name": "Hauke Dämpfling",
77
"email": "haukex@zero-g.net"
88
},
99
"license": "ISC",
@@ -47,4 +47,4 @@
4747
}
4848
}
4949
}
50-
}
50+
}

src/__tests__/merge-insertion.test.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1515
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1616
*/
17-
import mergeInsertionSort, { _binInsertIdx, _groupSizes, _makeGroups, Comparable, Comparator,
17+
import { mergeInsertionSort, _binInsertIdx, _groupSizes, _makeGroups, Comparable, Comparator,
1818
fisherYates, mergeInsertionMaxComparisons, permutations, xorshift32 } from '../merge-insertion'
1919
import { describe, expect, test } from '@jest/globals'
2020

21-
const comp :Comparator<string> = ([a,b]) => Promise.resolve(a>b?0:1)
21+
const comp :Comparator<string> = async ([a, b]) => a > b ? 0 : 1
2222

2323
test('smoke', async () => {
2424
expect( await mergeInsertionSort(['D','E','A','C','F','B','G'],
@@ -54,13 +54,14 @@ test('_binInsertIdx', async () => {
5454
// A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
5555
const a = ['B','D','F','H','J','L','N','P','R','T','V','X','Z']
5656

57-
await expect( _binInsertIdx(a.slice(0,5), 'J', comp) ).rejects.toThrow('already in')
57+
await expect( _binInsertIdx(a, 'J', comp) ).rejects.toThrow('already in')
5858

59-
expect( await _binInsertIdx([], 'A', testComp(comp,0)) ).toStrictEqual(0)
60-
expect( await _binInsertIdx(['B'], 'A', testComp(comp,1)) ).toStrictEqual(0)
61-
expect( await _binInsertIdx(['B'], 'C', testComp(comp,1)) ).toStrictEqual(1)
59+
expect( await _binInsertIdx([], 'A', testComp(comp,0)) ).toStrictEqual(0)
60+
expect( await _binInsertIdx(['B'], 'A', testComp(comp,1)) ).toStrictEqual(0)
61+
expect( await _binInsertIdx(['B'], 'C', testComp(comp,1)) ).toStrictEqual(1)
6262

63-
/* while L ≤ R:
63+
/* Adapted from <https://en.wikipedia.org/wiki/Binary_search>:
64+
* while L ≤ R:
6465
* M = L + floor( (R - L) / 2 )
6566
* if T < A[M] then: R = M − 1
6667
* else: L = M + 1 */
@@ -232,7 +233,7 @@ function testComp<T extends Comparable>(c :Comparator<T>, maxCalls :number, log
232233
else pairMap.set(a, new Map([[b, null]]))
233234
if (++callCount > maxCalls)
234235
throw new Error(`too many Comparator calls (${callCount})`)
235-
if (log!==undefined) log.push([a,b])
236+
if (log!=undefined) log.push([a,b])
236237
return await c([a,b])
237238
}
238239
}
@@ -312,11 +313,12 @@ describe('mergeInsertionSort', () => {
312313
try {
313314
// in order array
314315
expect( await mergeInsertionSort(a, testComp(comp, mergeInsertionMaxComparisons(len))) ).toStrictEqual(array)
316+
if (len<2) return
315317
// reverse order
316318
a.reverse()
317319
expect( await mergeInsertionSort(a, testComp(comp, mergeInsertionMaxComparisons(len))) ).toStrictEqual(array)
318-
// a few shuffles
319-
for (let i=0;i<10;i++) {
320+
// several shuffles
321+
for (let i=0;i<100;i++) {
320322
fisherYates(a)
321323
expect( await mergeInsertionSort(a, testComp(comp, mergeInsertionMaxComparisons(len))) ).toStrictEqual(array)
322324
}
@@ -326,8 +328,8 @@ describe('mergeInsertionSort', () => {
326328
}
327329
})
328330

329-
// 6! = 720, 7! = 5040, 8! = 40320, 9! = 362880 - Be very careful with increasing this!
330-
test.each( Array.from({ length: 8 }, (_, i) => ({ len: i })) )('all perms of length $len', async ({len}) => {
331+
// 6! = 720, 7! = 5040, 8! = 40320, 9! = 362880, 10! = 3628800 - Be very careful with increasing this!
332+
test.each( Array.from({ length: 9 }, (_, i) => ({ len: i })) )('all perms of length $len', async ({len}) => {
331333
const array :Readonly<string[]> = Array.from({ length: len }, (_,i) => String.fromCharCode(65 + i))
332334
for (const perm of permutations(array))
333335
try {
@@ -338,7 +340,7 @@ describe('mergeInsertionSort', () => {
338340
}
339341
})
340342

341-
})
343+
}) // describe('mergeInsertionSort', ...)
342344

343345
test('mergeInsertionMaxComparisons', () => {
344346
// <https://oeis.org/A001768>: "Sorting numbers: number of comparisons for merge insertion sort of n elements." (plus 0=0)

src/merge-insertion.ts

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
1-
/**
2-
* Merge-Insertion Sort a.k.a. Ford-Johnson Algorithm
3-
* ==================================================
1+
/** Merge-Insertion Sort a.k.a. Ford-Johnson Algorithm
2+
* ===================================================
43
*
54
* The Ford-Johnson algorithm[1], also known as the merge-insertion sort[2,3] uses the minimum
65
* number of possible comparisons for lists of 22 items or less, and at the time of writing has
76
* the fewest comparisons known for lists of 46 items or less. It is therefore very well suited
87
* for cases where comparisons are expensive, such as user input, and the API is implemented to
98
* take an async comparator function for this reason.
109
*
10+
* ### Example
11+
*
12+
* ```typescript
13+
* import { mergeInsertionSort, Comparator } from 'merge-insertion'
14+
*
15+
* // A Comparator should return 0 if the first item is larger, or 1 if the second item is larger.
16+
* const comp :Comparator<string> = async ([a, b]) => a > b ? 0 : 1
17+
*
18+
* // Sort five items in ascending order with a maximum of only seven comparisons:
19+
* const sorted = await mergeInsertionSort(['D', 'A', 'B', 'E', 'C'], comp)
20+
* ```
21+
*
22+
* ### References
23+
*
1124
* 1. Ford, L. R., & Johnson, S. M. (1959). A Tournament Problem.
1225
* The American Mathematical Monthly, 66(5), 387–389. <https://doi.org/10.1080/00029890.1959.11989306>
13-
* 2. Knuth, D. E. (1998). The Art of Computer Programming: Volume 3: Sorting and Searching (2nd ed.). Addison-Wesley.
26+
* 2. Knuth, D. E. (1998). The Art of Computer Programming: Volume 3: Sorting and Searching (2nd ed.).
27+
* Addison-Wesley. <https://cs.stanford.edu/~knuth/taocp.html#vol3>
1428
* 3. <https://en.wikipedia.org/wiki/Merge-insertion_sort>
1529
*
1630
* Author, Copyright and License
@@ -37,7 +51,7 @@
3751
/** Turns on debugging output. */
3852
const DEBUG :boolean = false
3953

40-
/** A type of object that can be compared by a `Comparator` and therefore sorted by `mergeInsertionSort`.
54+
/** A type of object that can be compared by a {@link Comparator} and therefore sorted by {@link mergeInsertionSort}.
4155
* Must have sensible support for the equality operators. */
4256
export type Comparable = NonNullable<unknown>
4357

@@ -48,7 +62,7 @@ export type Comparable = NonNullable<unknown>
4862
*/
4963
export type Comparator<T extends Comparable> = (ab :Readonly<[a :T, b :T]>) => Promise<0|1>
5064

51-
/** Helper that generates the group sizes for `_makeGroups`.
65+
/** Helper that generates the group sizes for {@link _makeGroups}.
5266
* @internal */
5367
export function* _groupSizes() :Generator<number, never, never> {
5468
// <https://en.wikipedia.org/wiki/Merge-insertion_sort>:
@@ -63,43 +77,42 @@ export function* _groupSizes() :Generator<number, never, never> {
6377
}
6478

6579
/** Helper function to group and reorder items to be inserted via binary search.
80+
* See also the description in the code of {@link mergeInsertionSort}.
6681
* @internal */
6782
export function _makeGroups<T>(array :ReadonlyArray<T>) :[origIdx :number, item :T][] {
68-
// See the description in `_fordJohnson`.
6983
const items :ReadonlyArray<[number, T]> = array.map((e,i) => [i, e])
7084
const rv :[number,T][] = []
7185
const gen = _groupSizes()
7286
let i = 0
7387
while (true) {
74-
const curGroupSize = gen.next().value
75-
const curGroup = items.slice(i, i+curGroupSize)
76-
curGroup.reverse()
77-
rv.push(...curGroup)
78-
if (curGroup.length < curGroupSize) break
79-
i += curGroupSize
88+
const size = gen.next().value
89+
const group = items.slice(i, i+size)
90+
group.reverse()
91+
rv.push(...group)
92+
if (group.length < size) break
93+
i += size
8094
}
8195
return rv
8296
}
8397

8498
/** Helper function to insert an item into a sorted array via binary search.
8599
* @returns The index **before** which to insert the new item, e.g. `array.splice(index, 0, item)`.
86100
* @internal */
87-
export async function _binInsertIdx<T extends Comparable>(array :ReadonlyArray<T>, item :T, comparator :Comparator<T>) :Promise<number> {
88-
for (const e of array) if (e===item) throw new Error('item is already in target array')
89-
if (array.length<1) { return 0 }
90-
if (array.length==1) { return await comparator([ item, array[0]! ]) ? 0 : 1 }
101+
export async function _binInsertIdx<T extends Comparable>(array :ReadonlyArray<T>, item :T, comp :Comparator<T>) :Promise<number> {
102+
if (array.length<1) return 0
103+
if (array.indexOf(item)>=0) throw new Error('item is already in target array')
104+
if (array.length==1) return await comp([ item, array[0]! ]) ? 0 : 1
91105
/* istanbul ignore next */ if (DEBUG) console.debug('binary insert',item,'into',array)
92106
let l = 0, r = array.length-1
93107
while (l <= r) {
94108
const m = l + Math.floor((r-l)/2)
95-
const c = await comparator([item, array[m]!])
109+
const c = await comp([item, array[m]!])
96110
/* istanbul ignore next */ if (DEBUG) console.debug('left',l,'mid',m,'right',r,'item',item,c?'<':'>','array[m]',array[m])
97111
if (c) r = m - 1
98112
else l = m + 1
99113
}
100-
/* istanbul ignore next */ if (DEBUG) console.debug('binary insert',item,'into',array,
101-
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
102-
l===0?'at start':l===array.length?'at end':`before ${array[l]}`)
114+
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
115+
/* istanbul ignore next */ if (DEBUG) console.debug('insert', l===0?'at start':l===array.length?'at end':`before [${l}] '${array[l]}'`)
103116
return l
104117
}
105118

@@ -108,9 +121,9 @@ export async function _binInsertIdx<T extends Comparable>(array :ReadonlyArray<T
108121
* @typeParam T The type of the items to sort.
109122
* @param array Array of to sort. Duplicate items are not allowed.
110123
* @param comparator Async comparison function.
111-
* @returns A shallow copy of the array sorted in ascending order.
124+
* @returns A Promise resolving to a shallow copy of the array sorted in ascending order.
112125
*/
113-
export default async function mergeInsertionSort<T extends Comparable>(array :ReadonlyArray<T>, comparator :Comparator<T>) :Promise<T[]> {
126+
export async function mergeInsertionSort<T extends Comparable>(array :ReadonlyArray<T>, comparator :Comparator<T>) :Promise<T[]> {
114127
if (array.length<1) return []
115128
if (array.length==1) return Array.from(array)
116129
if (array.length != new Set(array).size) throw new Error('array may not contain duplicate items')
@@ -155,9 +168,9 @@ export default async function mergeInsertionSort<T extends Comparable>(array :Re
155168
* b. Order the un-inserted elements by their groups (smaller indexes to larger indexes), but within each
156169
* group order them from larger indexes to smaller indexes. Thus, the ordering becomes:
157170
* y₄, y₃, y₆, y₅, y₁₂, y₁₁, y₁₀, y₉, y₈, y₇, y₂₂, y₂₁, ...
158-
* c. Use this ordering to insert the elements yᵢ into the output sequence². For each element yᵢ,
171+
* c. Use this ordering to insert the elements yᵢ into the output sequence. For each element yᵢ,
159172
* use a binary search from the start of the output sequence up to but not including xᵢ to determine
160-
* where to insert yᵢ.
173+
* where to insert yᵢ.²
161174
*
162175
* ¹ My explanation: The items already in the sorted output sequence (the larger elements of each pair) are
163176
* labeled xᵢ and the yet unsorted (smaller) elements are labeled yᵢ, with i starting at 1. However, due
@@ -176,24 +189,26 @@ export default async function mergeInsertionSort<T extends Comparable>(array :Re
176189
* the correct array indices over which to perform the insertion search. So instead, below, I use a linear
177190
* search to find the main chain item being operated on each time, which is expensive, but much easier. It
178191
* should also be noted that the leftover unpaired element, if there is one, gets inserted across the whole
179-
* main chain as it exists at the time of its insertion - because it may not be inserted last.
192+
* main chain as it exists at the time of its insertion - it may not be inserted last. So even though there
193+
* is still some optimization potential, this algorithm is used in cases where the comparisons are much more
194+
* expensive than the rest of the algorithm, so the cost is acceptable for now.
180195
*/
181196

182197
// Build the groups to be inserted (explained above), skipping the already handled first two items.
183198
const toInsert = mainChain.slice(2)
184-
/* If there was a leftover item from an odd input length, treat it as the last "smaller" item (special handling below).
185-
* We'll use the fact that at this point, all items in `toInsert` have their `.smaller` property set, so we'll mark
186-
* the leftover item as a special case by it not having its `.smaller` set. */
199+
/* If there was a leftover item from an odd input length, treat it as the last "smaller" item. We'll use the
200+
* fact that at this point, all items in `toInsert` have their `.smaller` property set, so we'll mark the
201+
* leftover item as a special case by storing it in `.item` and it not having its `.smaller` set. */
187202
if (array.length%2) toInsert.push({ item: array[array.length - 1]! })
188-
// In the current implementation we don't need the original indices.
203+
// Make the groups; in the current implementation we don't need the original indices returned here.
189204
const groups = _makeGroups(toInsert).map(g=>g[1])
190205
/* istanbul ignore next */ if (DEBUG) console.debug('step pre5: groups',groups)
191206

192207
for (const pair of groups) {
193208
// Determine which item to insert and where.
194209
const [insertItem, insertIdx] :[T, number] = await (async () => {
195-
if (pair.smaller===undefined) // see explanation above
196-
// This is the leftover item, it gets inserted into the whole main chain.
210+
if (pair.smaller===undefined) // See explanation of this special case above.
211+
// This is the leftover item, it gets inserted into the current whole main chain.
197212
return [pair.item, await _binInsertIdx(mainChain.map(p => p.item), pair.item, comparator)]
198213
else {
199214
// Locate the pair we're about to insert in the main chain, to limit the extent of the binary search (see also explanation above).
@@ -213,7 +228,7 @@ export default async function mergeInsertionSort<T extends Comparable>(array :Re
213228
return mainChain.map( pair => pair.item )
214229
}
215230

216-
/** Returns the maximum number of comparisons that `mergeInsertionSort` will perform depending on the input length `n`.
231+
/** Returns the maximum number of comparisons that {@link mergeInsertionSort} will perform depending on the input length.
217232
*
218233
* @param n The number of items in the list to be sorted.
219234
* @returns The expected maximum number of comparisons.
@@ -274,7 +289,7 @@ export function* xorshift32() :Generator<number, never, never> {
274289
* 2. Durstenfeld, R. (1964). Algorithm 235: Random permutation. Communications of the ACM, 7(7), 420. doi:10.1145/364520.364540
275290
*
276291
* @param random Generator that returns random integers in the range `0` (inclusive) and `>= array.length-1`.
277-
* The default is `xorshift32` - which may not return integers big enough for very large arrays!
292+
* The default is {@link xorshift32} - which may not return integers big enough for very large arrays!
278293
* @internal */
279294
export function fisherYates(array :unknown[], random :Generator<number> = xorshift32()) {
280295
for (let i=array.length-1; i>0; i--) {

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@
4242
"dev",
4343
"dist"
4444
],
45-
}
45+
}

0 commit comments

Comments
 (0)