Skip to content

Commit fb84b10

Browse files
committed
ref: refactor implementation
- calculates the minimum run length for Tim sort based on the length of the array - add some edge tests
1 parent db864bf commit fb84b10

File tree

2 files changed

+130
-126
lines changed

2 files changed

+130
-126
lines changed

sorts/test/tim_sort.test.ts

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

33
describe('Tim Sort', () => {
44
const testTimSort = (
55
arr: number[],
66
comparator: (a: number, b: number) => number
77
): void => {
8-
const originalArr = [...arr]
9-
timSort(arr, comparator)
10-
expect(arr).toEqual(originalArr.slice().sort(comparator))
11-
}
8+
const originalArr = [...arr];
9+
timSort(arr, comparator);
10+
expect(arr).toEqual(originalArr.slice().sort(comparator));
11+
};
1212

1313
const testComparator = (
1414
comparator: (a: number, b: number) => number
1515
): void => {
1616
it('should return the sorted array for an empty array', () => {
17-
const arr: number[] = []
18-
testTimSort(arr, comparator)
19-
})
17+
const arr: number[] = [];
18+
testTimSort(arr, comparator);
19+
});
2020

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

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

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

3636
it('should return the sorted array for a large array', () => {
3737
const arr = Array.from({ length: 1000 }, () =>
3838
Math.floor(Math.random() * 1000)
39-
)
40-
testTimSort(arr, comparator)
41-
})
39+
);
40+
testTimSort(arr, comparator);
41+
});
4242

4343
it('should return the sorted array for an array with duplicated elements', () => {
44-
const arr = [5, 3, 8, 1, 7, 3, 6, 4, 5, 8, 2, 1]
45-
testTimSort(arr, comparator)
46-
})
47-
}
44+
const arr = [5, 3, 8, 1, 7, 3, 6, 4, 5, 8, 2, 1];
45+
testTimSort(arr, comparator);
46+
});
47+
48+
it('should return the sorted array for an array with all identical elements', () => {
49+
const arr = Array(1000).fill(5);
50+
testTimSort(arr, comparator);
51+
});
52+
53+
it('should return the reverse sorted array for an array sorted in descending order', () => {
54+
const arr = [5, 4, 3, 2, 1];
55+
testTimSort(arr, comparator);
56+
});
57+
58+
it('should return the pre-sorted array for an array sorted in descending order', () => {
59+
const arr = [1, 2, 3, 4, 5];
60+
testTimSort(arr, comparator);
61+
});
62+
63+
it('should return the sorted array for a very large array', () => {
64+
const arr = Array.from({ length: 100000 }, () =>
65+
Math.floor(Math.random() * 100000)
66+
);
67+
testTimSort(arr, comparator);
68+
});
69+
70+
it('should return the sorted array for an array with negative numbers', () => {
71+
const arr = [5, -3, 8, 1, -7, 0];
72+
testTimSort(arr, comparator);
73+
});
74+
75+
it('should return the sorted array for an array with floating-point numbers', () => {
76+
const arr = [5.1, 3.3, 8.8, 1.2, 7.7];
77+
testTimSort(arr, comparator);
78+
});
79+
};
4880

4981
describe('Sorting in increasing order', () => {
50-
testComparator((a, b) => a - b)
51-
})
82+
testComparator((a, b) => a - b);
83+
});
5284

5385
describe('Sorting in decreasing order', () => {
54-
testComparator((a, b) => b - a)
55-
})
56-
})
86+
testComparator((a, b) => b - a);
87+
});
88+
});
89+

sorts/tim_sort.ts

Lines changed: 68 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
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;
15+
const MIN_GALLOP = 7;
1516

