Skip to content

Commit

Permalink
ref: refactor implementation
Browse files Browse the repository at this point in the history
- calculates the minimum run length for Tim sort based on the length of the array
- add some edge tests
  • Loading branch information
sozelfist committed Jun 17, 2024
1 parent db864bf commit fb84b10
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 126 deletions.
91 changes: 62 additions & 29 deletions sorts/test/tim_sort.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

165 changes: 68 additions & 97 deletions sorts/tim_sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
* a positive value if `a` should come after `b`,
* and zero if `a` and `b` are considered equal.
*/
type Comparator<T> = (a: T, b: T) => number
type Comparator<T> = (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.
Expand All @@ -30,63 +31,57 @@ const merge = <T>(
rightIndex: number,
compare: Comparator<T>
): 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.
Expand All @@ -96,21 +91,7 @@ const merge = <T>(
* @param compare The comparator function defining the order of elements.
*/
export const timSort = <T>(arr: T[], compare: Comparator<T>): 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.
Expand All @@ -120,16 +101,31 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): 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.
Expand All @@ -139,50 +135,25 @@ export const timSort = <T>(arr: T[], compare: Comparator<T>): 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()
}

0 comments on commit fb84b10

Please sign in to comment.