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
3751/** Turns on debugging output. */
3852const 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. */
4256export type Comparable = NonNullable < unknown >
4357
@@ -48,7 +62,7 @@ export type Comparable = NonNullable<unknown>
4862 */
4963export 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 */
5367export 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 */
6782export 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 */
279294export function fisherYates ( array :unknown [ ] , random :Generator < number > = xorshift32 ( ) ) {
280295 for ( let i = array . length - 1 ; i > 0 ; i -- ) {
0 commit comments