Skip to content

Commit 4721905

Browse files
committed
ref: optimize merge runs
1 parent e7e6daa commit 4721905

File tree

1 file changed

+96
-16
lines changed

1 file changed

+96
-16
lines changed

sorts/tim_sort.ts

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
*/
1111
type Comparator<T> = (a: T, b: T) => number
1212

13-
// Minimum size of subarrays to be sorted using insertion sort before merging
13+
/**
14+
* Constants for the TimSort algorithm.
15+
*/
1416
const MIN_MERGE = 32
1517
const MIN_GALLOP = 7
1618

@@ -96,7 +98,7 @@ const merge = <T>(
9698
}
9799

98100
/**
99-
* Sorts an array using the Tim sort algorithm.
101+
* Sorts an array using the TimSort algorithm.
100102
*
101103
* @typeparam T The type of elements in the array.
102104
* @param arr The array to sort.
@@ -140,31 +142,109 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
140142
}
141143

142144
/**
143-
* Merges runs in the array.
145+
* Pushes a new run onto the stack of runs.
146+
*
147+
* @param runs The stack of runs.
148+
* @param start The starting index of the run.
149+
* @param length The length of the run.
150+
*/
151+
const pushRun = (
152+
runs: { start: number; length: number }[],
153+
start: number,
154+
length: number
155+
) => {
156+
runs.push({ start, length })
157+
}
158+
159+
/**
160+
* Merges two adjacent runs in the stack of runs.
161+
*
162+
* @param runs The stack of runs.
163+
* @param i The index of the first run to merge.
164+
*/
165+
const mergeAt = (runs: { start: number; length: number }[], i: number) => {
166+
const { start: start1, length: length1 } = runs[i]
167+
const { start: start2, length: length2 } = runs[i + 1]
168+
runs[i] = { start: start1, length: length1 + length2 }
169+
170+
merge(
171+
arr,
172+
start1,
173+
start1 + length1 - 1,
174+
start1 + length1 + length2 - 1,
175+
compare
176+
)
177+
178+
runs.splice(i + 1, 1)
179+
}
180+
181+
/**
182+
* Forces the collapse of all remaining runs in the stack.
183+
*
184+
* @param runs The stack of runs.
185+
*/
186+
const mergeForceCollapse = (runs: { start: number; length: number }[]) => {
187+
while (runs.length > 1) {
188+
let n = runs.length - 2
189+
if (n > 0 && runs[n - 1].length < runs[n + 1].length) {
190+
n--
191+
}
192+
mergeAt(runs, n)
193+
}
194+
}
195+
196+
/**
197+
* Ensures the runs maintain the size invariant required by TimSort.
144198
*
145-
* @param minRunLength The minimum length of a run.
199+
* @param runs The stack of runs.
146200
*/
147-
const mergeRuns = (minRunLength: number): void => {
148-
for (let size = minRunLength; size < length; size *= 2) {
149-
for (let left = 0; left < length; left += 2 * size) {
150-
const mid = Math.min(left + size - 1, length - 1)
151-
const right = Math.min(left + 2 * size - 1, length - 1)
152-
153-
if (mid < right) {
154-
merge(arr, left, mid, right, compare)
201+
const mergeCollapse = (runs: { start: number; length: number }[]) => {
202+
while (runs.length > 1) {
203+
let n = runs.length - 2
204+
205+
if (n > 0 && runs[n - 1].length <= runs[n].length + runs[n + 1].length) {
206+
if (runs[n - 1].length < runs[n + 1].length) {
207+
n--
155208
}
209+
mergeAt(runs, n)
210+
} else if (runs[n].length <= runs[n + 1].length) {
211+
mergeAt(runs, n)
212+
} else {
213+
break
156214
}
157215
}
158216
}
159217

160218
// Determine the minimum run length
161219
const minRun = minRunLength(length)
220+
let runs: { start: number; length: number }[] = []
162221

163222
// Find runs and sort them
164-
for (let i = 0; i < length; i += minRun) {
165-
findRunsAndSort(i, Math.min(i + minRun - 1, length - 1))
223+
let start = 0
224+
while (start < length) {
225+
let end = start + 1
226+
if (end < length && compare(arr[end - 1], arr[end]) <= 0) {
227+
// Ascending run
228+
while (end < length && compare(arr[end - 1], arr[end]) <= 0) {
229+
end++
230+
}
231+
} else {
232+
// Descending run
233+
while (end < length && compare(arr[end - 1], arr[end]) > 0) {
234+
end++
235+
}
236+
// Reverse descending run to make it ascending
237+
arr.slice(start, end).reverse()
238+
}
239+
240+
findRunsAndSort(start, end - 1)
241+
pushRun(runs, start, end - start)
242+
243+
mergeCollapse(runs)
244+
245+
start = end
166246
}
167247

168-
// Merge runs
169-
mergeRuns(minRun)
248+
// Merge all remaining runs
249+
mergeForceCollapse(runs)
170250
}

0 commit comments

Comments
 (0)