3737/** Turns on debugging output. */
3838const DEBUG :boolean = false
3939
40- /* istanbul ignore next */
41- function assert ( condition : unknown , msg ?: string ) : asserts condition { if ( ! condition ) throw new Error ( msg ) }
42-
4340/** A type of object that can be compared by a `Comparator` and therefore sorted by `mergeInsertionSort`.
4441 * Must have sensible support for the equality operators. */
4542export type Comparable = NonNullable < unknown >
@@ -100,7 +97,6 @@ export async function _binInsertIdx<T extends Comparable>(array :ReadonlyArray<T
10097 if ( c ) r = m - 1
10198 else l = m + 1
10299 }
103- assert ( l >= 0 && l <= array . length ) // paranoia
104100 /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'binary insert' , item , 'into' , array ,
105101 // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
106102 l === 0 ?'at start' :l === array . length ?'at end' :`before ${ array [ l ] } ` )
@@ -145,7 +141,7 @@ export default async function mergeInsertionSort<T extends Comparable>(array :Re
145141
146142 /* 4. Insert at the start of the sorted sequence the element that was paired with
147143 * the first and smallest element of the sorted sequence. */
148- assert ( mainChain . length > 0 ) // Known due to the special cases at the beginning of this function.
144+ // Note that we know the main chain has at least one item here due to the special cases at the beginning of this function.
149145 mainChain . unshift ( { item : mainChain [ 0 ] ! . smaller ! } )
150146 delete mainChain [ 1 ] ! . smaller
151147 /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'step 4: first pair' , mainChain )
@@ -168,7 +164,8 @@ export default async function mergeInsertionSort<T extends Comparable>(array :Re
168164 * to step 4 above, the item that would have been labeled y₁ has actually already become element x₁, and
169165 * therefore the element that would have been x₁ is now x₂ and no longer has a paired yᵢ element. It
170166 * follows that the first paired elements are x₃ and y₃, and so the first unsorted element to be inserted
171- * into the output sequence is y₃.
167+ * into the output sequence is y₃. Also noteworthy is that if the input had an odd number of elements,
168+ * the leftover unpaired element is treated as the last yᵢ element.
172169 *
173170 * ² In my opinion, this is lacking detail, and this seems to be true for the other two sources (Ford-Johnson
174171 * and Knuth) as well. So here is my attempt at adding more details to the explanation: The "main chain" is
@@ -177,39 +174,43 @@ export default async function mergeInsertionSort<T extends Comparable>(array :Re
177174 * with the various descriptions is that they don't explicitly explain that the insertion process shifts all
178175 * the indices of the array, and due to the nonlinear insertion order, this makes it tricky to keep track of
179176 * the correct array indices over which to perform the insertion search. So instead, below, I use a linear
180- * search to find the main chain item being operated on each time, which is expensive, but much easier.
177+ * search to find the main chain item being operated on each time, which is expensive, but much easier. It
178+ * 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.
181180 */
182181
183- /* Build the groups to be inserted (explained above), skipping the already handled first two items.
184- * (In the current implementation we don't need the original indices.) */
185- const groups = _makeGroups ( mainChain . slice ( 2 ) ) . map ( g => g [ 1 ] )
182+ // Build the groups to be inserted (explained above), skipping the already handled first two items.
183+ 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. */
187+ if ( array . length % 2 ) toInsert . push ( { item : array [ array . length - 1 ] ! } )
188+ // In the current implementation we don't need the original indices.
189+ const groups = _makeGroups ( toInsert ) . map ( g => g [ 1 ] )
186190 /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'step pre5: groups' , groups )
187191
188192 for ( const pair of groups ) {
189- assert ( pair . smaller != undefined )
190- // Locate the pair we're about to insert in the main chain, to limit the extent of the binary search (see also explanation above).
191- const pairIdx = mainChain . findIndex ( v => Object . is ( v , pair ) )
192- // Locate the index in the main chain where the pair's smaller item needs to be inserted, and insert it.
193- const insertIdx = await _binInsertIdx ( mainChain . slice ( 0 , pairIdx ) . map ( p => p . item ) , pair . smaller , comparator )
194- /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'will insert' , pair , 'at index' , insertIdx )
195- mainChain . splice ( insertIdx , 0 , { item : pair . smaller } )
193+ // Determine which item to insert and where.
194+ 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.
197+ return [ pair . item , await _binInsertIdx ( mainChain . map ( p => p . item ) , pair . item , comparator ) ]
198+ else {
199+ // Locate the pair we're about to insert in the main chain, to limit the extent of the binary search (see also explanation above).
200+ const pairIdx = mainChain . findIndex ( v => Object . is ( v , pair ) )
201+ // Locate the index in the main chain where the pair's smaller item needs to be inserted, and insert it.
202+ return [ pair . smaller , await _binInsertIdx ( mainChain . slice ( 0 , pairIdx ) . map ( p => p . item ) , pair . smaller , comparator ) ]
203+ }
204+ } ) ( )
205+ // Actually do the insertion.
206+ mainChain . splice ( insertIdx , 0 , { item : insertItem } )
196207 delete pair . smaller
197- /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'main chain is now' , mainChain )
208+ /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'inserted' , insertItem , 'at index' , insertIdx , ' main chain is now', mainChain )
198209 }
199210
211+ /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'fordJohnson done' , mainChain )
200212 // Turn the "main chain" data structure back into an array of values.
201- const results = mainChain . map ( pair => { assert ( pair . smaller === undefined ) ; return pair . item } )
202-
203- // If there was a leftover item from an odd input length, insert that last.
204- if ( array . length % 2 ) {
205- const item = array [ array . length - 1 ] !
206- const idx = await _binInsertIdx ( results , item , comparator )
207- /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'inserting odd item' , item , 'at' , idx )
208- results . splice ( idx , 0 , item )
209- }
210-
211- /* istanbul ignore next */ if ( DEBUG ) console . debug ( 'fordJohnson done' , results )
212- return results
213+ return mainChain . map ( pair => pair . item )
213214}
214215
215216/** Returns the maximum number of comparisons that `mergeInsertionSort` will perform depending on the input length `n`.
0 commit comments