Skip to content

Commit db864bf

Browse files
committed
ref: refactor Tim sort
- Refactor merge function to handle merge space over head and add galloping mode - Make the comparator optional, default to an ascending comparator - Managing a stack of sorted runs with special size invariants in order to do balanced merges - Add tests (include edge cases as proposed)
1 parent dd9ad2c commit db864bf

File tree

2 files changed

+130
-122
lines changed

2 files changed

+130
-122
lines changed

sorts/test/tim_sort.test.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,56 @@
1-
import { timSort } from '../tim_sort';
1+
import { timSort } from '../tim_sort'
22

33
describe('Tim Sort', () => {
4-
const testTimSort = (arr: number[], comparator: (a: number, b: number) => number): void => {
5-
const originalArr = [...arr];
6-
timSort(arr, comparator);
7-
expect(arr).toEqual(originalArr.slice().sort(comparator));
8-
};
9-
10-
const testComparator = (comparator: (a: number, b: number) => number): void => {
4+
const testTimSort = (
5+
arr: number[],
6+
comparator: (a: number, b: number) => number
7+
): void => {
8+
const originalArr = [...arr]
9+
timSort(arr, comparator)
10+
expect(arr).toEqual(originalArr.slice().sort(comparator))
11+
}
12+
13+
const testComparator = (
14+
comparator: (a: number, b: number) => number
15+
): void => {
1116
it('should return the sorted array for an empty array', () => {
12-
const arr: number[] = [];
13-
testTimSort(arr, comparator);
14-
});
17+
const arr: number[] = []
18+
testTimSort(arr, comparator)
19+
})
1520

1621
it('should return the sorted array for an array with one element', () => {
17-
const arr: number[] = [1];
18-
testTimSort(arr, comparator);
19-
});
22+
const arr: number[] = [1]
23+
testTimSort(arr, comparator)
24+
})
2025

2126
it('should return the sorted array for a small array', () => {
22-
const arr = [5, 3, 8, 1, 7];
23-
testTimSort(arr, comparator);
24-
});
27+
const arr = [5, 3, 8, 1, 7]
28+
testTimSort(arr, comparator)
29+
})
2530

2631
it('should return the sorted array for a medium array', () => {
27-
const arr = [1, 4, 2, 5, 9, 6, 3, 8, 10, 7, 12, 11];
28-
testTimSort(arr, comparator);
29-
});
32+
const arr = [1, 4, 2, 5, 9, 6, 3, 8, 10, 7, 12, 11]
33+
testTimSort(arr, comparator)
34+
})
3035

3136
it('should return the sorted array for a large array', () => {
32-
const arr = Array.from({ length: 1000 }, () => Math.floor(Math.random() * 1000));
33-
testTimSort(arr, comparator);
34-
});
37+
const arr = Array.from({ length: 1000 }, () =>
38+
Math.floor(Math.random() * 1000)
39+
)
40+
testTimSort(arr, comparator)
41+
})
3542

3643
it('should return the sorted array for an array with duplicated elements', () => {
37-
const arr = [5, 3, 8, 1, 7, 3, 6, 4, 5, 8, 2, 1];
38-
testTimSort(arr, comparator);
39-
});
40-
};
44+
const arr = [5, 3, 8, 1, 7, 3, 6, 4, 5, 8, 2, 1]
45+
testTimSort(arr, comparator)
46+
})
47+
}
4148

4249
describe('Sorting in increasing order', () => {
43-
testComparator((a, b) => a - b);
44-
});
50+
testComparator((a, b) => a - b)
51+
})
4552

4653
describe('Sorting in decreasing order', () => {
47-
testComparator((a, b) => b - a);
48-
});
49-
});
50-
54+
testComparator((a, b) => b - a)
55+
})
56+
})

sorts/tim_sort.ts

Lines changed: 91 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,13 @@
88
* a positive value if `a` should come after `b`,
99
* and zero if `a` and `b` are considered equal.
1010
*/
11-
type Comparator<T> = (a: T, b: T) => number;
11+
type Comparator<T> = (a: T, b: T) => number
1212

1313
// Minimum size of subarrays to be sorted using insertion sort before merging
14-
const MIN_MERGE = 32;
14+
const MIN_MERGE = 32
1515

