|
10 | 10 | */
|
11 | 11 | type Comparator<T> = (a: T, b: T) => number
|
12 | 12 |
|
13 |
| -// Minimum size of subarrays to be sorted using insertion sort before merging |
| 13 | +/** |
| 14 | + * Constants for the TimSort algorithm. |
| 15 | + */ |
14 | 16 | const MIN_MERGE = 32
|
15 | 17 | const MIN_GALLOP = 7
|
16 | 18 |
|
@@ -96,7 +98,7 @@ const merge = <T>(
|
96 | 98 | }
|
97 | 99 |
|
98 | 100 | /**
|
99 |
| - * Sorts an array using the Tim sort algorithm. |
| 101 | + * Sorts an array using the TimSort algorithm. |
100 | 102 | *
|
101 | 103 | * @typeparam T The type of elements in the array.
|
102 | 104 | * @param arr The array to sort.
|
@@ -140,31 +142,109 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
|
140 | 142 | }
|
141 | 143 |
|
142 | 144 | /**
|
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. |
144 | 198 | *
|
145 |
| - * @param minRunLength The minimum length of a run. |
| 199 | + * @param runs The stack of runs. |
146 | 200 | */
|
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-- |
155 | 208 | }
|
| 209 | + mergeAt(runs, n) |
| 210 | + } else if (runs[n].length <= runs[n + 1].length) { |
| 211 | + mergeAt(runs, n) |
| 212 | + } else { |
| 213 | + break |
156 | 214 | }
|
157 | 215 | }
|
158 | 216 | }
|
159 | 217 |
|
160 | 218 | // Determine the minimum run length
|
161 | 219 | const minRun = minRunLength(length)
|
| 220 | + let runs: { start: number; length: number }[] = [] |
162 | 221 |
|
163 | 222 | // 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 |
166 | 246 | }
|
167 | 247 |
|
168 |
| - // Merge runs |
169 |
| - mergeRuns(minRun) |
| 248 | + // Merge all remaining runs |
| 249 | + mergeForceCollapse(runs) |
170 | 250 | }
|
0 commit comments