diff --git a/sorts/test/tim_sort.test.ts b/sorts/test/tim_sort.test.ts index 3ff34a2b..d7aef45b 100644 --- a/sorts/test/tim_sort.test.ts +++ b/sorts/test/tim_sort.test.ts @@ -1,56 +1,89 @@ -import { timSort } from '../tim_sort' +import { timSort } from '../tim_sort'; describe('Tim Sort', () => { const testTimSort = ( arr: number[], comparator: (a: number, b: number) => number ): void => { - const originalArr = [...arr] - timSort(arr, comparator) - expect(arr).toEqual(originalArr.slice().sort(comparator)) - } + const originalArr = [...arr]; + timSort(arr, comparator); + expect(arr).toEqual(originalArr.slice().sort(comparator)); + }; const testComparator = ( comparator: (a: number, b: number) => number ): void => { it('should return the sorted array for an empty array', () => { - const arr: number[] = [] - testTimSort(arr, comparator) - }) + const arr: number[] = []; + testTimSort(arr, comparator); + }); it('should return the sorted array for an array with one element', () => { - const arr: number[] = [1] - testTimSort(arr, comparator) - }) + const arr: number[] = [1]; + testTimSort(arr, comparator); + }); it('should return the sorted array for a small array', () => { - const arr = [5, 3, 8, 1, 7] - testTimSort(arr, comparator) - }) + const arr = [5, 3, 8, 1, 7]; + testTimSort(arr, comparator); + }); it('should return the sorted array for a medium array', () => { - const arr = [1, 4, 2, 5, 9, 6, 3, 8, 10, 7, 12, 11] - testTimSort(arr, comparator) - }) + const arr = [1, 4, 2, 5, 9, 6, 3, 8, 10, 7, 12, 11]; + testTimSort(arr, comparator); + }); it('should return the sorted array for a large array', () => { const arr = Array.from({ length: 1000 }, () => Math.floor(Math.random() * 1000) - ) - testTimSort(arr, comparator) - }) + ); + testTimSort(arr, comparator); + }); it('should return the sorted array for an array with duplicated elements', () => { - const arr = [5, 3, 8, 1, 7, 3, 6, 4, 5, 8, 2, 1] - testTimSort(arr, comparator) - }) - } + const arr = [5, 3, 8, 1, 7, 3, 6, 4, 5, 8, 2, 1]; + testTimSort(arr, comparator); + }); + + it('should return the sorted array for an array with all identical elements', () => { + const arr = Array(1000).fill(5); + testTimSort(arr, comparator); + }); + + it('should return the reverse sorted array for an array sorted in descending order', () => { + const arr = [5, 4, 3, 2, 1]; + testTimSort(arr, comparator); + }); + + it('should return the pre-sorted array for an array sorted in descending order', () => { + const arr = [1, 2, 3, 4, 5]; + testTimSort(arr, comparator); + }); + + it('should return the sorted array for a very large array', () => { + const arr = Array.from({ length: 100000 }, () => + Math.floor(Math.random() * 100000) + ); + testTimSort(arr, comparator); + }); + + it('should return the sorted array for an array with negative numbers', () => { + const arr = [5, -3, 8, 1, -7, 0]; + testTimSort(arr, comparator); + }); + + it('should return the sorted array for an array with floating-point numbers', () => { + const arr = [5.1, 3.3, 8.8, 1.2, 7.7]; + testTimSort(arr, comparator); + }); + }; describe('Sorting in increasing order', () => { - testComparator((a, b) => a - b) - }) + testComparator((a, b) => a - b); + }); describe('Sorting in decreasing order', () => { - testComparator((a, b) => b - a) - }) -}) + testComparator((a, b) => b - a); + }); +}); + diff --git a/sorts/tim_sort.ts b/sorts/tim_sort.ts index 771fcf2d..1b91c952 100644 --- a/sorts/tim_sort.ts +++ b/sorts/tim_sort.ts @@ -8,10 +8,11 @@ * a positive value if `a` should come after `b`, * and zero if `a` and `b` are considered equal. */ -type Comparator = (a: T, b: T) => number +type Comparator = (a: T, b: T) => number; // Minimum size of subarrays to be sorted using insertion sort before merging -const MIN_MERGE = 32 +const MIN_MERGE = 32; +const MIN_GALLOP = 7; /** * Merges two sorted subarrays into one sorted array with optimized galloping mode. @@ -30,63 +31,57 @@ const merge = ( rightIndex: number, compare: Comparator ): void => { - const leftArrayLength = middleIndex - leftIndex + 1 - const rightArrayLength = rightIndex - middleIndex + const leftArrayLength = middleIndex - leftIndex + 1; + const rightArrayLength = rightIndex - middleIndex; // Create temporary arrays for the left and right subarrays - const leftSubarray: T[] = arr.slice(leftIndex, middleIndex + 1) - const rightSubarray: T[] = arr.slice(middleIndex + 1, rightIndex + 1) + const leftSubarray: T[] = arr.slice(leftIndex, middleIndex + 1); + const rightSubarray: T[] = arr.slice(middleIndex + 1, rightIndex + 1); - let leftPointer = 0 - let rightPointer = 0 - let mergedIndex = leftIndex + let leftPointer = 0; + let rightPointer = 0; + let mergedIndex = leftIndex; - // Regular merge with galloping mode + // Merge the two subarrays back into the main array while (leftPointer < leftArrayLength && rightPointer < rightArrayLength) { - let numGallops = 0 - - // Galloping through the left subarray - while ( - leftPointer < leftArrayLength && - numGallops < MIN_MERGE && - compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0 - ) { - arr[mergedIndex++] = leftSubarray[leftPointer++] - numGallops++ + if (compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0) { + arr[mergedIndex++] = leftSubarray[leftPointer++]; + } else { + arr[mergedIndex++] = rightSubarray[rightPointer++]; } - // Galloping through the right subarray - while ( - rightPointer < rightArrayLength && - numGallops < MIN_MERGE && - compare(rightSubarray[rightPointer], leftSubarray[leftPointer]) < 0 - ) { - arr[mergedIndex++] = rightSubarray[rightPointer++] - numGallops++ - } - - // Standard merge without galloping - while (leftPointer < leftArrayLength && rightPointer < rightArrayLength) { - if ( - compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0 - ) { - arr[mergedIndex++] = leftSubarray[leftPointer++] + // Implement galloping mode + let numGallops = 0; + while (leftPointer < leftArrayLength && rightPointer < rightArrayLength && numGallops < MIN_GALLOP) { + if (compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0) { + arr[mergedIndex++] = leftSubarray[leftPointer++]; } else { - arr[mergedIndex++] = rightSubarray[rightPointer++] + arr[mergedIndex++] = rightSubarray[rightPointer++]; } + numGallops++; + } + + // Gallop left + while (leftPointer < leftArrayLength && compare(leftSubarray[leftPointer], rightSubarray[rightPointer]) <= 0) { + arr[mergedIndex++] = leftSubarray[leftPointer++]; + } + + // Gallop right + while (rightPointer < rightArrayLength && compare(rightSubarray[rightPointer], leftSubarray[leftPointer]) < 0) { + arr[mergedIndex++] = rightSubarray[rightPointer++]; } } // Copy remaining elements from left subarray, if any while (leftPointer < leftArrayLength) { - arr[mergedIndex++] = leftSubarray[leftPointer++] + arr[mergedIndex++] = leftSubarray[leftPointer++]; } // Copy remaining elements from right subarray, if any while (rightPointer < rightArrayLength) { - arr[mergedIndex++] = rightSubarray[rightPointer++] + arr[mergedIndex++] = rightSubarray[rightPointer++]; } -} +}; /** * Sorts an array using the Tim sort algorithm. @@ -96,21 +91,7 @@ const merge = ( * @param compare The comparator function defining the order of elements. */ export const timSort = (arr: T[], compare: Comparator): void => { - const length = arr.length - - /** - * Reverses a portion of the array. - * - * @param start The starting index of the portion to reverse. - * @param end The ending index of the portion to reverse. - */ - const reverseRange = (start: number, end: number): void => { - while (start < end) { - const temp = arr[start] - arr[start++] = arr[end] - arr[end--] = temp - } - } + const length = arr.length; /** * Identifies runs and sorts them using insertion sort. @@ -120,16 +101,31 @@ export const timSort = (arr: T[], compare: Comparator): void => { */ const findRunsAndSort = (start: number, end: number): void => { for (let currIdx = start + 1; currIdx <= end; currIdx++) { - const currentElement = arr[currIdx] - let prevIdx = currIdx - 1 + const currentElement = arr[currIdx]; + let prevIdx = currIdx - 1; while (prevIdx >= start && compare(arr[prevIdx], currentElement) > 0) { - arr[prevIdx + 1] = arr[prevIdx] - prevIdx-- + arr[prevIdx + 1] = arr[prevIdx]; + prevIdx--; } - arr[prevIdx + 1] = currentElement + arr[prevIdx + 1] = currentElement; } - } + }; + + /** + * Calculates the minimum run length. + * + * @param n The length of the array. + * @returns The minimum run length. + */ + const minRunLength = (n: number): number => { + let r = 0; + while (n >= MIN_MERGE) { + r |= n & 1; + n >>= 1; + } + return n + r; + }; /** * Merges runs in the array. @@ -139,50 +135,25 @@ export const timSort = (arr: T[], compare: Comparator): void => { const mergeRuns = (minRunLength: number): void => { for (let size = minRunLength; size < length; size *= 2) { for (let left = 0; left < length; left += 2 * size) { - const mid = left + size - 1 - const right = Math.min(left + 2 * size - 1, length - 1) + const mid = Math.min(left + size - 1, length - 1); + const right = Math.min(left + 2 * size - 1, length - 1); if (mid < right) { - merge(arr, left, mid, right, compare) + merge(arr, left, mid, right, compare); } } } - } - - /** - * Handles descending runs in the array. - */ - const handleDescendingRuns = (): void => { - let stackSize = 0 - const runStack: [number, number][] = [] - - // Push runs onto stack - for (let idx = 0; idx < length; idx++) { - let runStart = idx - while (idx < length - 1 && compare(arr[idx], arr[idx + 1]) > 0) { - idx++ - } - if (runStart !== idx) { - runStack.push([runStart, idx]) - } - } + }; - // Merge descending runs - while (runStack.length > 1) { - const [start1, end1] = runStack.pop()! - const [start2, end2] = runStack.pop()! - - merge(arr, start2, end2, end1, compare) - runStack.push([start2, end1]) - } - } + // Determine the minimum run length + const minRun = minRunLength(length); // Find runs and sort them - findRunsAndSort(0, length - 1) + for (let i = 0; i < length; i += minRun) { + findRunsAndSort(i, Math.min(i + minRun - 1, length - 1)); + } // Merge runs - mergeRuns(MIN_MERGE) + mergeRuns(minRun); +}; - // Handle descending runs - handleDescendingRuns() -}