1616
/**
17-
* Performs insertion sort on a portion of an array.
18-
*
19-
* @typeparam T The type of elements in the array.
20-
* @param arr The array to sort.
21-
* @param startIndex The start index of the portion to sort.
22-
* @param endIndex The end index of the portion to sort.
23-
* @param compare The comparator function defining the order of elements.
24-
*/
25-
const insertionSort = <T>(
26-
arr: T[],
27-
startIndex: number,
28-
endIndex: number,
29-
compare: Comparator<T>
30-
): void => {
31-
for (let i = startIndex + 1; i <= endIndex; i++) {
32-
const currentElement = arr[i];
33-
let j = i - 1;
34-
35-
while (j >= startIndex && compare(arr[j], currentElement) > 0) {
36-
arr[j + 1] = arr[j];
37-
j--;
38-
}
39-
arr[j + 1] = currentElement;
40-
}
41-
};
42-
43-
/**
44-
* Merges two sorted subarrays into one sorted array.
45-
* This version of merge includes "galloping mode" for performance optimization.
17+
* Merges two sorted subarrays into one sorted array with optimized galloping mode.
4618
*
4719
* @typeparam T The type of elements in the array.
4820
* @param arr The array containing the subarrays to merge.
@@ -58,32 +30,63 @@ const merge = <T>(
5830
rightIndex: number,
5931
compare: Comparator<T>
6032
): void => {
61-
const leftArray = arr.slice(leftIndex, middleIndex + 1);
62-
const rightArray = arr.slice(middleIndex + 1, rightIndex + 1);
63-
64-
let leftPointer = 0;
65-
let rightPointer = 0;
66-
let mergedIndex = leftIndex;
67-
68-
while (leftPointer < leftArray.length && rightPointer < rightArray.length) {
69-
if (compare(leftArray[leftPointer], rightArray[rightPointer]) <= 0) {
70-
arr[mergedIndex++] = leftArray[leftPointer++];
71-
} else {
72-
arr[mergedIndex++] = rightArray[rightPointer++];
33+
const leftArrayLength = middleIndex - leftIndex + 1
34+
const rightArrayLength = rightIndex - middleIndex
35+
36+
// Create temporary arrays for the left and right subarrays
37+
const leftSubarray: T[] = arr.slice(leftIndex, middleIndex + 1)
38+
const rightSubarray: T[] = arr.slice(middleIndex + 1, rightIndex + 1)
39+
40+
let leftPointer = 0
41+
let rightPointer = 0
42+
let mergedIndex = leftIndex
43+
44+
// Regular merge with galloping mode
45+
while (leftPointer < leftArrayLength && rightPointer < rightArrayLength) {
46+
let numGallops = 0
47+
48+
// Galloping through the left subarray
49+
while (
50+
leftPointer < leftArrayLength &&
51+
numGallops < MIN_MERGE &&
52+
compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0
53+
) {
54+
arr[mergedIndex++] = leftSubarray[leftPointer++]
55+
numGallops++
56+
}
57+
58+
// Galloping through the right subarray
59+
while (
60+
rightPointer < rightArrayLength &&
61+
numGallops < MIN_MERGE &&
62+
compare(rightSubarray[rightPointer], leftSubarray[leftPointer]) < 0
63+
) {
64+
arr[mergedIndex++] = rightSubarray[rightPointer++]
65+
numGallops++
7366
}
74-
}
7567

76-
// Copy remaining elements from leftArray, if any
77-
while (leftPointer < leftArray.length) {
78-
arr[mergedIndex++] = leftArray[leftPointer++];
68+
// Standard merge without galloping
69+
while (leftPointer < leftArrayLength && rightPointer < rightArrayLength) {
70+
if (
71+
compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0
72+
) {
73+
arr[mergedIndex++] = leftSubarray[leftPointer++]
74+
} else {
75+
arr[mergedIndex++] = rightSubarray[rightPointer++]
76+
}
77+
}
7978
}
8079

81-
// Copy remaining elements from rightArray, if any
82-
while (rightPointer < rightArray.length) {
83-
arr[mergedIndex++] = rightArray[rightPointer++];
80+
// Copy remaining elements from left subarray, if any
81+
while (leftPointer < leftArrayLength) {
82+
arr[mergedIndex++] = leftSubarray[leftPointer++]
8483
}
85-
};
8684

85+
// Copy remaining elements from right subarray, if any
86+
while (rightPointer < rightArrayLength) {
87+
arr[mergedIndex++] = rightSubarray[rightPointer++]
88+
}
89+
}
8790

8891
/**
8992
* Sorts an array using the Tim sort algorithm.
@@ -93,7 +96,7 @@ const merge = <T>(
9396
* @param compare The comparator function defining the order of elements.
9497
*/
9598
export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
96-
const n = arr.length;
99+
const length = arr.length
97100