1617
/**
1718
* Merges two sorted subarrays into one sorted array with optimized galloping mode.
@@ -30,63 +31,57 @@ const merge = <T>(
3031
rightIndex: number,
3132
compare: Comparator<T>
3233
): void => {
33-
const leftArrayLength = middleIndex - leftIndex + 1
34-
const rightArrayLength = rightIndex - middleIndex
34+
const leftArrayLength = middleIndex - leftIndex + 1;
35+
const rightArrayLength = rightIndex - middleIndex;
3536

3637
// 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)
38+
const leftSubarray: T[] = arr.slice(leftIndex, middleIndex + 1);
39+
const rightSubarray: T[] = arr.slice(middleIndex + 1, rightIndex + 1);
3940

40-
let leftPointer = 0
41-
let rightPointer = 0
42-
let mergedIndex = leftIndex
41+
let leftPointer = 0;
42+
let rightPointer = 0;
43+
let mergedIndex = leftIndex;
4344

44-
// Regular merge with galloping mode
45+
// Merge the two subarrays back into the main array
4546
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++
47+
if (compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0) {
48+
arr[mergedIndex++] = leftSubarray[leftPointer++];
49+
} else {
50+
arr[mergedIndex++] = rightSubarray[rightPointer++];
5651
}
5752

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++
66-
}
67-
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++]
53+
// Implement galloping mode
54+
let numGallops = 0;
55+
while (leftPointer < leftArrayLength && rightPointer < rightArrayLength && numGallops < MIN_GALLOP) {
56+
if (compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0) {
57+
arr[mergedIndex++] = leftSubarray[leftPointer++];
7458
} else {
75-
arr[mergedIndex++] = rightSubarray[rightPointer++]
59+
arr[mergedIndex++] = rightSubarray[rightPointer++];
7660
}
61+
numGallops++;
62+
}
63+
64+
// Gallop left
65+
while (leftPointer < leftArrayLength && compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0) {
66+
arr[mergedIndex++] = leftSubarray[leftPointer++];
67+
}
68+
69+
// Gallop right
70+
while (rightPointer < rightArrayLength && compare(rightSubarray[rightPointer], leftSubarray[leftPointer]) < 0) {
71+
arr[mergedIndex++] = rightSubarray[rightPointer++];
7772
}
7873
}
7974

8075
// Copy remaining elements from left subarray, if any
8176
while (leftPointer < leftArrayLength) {
82-
arr[mergedIndex++] = leftSubarray[leftPointer++]
77+
arr[mergedIndex++] = leftSubarray[leftPointer++];
8378
}
8479

8580
// Copy remaining elements from right subarray, if any
8681
while (rightPointer < rightArrayLength) {
87-
arr[mergedIndex++] = rightSubarray[rightPointer++]
82+
arr[mergedIndex++] = rightSubarray[rightPointer++];
8883
}
89-
}
84+
};
9085

9186
/**
9287
* Sorts an array using the Tim sort algorithm.
@@ -96,21 +91,7 @@ const merge = <T>(
9691
* @param compare The comparator function defining the order of elements.
9792
*/
9893
export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
99-
const length = arr.length
100-
101-
/**
102-
* Reverses a portion of the array.
103-
*
104-
* @param start The starting index of the portion to reverse.
105-
* @param end The ending index of the portion to reverse.
106-
*/
107-
const reverseRange = (start: number, end: number): void => {
108-
while (start < end) {
109-
const temp = arr[start]
110-
arr[start++] = arr[end]
111-
arr[end--] = temp
112-
}
113-
}
94+
const length = arr.length;
11495

11596
/**
11697
* Identifies runs and sorts them using insertion sort.
@@ -120,16 +101,31 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
120101
*/
121102
const findRunsAndSort = (start: number, end: number): void => {
122103
for (let currIdx = start + 1; currIdx <= end; currIdx++) {
123-
const currentElement = arr[currIdx]
124-
let prevIdx = currIdx - 1
104+
const currentElement = arr[currIdx];
105+
let prevIdx = currIdx - 1;
125106

126107
while (prevIdx >= start && compare(arr[prevIdx], currentElement) > 0) {
127-
arr[prevIdx + 1] = arr[prevIdx]
128-
prevIdx--
108+
arr[prevIdx + 1] = arr[prevIdx];
109+
prevIdx--;
129110
}
130-
arr[prevIdx + 1] = currentElement
111+
arr[prevIdx + 1] = currentElement;
131112
}
132-
}
113+
};
114+
115+
/**
116+
* Calculates the minimum run length.
117+
*
118+
* @param n The length of the array.
119+
* @returns The minimum run length.
120+
*/
121+
const minRunLength = (n: number): number => {
122+
let r = 0;
123+
while (n >= MIN_MERGE) {
124+
r |= n & 1;
125+
n >>= 1;
126+
}
127+
return n + r;
128+
};
133129

134130
/**
135131
* Merges runs in the array.
@@ -139,50 +135,25 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): void => {
139135
const mergeRuns = (minRunLength: number): void => {
140136
for (let size = minRunLength; size < length; size *= 2) {
141137
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)
138+
const mid = Math.min(left + size - 1, length - 1);
139+
const right = Math.min(left + 2 * size - 1, length - 1);
144140

145141
if (mid < right) {
146-
merge(arr, left, mid, right, compare)
142+
merge(arr, left, mid, right, compare);
147143
}
148144
}
149145
}
150-
}
151-
152-
/**
153-
* Handles descending runs in the array.
154-
*/
155-
const handleDescendingRuns = (): void => {
156-
let stackSize = 0
157-
const runStack: [number, number][] = []
158-
159-
// Push runs onto stack
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++
164-
}
165-
if (runStart !== idx) {
166-
runStack.push([runStart, idx])
167-
}
168-
}
146+
};
169147

170-
// Merge descending runs
171-
while (runStack.length > 1) {
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])
177-
}
178-
}
148+
// Determine the minimum run length
149+
const minRun = minRunLength(length);
179150

180151
// Find runs and sort them
181-
findRunsAndSort(0, length - 1)
152+
for (let i = 0; i < length; i += minRun) {
153+
findRunsAndSort(i, Math.min(i + minRun - 1, length - 1));
154+
}
182155

183156
// Merge runs
184-
mergeRuns(MIN_MERGE)
157+
mergeRuns(minRun);
158+
};
185159

186-
// Handle descending runs
187-
handleDescendingRuns()
188-
}

0 commit comments

Comments
 (0)