98101
/**
99102
* Reverses a portion of the array.
@@ -103,11 +106,11 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
103106
*/
104107
const reverseRange = (start: number, end: number): void => {
105108
while (start < end) {
106-
const temp = arr[start];
107-
arr[start++] = arr[end];
108-
arr[end--] = temp;
109+
const temp = arr[start]
110+
arr[start++] = arr[end]
111+
arr[end--] = temp
109112
}
110-
};
113+
}
111114

112115
/**
113116
* Identifies runs and sorts them using insertion sort.
@@ -116,71 +119,70 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
116119
* @param end The ending index of the range to find runs.
117120
*/
118121
const findRunsAndSort = (start: number, end: number): void => {
119-
for (let i = start + 1; i <= end; i++) {
120-
const currentElement = arr[i];
121-
let j = i - 1;
122+
for (let currIdx = start + 1; currIdx <= end; currIdx++) {
123+
const currentElement = arr[currIdx]
124+
let prevIdx = currIdx - 1
122125

123-
while (j >= start && compare(arr[j], currentElement) > 0) {
124-
arr[j + 1] = arr[j];
125-
j--;
126+
while (prevIdx >= start && compare(arr[prevIdx], currentElement) > 0) {
127+
arr[prevIdx + 1] = arr[prevIdx]
128+
prevIdx--
126129
}
127-
arr[j + 1] = currentElement;
130+
arr[prevIdx + 1] = currentElement
128131
}
129-
};
132+
}
130133

131134
/**
132135
* Merges runs in the array.
133136
*
134137
* @param minRunLength The minimum length of a run.
135138
*/
136139
const mergeRuns = (minRunLength: number): void => {
137-
for (let size = minRunLength; size < n; size *= 2) {
138-
for (let left = 0; left < n; left += 2 * size) {
139-
const mid = left + size - 1;
140-
const right = Math.min(left + 2 * size - 1, n - 1);
140+
for (let size = minRunLength; size < length; size *= 2) {
141+
for (let left = 0; left < length; left += 2 * size) {
142+
const mid = left + size - 1
143+
const right = Math.min(left + 2 * size - 1, length - 1)
141144

142145
if (mid < right) {
143-
merge(arr, left, mid, right, compare);
146+
merge(arr, left, mid, right, compare)
144147
}
145148
}
146149
}
147-
};
150+
}
148151

149152
/**
150153
* Handles descending runs in the array.
151154
*/
152155
const handleDescendingRuns = (): void => {
153-
let stackSize = 0;
154-
const runStack: [number, number][] = [];
156+
let stackSize = 0
157+
const runStack: [number, number][] = []
155158

156159
// Push runs onto stack
157-
for (let i = 0; i < n; i++) {
158-
let runStart = i;
159-
while (i < n - 1 && compare(arr[i], arr[i + 1]) > 0) {
160-
i++;
160+
for (let idx = 0; idx < length; idx++) {
161+
let runStart = idx
162+
while (idx < length - 1 && compare(arr[idx], arr[idx + 1]) > 0) {
163+
idx++
161164
}
162-
if (runStart !== i) {
163-
runStack.push([runStart, i]);
165+
if (runStart !== idx) {
166+
runStack.push([runStart, idx])
164167
}
165168
}
166169

167170
// Merge descending runs
168171
while (runStack.length > 1) {
169-
const [start1, end1] = runStack.pop()!;
170-
const [start2, end2] = runStack.pop()!;
171-
172-
merge(arr, start2, end2, end1, compare);
173-
runStack.push([start2, end1]);
172+
const [start1, end1] = runStack.pop()!
173+
const [start2, end2] = runStack.pop()!
174+
175+
merge(arr, start2, end2, end1, compare)
176+
runStack.push([start2, end1])
174177
}
175-
};
178+
}
176179

177180
// Find runs and sort them
178-
findRunsAndSort(0, n - 1);
181+
findRunsAndSort(0, length - 1)
179182

180183
// Merge runs
181-
mergeRuns(MIN_MERGE);
184+
mergeRuns(MIN_MERGE)
182185

183186
// Handle descending runs
184-
handleDescendingRuns();
185-
};
186-
187+
handleDescendingRuns()
188+
}

0 commit comments

Comments
 (0)