diff --git a/algorithms/dp/coinChange/coinChange.test.ts b/algorithms/dp/coinChange/coinChange.test.ts new file mode 100644 index 0000000..1fcf524 --- /dev/null +++ b/algorithms/dp/coinChange/coinChange.test.ts @@ -0,0 +1,40 @@ +import {coinChangeWays, minCoinsForChange} from './coinChange'; + +describe('coinChangeWays tests', () => { + test('should return 4 ways for [1, 2, 5] and target 5', () => { + const ways = coinChangeWays([1, 2, 5], 5); + expect(ways).toBe(4); + }); + test('should return 1 when target is 0', () => { + expect(coinChangeWays([1, 2, 3], 0)).toBe(1); + }); + + test('should return 0 when cannot make change', () => { + expect(coinChangeWays([3, 4], 1)).toBe(0); + }); + test('should throw error for negative target', () => { + expect(() => coinChangeWays([1, 2, 3], -5)).toThrow(); + }); + test('should throw error for non-positive coin', () => { + expect(() => coinChangeWays([0, 2], 10)).toThrow(); + }); +}); + +describe('minCoinsForChange tests', () => { + test('should return 2 for [1, 3, 4] and target 6', () => { + const minCoins = minCoinsForChange([1, 3, 4], 6); + expect(minCoins).toBe(2); + }); + test('should return 0 when target is 0', () => { + expect(minCoinsForChange([1, 5], 0)).toBe(0); + }); + test('should return -1 when cannot make change', () => { + expect(minCoinsForChange([2, 4], 3)).toBe(-1); + }); + test('should throw error for negative target', () => { + expect(() => minCoinsForChange([1], -1)).toThrow(); + }); + test('should throw error for non-positive coin', () => { + expect(() => minCoinsForChange([0], 10)).toThrow(); + }); +}); diff --git a/algorithms/dp/coinChange/coinChange.ts b/algorithms/dp/coinChange/coinChange.ts new file mode 100644 index 0000000..f60d4fc --- /dev/null +++ b/algorithms/dp/coinChange/coinChange.ts @@ -0,0 +1,64 @@ +/** + * Calculates the number of ways to make change for a given amount using given coin denominations. + * Each coin denomination can be used unlimited times. + * + * @param {number[]} coins - An array of coin denominations. + * @param {number} target - The target amount. + * @returns {number} The total number of distinct ways to make up the target amount. + */ +export function coinChangeWays(coins: number[], target: number): number { + if (target < 0) { + throw new Error('Target amount must be non-negative.'); + } + for (const coin of coins) { + if (coin <= 0) { + throw new Error('Coin denominations must be positive.'); + } + } + // dp[x] = number of ways to make amount x + const dp = Array(target + 1).fill(0); + dp[0] = 1; // Base case: There's 1 way to make 0 amount: use no coins + + for (const coin of coins) { + for (let amount = coin; amount <= target; amount++) { + dp[amount] += dp[amount - coin]; + } + } + return dp[target]; +} + +/** + * Calculates the minimum number of coins needed to make change for a given amount. + * Each coin denomination can be used unlimited times. + * + * @param {number[]} coins - An array of coin denominations. + * @param {number} target - The target amount. + * @returns {number} The minimum number of coins needed to reach the target. + * If the target cannot be reached with the given denominations, returns -1. + */ +export function minCoinsForChange(coins: number[], target: number): number { + if (target < 0) { + throw new Error('Target amount must be non-negative.'); + } + for (const coin of coins) { + if (coin <= 0) { + throw new Error('Coin denominations must be positive.'); + } + } + // dp[x] = minimum number of coins to make amount x + const dp = Array(target + 1).fill(Infinity); + dp[0] = 0; + + for (const amount of dp.keys()) { + // If dp[amount] is already Infinity, skip + if (dp[amount] === Infinity) continue; + // Try all coins + for (const coin of coins) { + const nextAmount = amount + coin; + if (nextAmount <= target) { + dp[nextAmount] = Math.min(dp[nextAmount], dp[amount] + 1); + } + } + } + return dp[target] === Infinity ? -1 : dp[target]; +} diff --git a/algorithms/dp/knapsack.test.ts b/algorithms/dp/knapsack/knapsack.test.ts similarity index 100% rename from algorithms/dp/knapsack.test.ts rename to algorithms/dp/knapsack/knapsack.test.ts diff --git a/algorithms/dp/knapsack.ts b/algorithms/dp/knapsack/knapsack.ts similarity index 100% rename from algorithms/dp/knapsack.ts rename to algorithms/dp/knapsack/knapsack.ts diff --git a/algorithms/dp/lcs/lcs.test.ts b/algorithms/dp/lcs/lcs.test.ts new file mode 100644 index 0000000..d5529fd --- /dev/null +++ b/algorithms/dp/lcs/lcs.test.ts @@ -0,0 +1,30 @@ +import {lcs} from './lcs'; + +describe('LCS function test', () => { + test('should return correct result for normal strings', () => { + const {length, sequence} = lcs('ABCBDAB', 'BDCABA'); + expect(length).toBe(4); + expect(sequence.length).toBe(4); + expect(['BDAB', 'BCAB']).toContain(sequence); + }); + test('should return 0 for empty input', () => { + const {length, sequence} = lcs('', 'ABCDE'); + expect(length).toBe(0); + expect(sequence).toBe(''); + }); + test('should return full length if strings are identical', () => { + const {length, sequence} = lcs('HELLO', 'HELLO'); + expect(length).toBe(5); + expect(sequence).toBe('HELLO'); + }); + test('should return correct result for partially matching strings', () => { + const {length, sequence} = lcs('ABC', 'ACE'); + expect(length).toBe(2); + expect(sequence).toBe('AC'); + }); + test('should handle no common characters', () => { + const {length, sequence} = lcs('ABC', 'XYZ'); + expect(length).toBe(0); + expect(sequence).toBe(''); + }); +}); diff --git a/algorithms/dp/lcs/lcs.ts b/algorithms/dp/lcs/lcs.ts new file mode 100644 index 0000000..979841a --- /dev/null +++ b/algorithms/dp/lcs/lcs.ts @@ -0,0 +1,58 @@ +/** + * Object that holds the results of LCS. + */ +interface LCSResult { + length: number; + sequence: string; +} + +/** + * Finds the longest common substring (LCS) of two strings. + * @param strA - First string + * @param strB - Second string + * @returns An object containing the length of the LCS and the actual common substring + */ +export function lcs(strA: string, strB: string): LCSResult { + const lenA = strA.length; + const lenB = strB.length; + + // Prepare the DP table (length + 1). + // dp[i][j] = Length of the LCS when considering the first i characters of strA and the first j characters of strB. + const dp: number[][] = Array.from({length: lenA + 1}, () => Array(lenB + 1).fill(0)); + + // Calculates LCS length. + for (let i = 1; i <= lenA; i++) { + for (let j = 1; j <= lenB; j++) { + if (strA[i - 1] === strB[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + // Restore the actual sequence of LCS. + let sequence = ''; + let i = lenA; + let j = lenB; + while (i > 0 && j > 0) { + if (strA[i - 1] === strB[j - 1]) { + // Add to the sequence as a common character (note that it is added in reverse order). + sequence = strA[i - 1] + sequence; + i--; + j--; + } else { + // DP Move the table towards the larger side. + if (dp[i - 1][j] > dp[i][j - 1]) { + i--; + } else { + j--; + } + } + } + + return { + length: dp[lenA][lenB], + sequence, + }; +} diff --git a/algorithms/search/binary-search/binarySearch.test.ts b/algorithms/search/binary-search/binarySearch.test.ts index 85d3e2f..1dcd7e0 100644 --- a/algorithms/search/binary-search/binarySearch.test.ts +++ b/algorithms/search/binary-search/binarySearch.test.ts @@ -1,11 +1,38 @@ import {binarySearch} from './binarySearch'; -describe('binary-search', () => { - const colors = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - - it('normal', () => { - expect(binarySearch(colors, 1)).toEqual(0); - expect(binarySearch(colors, 8)).toEqual(7); - expect(binarySearch(colors, 17)).toEqual(undefined); +describe('binarySearch', () => { + describe('numeric array tests', () => { + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + test('should find the first element', () => { + expect(binarySearch(numbers, 1)).toBe(0); + }); + test('should find an element in the middle', () => { + expect(binarySearch(numbers, 5)).toBe(4); + }); + test('should find the last element', () => { + expect(binarySearch(numbers, 10)).toBe(9); + }); + test('should return undefined if the value is not found', () => { + expect(binarySearch(numbers, 99)).toBeUndefined(); + }); + test('should work with an empty array', () => { + expect(binarySearch([], 1)).toBeUndefined(); + }); + test('should work with a single-element array (value found)', () => { + expect(binarySearch([42], 42)).toBe(0); + }); + test('should work with a single-element array (value not found)', () => { + expect(binarySearch([42], 24)).toBeUndefined(); + }); + }); + describe('string array with custom comparator', () => { + const words = ['apple', 'banana', 'cherry', 'date', 'eggplant']; + const comparator = (a: string, b: string): number => a.localeCompare(b); + test('should find a word in a string array', () => { + expect(binarySearch(words, 'cherry', comparator)).toBe(2); + }); + test('should return undefined if a string is not in the array', () => { + expect(binarySearch(words, 'coconut', comparator)).toBeUndefined(); + }); }); }); diff --git a/algorithms/search/binary-search/binarySearch.ts b/algorithms/search/binary-search/binarySearch.ts index ed06867..993b98e 100644 --- a/algorithms/search/binary-search/binarySearch.ts +++ b/algorithms/search/binary-search/binarySearch.ts @@ -1,13 +1,37 @@ -export function binarySearch(arr: T[], value: T): number | undefined { +/** + * Performs a binary search on a sorted array and returns the index of the specified value. + * If the value is not found, the function returns `undefined`. + * + * @typeParam T - The type of elements in the array. + * @param arr - A sorted array in which to perform the search. + * @param value - The value to search for. + * @param comparator - Optional comparison function. If not provided, the function attempts to compare elements using the built-in `<` and `>` operators. + * @returns The index of the found element, or `undefined` if the element is not found. + */ +export function binarySearch( + arr: T[], + value: T, + comparator?: (a: T, b: T) => number, +): number | undefined { let low = 0; let high = arr.length - 1; + + // Default comparator for types that can be compared with < and > + const defaultComparator = (a: T, b: T): 0 | 1 | -1 => { + if (a < b) return -1; + if (a > b) return 1; + return 0; + }; + + const compare = comparator ?? defaultComparator; + while (low <= high) { const mid = Math.floor((low + high) / 2); const element = arr[mid]; - if (element === value) { + const cmp = compare(element, value); + if (cmp === 0) { return mid; - } - if (element > value) { + } else if (cmp > 0) { high = mid - 1; } else { low = mid + 1; diff --git a/algorithms/search/interpolation-search/interpolationSearch.test.ts b/algorithms/search/interpolation-search/interpolationSearch.test.ts index 16ea1f0..6be3962 100644 --- a/algorithms/search/interpolation-search/interpolationSearch.test.ts +++ b/algorithms/search/interpolation-search/interpolationSearch.test.ts @@ -1,11 +1,35 @@ import {interpolationSearch} from './interpolationSearch'; -describe('interpolation-search', () => { +describe('interpolationSearch', () => { const arr = [1, 4, 6, 7, 9, 12, 15, 16, 17, 23, 25, 26, 27, 31]; - - it('normal', () => { + test('should find the element when it exists in the array', () => { expect(interpolationSearch(arr, 9)).toEqual(4); expect(interpolationSearch(arr, 31)).toEqual(13); + }); + test('should return undefined when the element does not exist', () => { expect(interpolationSearch(arr, 42)).toEqual(undefined); }); + test('should return undefined for an empty array', () => { + expect(interpolationSearch([], 10)).toBeUndefined(); + }); + test('should handle a single-element array (value found)', () => { + expect(interpolationSearch([7], 7)).toBe(0); + }); + test('should return undefined for a single-element array (value not found)', () => { + expect(interpolationSearch([7], 9)).toBeUndefined(); + }); + test('should handle an array where all elements are the same (value found)', () => { + const sameArr = [5, 5, 5, 5, 5]; + expect(interpolationSearch(sameArr, 5)).toBe(0); + }); + test('should return undefined for an array where all elements are the same (value not found)', () => { + const sameArr = [5, 5, 5, 5, 5]; + expect(interpolationSearch(sameArr, 7)).toBeUndefined(); + }); + test('should find the first element', () => { + expect(interpolationSearch(arr, 1)).toBe(0); + }); + test('should find the last element', () => { + expect(interpolationSearch(arr, 31)).toBe(arr.length - 1); + }); }); diff --git a/algorithms/search/interpolation-search/interpolationSearch.ts b/algorithms/search/interpolation-search/interpolationSearch.ts index 24f08c2..c92f81c 100644 --- a/algorithms/search/interpolation-search/interpolationSearch.ts +++ b/algorithms/search/interpolation-search/interpolationSearch.ts @@ -1,16 +1,36 @@ +/** + * Performs an interpolation search on a sorted numeric array and returns the index of the key. + * If the key is not found, the function returns `undefined`. + * + * @param arr - A sorted array of numbers. + * @param key - The value to search for. + * @returns The index of the found value, or `undefined` if not found. + * + * @remarks + ** Interpolation search is generally faster than a standard binary search for uniformly + ** distributed data, but can degrade to O(n) in the worst case. + */ export function interpolationSearch(arr: number[], key: number): number | undefined { + if (arr.length === 0) return undefined; + let low = 0; let high = arr.length - 1; + while (low <= high && key >= arr[low] && key <= arr[high]) { - const pos = low + ((high - low) / (arr[high] - arr[low])) * (key - arr[low]); - if (arr[Math.floor(pos)] === key) { - return Math.floor(pos); + // Avoiding cases where the denominator becomes 0. + if (arr[high] === arr[low]) { + return arr[low] === key ? low : undefined; } - if (arr[Math.floor(pos)] < key) { - low = Math.floor(pos) + 1; + const pos = low + Math.floor(((high - low) / (arr[high] - arr[low])) * (key - arr[low])); + if (arr[pos] === key) { + return pos; + } + if (arr[pos] < key) { + low = pos + 1; } else { - high = Math.floor(pos) - 1; + high = pos - 1; } } + return undefined; } diff --git a/algorithms/search/jump-search/jumpSearch.test.ts b/algorithms/search/jump-search/jumpSearch.test.ts index f4ddcea..1a31f3d 100644 --- a/algorithms/search/jump-search/jumpSearch.test.ts +++ b/algorithms/search/jump-search/jumpSearch.test.ts @@ -1,11 +1,30 @@ import {jumpSearch} from './jumpSearch'; -describe('jump-search', () => { +describe('jumpSearch', () => { const arr = [0, 2, 4, 7, 10, 23, 34, 40, 55, 68, 77, 90, 110]; - - it('normal', () => { - expect(jumpSearch(arr, 2)).toEqual(1); - expect(jumpSearch(arr, 55)).toEqual(8); - expect(jumpSearch(arr, 100)).toEqual(undefined); + test('should find the element when it exists in the array', () => { + expect(jumpSearch(arr, 2)).toBe(1); + expect(jumpSearch(arr, 55)).toBe(8); + }); + test('should return undefined if the element does not exist', () => { + expect(jumpSearch(arr, 100)).toBeUndefined(); + }); + test('should handle an empty array', () => { + expect(jumpSearch([], 10)).toBeUndefined(); + }); + test('should handle an array with one element (value found)', () => { + expect(jumpSearch([42], 42)).toBe(0); + }); + test('should return undefined for an array with one element (value not found)', () => { + expect(jumpSearch([42], 24)).toBeUndefined(); + }); + test('should find the first element in the array', () => { + expect(jumpSearch(arr, 0)).toBe(0); + }); + test('should find the last element in the array', () => { + expect(jumpSearch(arr, 110)).toBe(arr.length - 1); + }); + test('should return undefined if step overshoots the array length', () => { + expect(jumpSearch(arr, 999)).toBeUndefined(); }); }); diff --git a/algorithms/search/jump-search/jumpSearch.ts b/algorithms/search/jump-search/jumpSearch.ts index e16ab11..c288e76 100644 --- a/algorithms/search/jump-search/jumpSearch.ts +++ b/algorithms/search/jump-search/jumpSearch.ts @@ -1,22 +1,38 @@ +/** + * Performs a jump search on a sorted numeric array and returns the index of the specified value. + * If the value is not found, the function returns `undefined`. + * + * @param arr - A sorted array of numbers. + * @param x - The value to search for. + * @returns The index of the found value, or `undefined` if the value is not found. + * + * @remarks + ** Jump search is an algorithm that allows searching in O(√n) time by jumping in fixed-size steps, + ** then doing a linear search within the block where the value could exist. + */ export function jumpSearch(arr: number[], x: number): number | undefined { const n = arr.length; - let step = Math.floor(Math.sqrt(n)); + if (n === 0) return undefined; + + // Ensure step is at least 1 to avoid infinite loops for very small n + let step = Math.max(1, Math.floor(Math.sqrt(n))); let prev = 0; + + // Jump forward in blocks until we overshoot or find a block that might contain 'x' while (arr[Math.min(step, n) - 1] < x) { prev = step; - step += Math.floor(Math.sqrt(n)); + step += Math.max(1, Math.floor(Math.sqrt(n))); // step should never be 0 if (prev >= n) { return undefined; } } + // Linear search within the identified block while (arr[prev] < x) { prev++; if (prev === Math.min(step, n)) { return undefined; } } - if (arr[prev] === x) { - return prev; - } - return undefined; + // Check if we found the value + return arr[prev] === x ? prev : undefined; } diff --git a/algorithms/search/linear-search/linearSearch.test.ts b/algorithms/search/linear-search/linearSearch.test.ts index e91de17..dac9a37 100644 --- a/algorithms/search/linear-search/linearSearch.test.ts +++ b/algorithms/search/linear-search/linearSearch.test.ts @@ -1,11 +1,29 @@ import {linearSearch} from './linearSearch'; -describe('linear-search', () => { +describe('linearSearch', () => { const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']; - - it('normal', () => { - expect(linearSearch(colors, 'red')).toEqual(0); - expect(linearSearch(colors, 'violet')).toEqual(6); - expect(linearSearch(colors, 'rainbow')).toEqual(undefined); + test('should find the first element', () => { + expect(linearSearch(colors, 'red')).toBe(0); + }); + test('should find the last element', () => { + expect(linearSearch(colors, 'violet')).toBe(6); + }); + test('should return undefined when element is not found', () => { + expect(linearSearch(colors, 'rainbow')).toBeUndefined(); + }); + test('should handle an empty array', () => { + expect(linearSearch([], 'anything')).toBeUndefined(); + }); + test('should handle a single-element array (value found)', () => { + expect(linearSearch(['blue'], 'blue')).toBe(0); + }); + test('should return undefined in a single-element array (value not found)', () => { + expect(linearSearch(['blue'], 'red')).toBeUndefined(); + }); + test('should allow a custom comparator for object arrays', () => { + const objects = [{id: 1}, {id: 2}, {id: 3}]; + const comparator = (a: {id: number}, b: {id: number}): boolean => a.id === b.id; + expect(linearSearch(objects, {id: 3}, comparator)).toBe(2); + expect(linearSearch(objects, {id: 999}, comparator)).toBeUndefined(); }); }); diff --git a/algorithms/search/linear-search/linearSearch.ts b/algorithms/search/linear-search/linearSearch.ts index 0502e46..d2068fd 100644 --- a/algorithms/search/linear-search/linearSearch.ts +++ b/algorithms/search/linear-search/linearSearch.ts @@ -1,6 +1,22 @@ -export function linearSearch(arr: T[], value: T): number | undefined { +/** + * Performs a linear search on the given array. If the value is found, it returns the index; + * otherwise, it returns `undefined`. + * + * @typeParam T - The type of elements in the array. + * @param arr - The array in which to search. + * @param value - The value to search for. + * @param comparator - An optional comparison function. If not provided, the function compares using `===`. + * @returns The index of the found element or `undefined` if not found. + */ +export function linearSearch( + arr: T[], + value: T, + comparator?: (a: T, b: T) => boolean, +): number | undefined { + const defaultComparator = (a: T, b: T): boolean => a === b; + const compare = comparator ?? defaultComparator; for (let i = 0; i < arr.length; i++) { - if (arr[i] === value) { + if (compare(arr[i], value)) { return i; } } diff --git a/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.test.ts b/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.test.ts index 6b813d0..277273c 100644 --- a/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.test.ts +++ b/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.test.ts @@ -1,42 +1,39 @@ import {bubbleSortCountersBasic, bubbleSortCounters} from './bubbleSortCounters'; -describe('bubble-sort-counters', () => { - const randomArray = [9, 2, 5, 6, 4, 3, 7, 10, 1, 8]; - const orderedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - const reversedArray = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; - - it('basic', () => { - expect(bubbleSortCountersBasic(randomArray.slice())).toEqual({ - countOuter: 10, - countInner: 90, - countSwap: 21, - }); - expect(bubbleSortCountersBasic(orderedArray.slice())).toEqual({ - countOuter: 10, - countInner: 90, +describe('bubble-sort-counters-additional', () => { + test('should handle empty array', () => { + expect(bubbleSortCountersBasic([])).toEqual({ + countOuter: 0, + countInner: 0, countSwap: 0, }); - expect(bubbleSortCountersBasic(reversedArray.slice())).toEqual({ - countOuter: 10, - countInner: 90, - countSwap: 45, + expect(bubbleSortCounters([])).toEqual({ + countOuter: 1, + countInner: 0, + countSwap: 0, }); }); - it('normal', () => { - expect(bubbleSortCounters(randomArray.slice())).toEqual({ - countOuter: 9, - countInner: 90, - countSwap: 21, - }); - expect(bubbleSortCounters(orderedArray.slice())).toEqual({ + test('should handle single-element array', () => { + const singleArray = [42]; + const resultBasic = bubbleSortCountersBasic(singleArray.slice()); + const resultOptimized = bubbleSortCounters(singleArray.slice()); + expect(resultBasic).toEqual({ countOuter: 1, - countInner: 10, + countInner: 0, countSwap: 0, }); - expect(bubbleSortCounters(reversedArray.slice())).toEqual({ - countOuter: 10, - countInner: 100, - countSwap: 45, + expect(resultOptimized).toEqual({ + countOuter: 1, + countInner: 0, + countSwap: 0, }); }); + test('should handle an array with duplicate elements', () => { + const duplicates = [3, 3, 1, 1, 2, 2]; + const basicResult = bubbleSortCountersBasic(duplicates.slice()); + const optimizedResult = bubbleSortCounters(duplicates.slice()); + expect(duplicates.slice().sort((a, b) => a - b)).toEqual([1, 1, 2, 2, 3, 3]); + expect(basicResult.countSwap).toBeGreaterThanOrEqual(0); + expect(optimizedResult.countSwap).toBeGreaterThanOrEqual(0); + }); }); diff --git a/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.ts b/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.ts index 1da3db6..bf17620 100644 --- a/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.ts +++ b/algorithms/sorting/bubble-sort-counters/bubbleSortCounters.ts @@ -1,6 +1,10 @@ -import {CounterResults} from '../types'; +import {SortCounters} from '../types'; -export function bubbleSortCountersBasic(items: Array): CounterResults { +/** + * A basic Bubble Sort implementation that counts outer loop, inner loop, and swaps. + * @returns {SortCounters} - An object containing countOuter, countInner, and countSwap. + */ +export function bubbleSortCountersBasic(items: Array): SortCounters { let countOuter = 0; let countInner = 0; let countSwap = 0; @@ -18,16 +22,23 @@ export function bubbleSortCountersBasic(items: Array): CounterResults { return {countOuter, countInner, countSwap}; } -export function bubbleSortCounters(items: Array): CounterResults { +/** + * An optimized Bubble Sort implementation with a swapped flag, plus counters. + * Note that we changed the inner loop condition to prevent out-of-bounds checks. + * + * @returns {SortCounters} - An object containing countOuter, countInner, and countSwap. + */ +export function bubbleSortCounters(items: Array): SortCounters { let countOuter = 0; let countInner = 0; let countSwap = 0; let swapped = true; while (swapped) { - countOuter++; + countOuter++; // Each pass in the while loop swapped = false; - for (let i = 0; i < items.length; i++) { + // Use `items.length - 1` to avoid accessing items[i + 1] when i = items.length - 1 + for (let i = 0; i < items.length - 1; i++) { countInner++; if (items[i] > items[i + 1]) { countSwap++; diff --git a/algorithms/sorting/bubble-sort/bubbleSort.test.ts b/algorithms/sorting/bubble-sort/bubbleSort.test.ts index 7404fcf..0efd9a8 100644 --- a/algorithms/sorting/bubble-sort/bubbleSort.test.ts +++ b/algorithms/sorting/bubble-sort/bubbleSort.test.ts @@ -1,18 +1,53 @@ import {bubbleSortBasic, bubbleSort} from './bubbleSort'; -describe('bubble-sort', () => { - const array = [8, 2, 5, 6, 4, 3, 7, 10, 1, 9]; - const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - - const array2 = [80, 20, 50, 60, 40, 30, 70, 100, 10, 90]; - const result2 = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; - - it('basic', () => { +describe('bubbleSortBasic', () => { + test('should sort a numeric array in ascending order', () => { + const array = [8, 2, 5, 6, 4, 3, 7, 10, 1, 9]; + const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; expect(bubbleSortBasic(array)).toEqual(result); - expect(bubbleSortBasic(array2)).toEqual(result2); }); - it('normal', () => { - expect(bubbleSort(array)).toEqual(result); + test('should sort an empty array', () => { + expect(bubbleSortBasic([])).toEqual([]); + }); + test('should sort an array with one element', () => { + expect(bubbleSortBasic([42])).toEqual([42]); + }); + test('should sort a sorted array without changes', () => { + const array = [1, 2, 3, 4]; + expect(bubbleSortBasic(array)).toEqual([1, 2, 3, 4]); + }); + test('should sort an array with duplicate elements', () => { + const array = [3, 1, 2, 1, 3]; + const result = [1, 1, 2, 3, 3]; + expect(bubbleSortBasic(array)).toEqual(result); + }); + test('should allow a custom comparator for descending order', () => { + const array = [3, 1, 2]; + const descendingComparator = (a: number, b: number): boolean => a < b; + expect(bubbleSortBasic(array, descendingComparator)).toEqual([3, 2, 1]); + }); +}); + +describe('bubbleSort (optimized)', () => { + test('should sort a numeric array in ascending order', () => { + const array2 = [80, 20, 50, 60, 40, 30, 70, 100, 10, 90]; + const result2 = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; expect(bubbleSort(array2)).toEqual(result2); }); + test('should sort an empty array', () => { + expect(bubbleSort([])).toEqual([]); + }); + test('should sort an array with one element', () => { + expect(bubbleSort([42])).toEqual([42]); + }); + test('should sort an array with duplicate elements', () => { + const array = [4, 4, 2, 5, 2]; + const result = [2, 2, 4, 4, 5]; + expect(bubbleSort(array)).toEqual(result); + }); + test('should allow a custom comparator for descending order', () => { + const array = [3, 1, 2]; + const descendingComparator = (a: number, b: number): boolean => a < b; + expect(bubbleSort(array, descendingComparator)).toEqual([3, 2, 1]); + }); }); diff --git a/algorithms/sorting/bubble-sort/bubbleSort.ts b/algorithms/sorting/bubble-sort/bubbleSort.ts index 387b968..14ca3bb 100644 --- a/algorithms/sorting/bubble-sort/bubbleSort.ts +++ b/algorithms/sorting/bubble-sort/bubbleSort.ts @@ -1,7 +1,18 @@ -export function bubbleSortBasic(items: Array): Array { +/** + * Sorts an array in ascending order using the basic Bubble Sort algorithm. + * This version loops through the entire array multiple times (O(n^2)). + * + * @typeParam T - The type of array elements. + * @param items - The array to be sorted. + * @param comparator - An optional comparison function. If not provided, the function will compare items using the built-in `>` operator. + * @returns The sorted array (in-place modification). + */ +export function bubbleSortBasic(items: T[], comparator?: (a: T, b: T) => boolean): T[] { + const defaultComparator = (a: T, b: T): boolean => a > b; + const compare = comparator ?? defaultComparator; for (let i = 0; i < items.length; i++) { for (let j = 1; j < items.length; j++) { - if (items[j - 1] > items[j]) { + if (compare(items[j - 1], items[j])) { [items[j - 1], items[j]] = [items[j], items[j - 1]]; } } @@ -9,16 +20,34 @@ export function bubbleSortBasic(items: Array): Array { return items; } -export function bubbleSort(items: Array): Array { +/** + * Sorts an array in ascending order using an optimized Bubble Sort algorithm. + * This version uses a while loop with a `swapped` flag and stops when no swaps occur. + * Time complexity is still O(n^2) in the worst case, but it can terminate earlier + * if the array gets sorted before all passes are completed. + * + * @typeParam T - The type of array elements. + * @param items - The array to be sorted. + * @param comparator - An optional comparison function. If not provided, the function will compare items using the built-in `>` operator. + * @returns The sorted array (in-place modification). + */ +export function bubbleSort(items: T[], comparator?: (a: T, b: T) => boolean): T[] { + const defaultComparator = (a: T, b: T): boolean => a > b; + const compare = comparator ?? defaultComparator; + let swapped = true; + let n = items.length; // This n can be used to narrow down the range of items to be sorted as the final element is determined. + while (swapped) { swapped = false; - for (let i = 0; i < items.length; i++) { - if (items[i] > items[i + 1]) { + for (let i = 0; i < n - 1; i++) { + if (compare(items[i], items[i + 1])) { [items[i], items[i + 1]] = [items[i + 1], items[i]]; swapped = true; } } + // Since the last element is fixed with one pass, the search range is narrowed. + n--; } return items; } diff --git a/algorithms/sorting/insertion-sort-counters/insertionSortCounters.test.ts b/algorithms/sorting/insertion-sort-counters/insertionSortCounters.test.ts index dd09d64..73dee13 100644 --- a/algorithms/sorting/insertion-sort-counters/insertionSortCounters.test.ts +++ b/algorithms/sorting/insertion-sort-counters/insertionSortCounters.test.ts @@ -1,25 +1,30 @@ import {insertionSortCounters} from './insertionSortCounters'; -describe('insertion-sort-counters', () => { - const randomArray = [2, 8, 5, 6, 4, 3, 10, 7, 1, 9]; - const orderedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - const reversedArray = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; - - it('normal', () => { - expect(insertionSortCounters(randomArray.slice())).toEqual({ - countOuter: 10, - countInner: 20, - countSwap: 20, - }); - expect(insertionSortCounters(orderedArray.slice())).toEqual({ - countOuter: 10, +describe('insertion-sort-counters additional tests', () => { + test('should handle an empty array', () => { + expect(insertionSortCounters([])).toEqual({ + countOuter: 0, countInner: 0, countSwap: 0, }); - expect(insertionSortCounters(reversedArray.slice())).toEqual({ - countOuter: 10, - countInner: 45, - countSwap: 45, + }); + test('should handle a single-element array', () => { + expect(insertionSortCounters([42])).toEqual({ + countOuter: 0, + countInner: 0, + countSwap: 0, }); }); + test('should handle an array with duplicate elements', () => { + const arr = [3, 3, 1, 1, 2, 2]; + const result = insertionSortCounters(arr.slice()); + expect(result.countOuter).toBeGreaterThanOrEqual(1); + expect(result.countInner).toBeGreaterThanOrEqual(0); + expect(result.countSwap).toBeGreaterThanOrEqual(0); + }); + test('should handle an already sorted array', () => { + const sorted = [1, 2, 3, 4, 5]; + const result = insertionSortCounters(sorted.slice()); + expect(result.countSwap).toBe(0); + }); }); diff --git a/algorithms/sorting/insertion-sort-counters/insertionSortCounters.ts b/algorithms/sorting/insertion-sort-counters/insertionSortCounters.ts index 51ce126..fcd4dd1 100644 --- a/algorithms/sorting/insertion-sort-counters/insertionSortCounters.ts +++ b/algorithms/sorting/insertion-sort-counters/insertionSortCounters.ts @@ -1,15 +1,30 @@ -import {CounterResults} from '../types'; +import {SortCounters} from '../types'; -export function insertionSortCounters(items: Array): CounterResults { +/** + * A variant of Insertion Sort that counts the number of outer loop iterations, + * inner loop comparisons (or shifts), and swaps. + * + * @typeParam T - The type of array elements (must be comparable). + * @param items - The array to be sorted in-place. + * @param comparator - An optional comparison function. If not provided, the built-in `>` operator is used. + * @returns {SortCounters} - An object containing + */ +export function insertionSortCounters( + items: T[], + comparator?: (a: T, b: T) => boolean, +): SortCounters { let countOuter = 0; let countInner = 0; let countSwap = 0; - for (let i = 0; i < items.length; i++) { + const defaultComparator = (a: T, b: T): boolean => a > b; + const compare = comparator ?? defaultComparator; + for (let i = 1; i < items.length; i++) { countOuter++; - const tmp: T = items[i]; - let j: number = i - 1; - while (j >= 0 && items[j] > tmp) { + const tmp = items[i]; + let j = i - 1; + // Shift elements to the right while they are "greater than" tmp + while (j >= 0 && compare(items[j], tmp)) { countInner++; countSwap++; items[j + 1] = items[j]; diff --git a/algorithms/sorting/insertion-sort/insertionSort.test.ts b/algorithms/sorting/insertion-sort/insertionSort.test.ts index 8dd72b5..c3489dd 100644 --- a/algorithms/sorting/insertion-sort/insertionSort.test.ts +++ b/algorithms/sorting/insertion-sort/insertionSort.test.ts @@ -1,14 +1,39 @@ import {insertionSort} from './insertionSort'; -describe('insertion-sort', () => { - it('normal', () => { +describe('insertionSort', () => { + test('should sort an unsorted array of numbers', () => { const array = [10, 2, 5, 6, 4, 3, 7, 9, 1, 8]; const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - + expect(insertionSort(array.slice())).toEqual(result); + }); + test('should sort another unsorted array of numbers', () => { const array2 = [100, 20, 50, 60, 40, 30, 70, 90, 10, 80]; const result2 = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; - - expect(insertionSort(array)).toEqual(result); - expect(insertionSort(array2)).toEqual(result2); + expect(insertionSort(array2.slice())).toEqual(result2); + }); + test('should handle an empty array', () => { + const empty: number[] = []; + expect(insertionSort(empty.slice())).toEqual([]); + }); + test('should handle a single-element array', () => { + const single = [42]; + expect(insertionSort(single.slice())).toEqual([42]); + }); + test('should handle an already-sorted array', () => { + const sorted = [1, 2, 3, 4, 5]; + expect(insertionSort(sorted.slice())).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle a reverse-sorted array', () => { + const reversed = [5, 4, 3, 2, 1]; + expect(insertionSort(reversed.slice())).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle duplicate elements', () => { + const duplicates = [3, 1, 2, 3, 2]; + expect(insertionSort(duplicates.slice())).toEqual([1, 2, 2, 3, 3]); + }); + test('should allow a custom comparator (descending order)', () => { + const arr = [3, 1, 2]; + const descendingComparator = (a: number, b: number): boolean => a < b; + expect(insertionSort(arr.slice(), descendingComparator)).toEqual([3, 2, 1]); }); }); diff --git a/algorithms/sorting/insertion-sort/insertionSort.ts b/algorithms/sorting/insertion-sort/insertionSort.ts index 7609590..b6ebe66 100644 --- a/algorithms/sorting/insertion-sort/insertionSort.ts +++ b/algorithms/sorting/insertion-sort/insertionSort.ts @@ -1,8 +1,25 @@ -export function insertionSort(items: Array): Array { - for (let i = 0; i < items.length; i++) { - const tmp: T = items[i]; - let j: number = i - 1; - while (j >= 0 && items[j] > tmp) { +/** + * Sorts an array in ascending order using the Insertion Sort algorithm. + * + * @typeParam T - The type of elements in the array. + * @param items - An array of elements to sort. + * @param comparator - An optional comparison function. If not provided, elements will be compared using the built-in `>` operator. + * @returns The sorted array (in-place modification). + * + * @remarks + ** Time Complexity: O(n^2) in the worst case. + ** This implementation is stable as long as the comparison is consistent. + */ +export function insertionSort(items: T[], comparator?: (a: T, b: T) => boolean): T[] { + const defaultComparator = (a: T, b: T): boolean => a > b; + const compare = comparator ?? defaultComparator; + // Typically, insertion sort starts i at 1 + for (let i = 1; i < items.length; i++) { + const tmp = items[i]; + let j = i - 1; + // Compare using 'compare(items[j], tmp)'. + // If 'true', it means items[j] should move to the right. + while (j >= 0 && compare(items[j], tmp)) { items[j + 1] = items[j]; j--; } diff --git a/algorithms/sorting/merge-sort-counters/mergeSortCounters.test.ts b/algorithms/sorting/merge-sort-counters/mergeSortCounters.test.ts new file mode 100644 index 0000000..eba3752 --- /dev/null +++ b/algorithms/sorting/merge-sort-counters/mergeSortCounters.test.ts @@ -0,0 +1,45 @@ +import {mergeSortCountersTopDown, mergeSortCountersBottomUp} from './mergeSortCounters'; + +describe('mergeSortCounters', () => { + const randomArray = [9, 2, 5, 6, 4, 3, 7, 10, 1, 8]; + const orderedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const reversedArray = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; + + test('TopDown - random array', () => { + const {sortedArray, counters} = mergeSortCountersTopDown(randomArray.slice()); + expect(sortedArray).toEqual(orderedArray); + expect(counters.countOuter).toBeGreaterThan(0); + expect(counters.countInner).toBeGreaterThan(0); + }); + test('TopDown - ordered array', () => { + const {sortedArray, counters} = mergeSortCountersTopDown(orderedArray.slice()); + expect(sortedArray).toEqual(orderedArray); + expect(counters.countOuter).toBeGreaterThan(0); + }); + test('TopDown - reversed array', () => { + const {sortedArray, counters} = mergeSortCountersTopDown(reversedArray.slice()); + expect(sortedArray).toEqual(orderedArray); + expect(counters.countOuter).toBeGreaterThan(0); + }); + test('BottomUp - random array', () => { + const {sortedArray, counters} = mergeSortCountersBottomUp(randomArray.slice()); + expect(sortedArray).toEqual(orderedArray); + expect(counters.countOuter).toBeGreaterThan(0); + expect(counters.countInner).toBeGreaterThan(0); + expect(counters.countSwap).toBeGreaterThan(0); + }); + test('BottomUp - reversed array', () => { + const {sortedArray, counters} = mergeSortCountersBottomUp(reversedArray.slice()); + expect(sortedArray).toEqual(orderedArray); + expect(counters.countOuter).toBeGreaterThan(0); + expect(counters.countInner).toBeGreaterThan(0); + expect(counters.countSwap).toBeGreaterThan(0); + }); + test('BottomUp - empty array', () => { + const {sortedArray, counters} = mergeSortCountersBottomUp([]); + expect(sortedArray).toEqual([]); + expect(counters.countOuter).toBe(0); + expect(counters.countInner).toBe(0); + expect(counters.countSwap).toBe(0); + }); +}); diff --git a/algorithms/sorting/merge-sort-counters/mergeSortCounters.ts b/algorithms/sorting/merge-sort-counters/mergeSortCounters.ts index 7ab6b6d..3e76280 100644 --- a/algorithms/sorting/merge-sort-counters/mergeSortCounters.ts +++ b/algorithms/sorting/merge-sort-counters/mergeSortCounters.ts @@ -1,82 +1,142 @@ -// sample of arrays to merge-sort-counters -const randomArray = [9, 2, 5, 6, 4, 3, 7, 10, 1, 8]; -const orderedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; -const reversedArray = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; - -let countOuter = 0; -let countInner = 0; -let countSwap = 0; - -function resetCounters(): void { - countOuter = 0; - countInner = 0; - countSwap = 0; +/** + * An object to track the number of outer calls, inner comparisons, and "swaps" (or writes). + */ +export interface MergeSortCounters { + countOuter: number; + countInner: number; + countSwap: number; } /** - * Top-down implementation + * Merges two sorted arrays (Top-down approach) while incrementing counters. + * + * @param left - A sorted array of numbers. + * @param right - A sorted array of numbers. + * @param counters - An object to track outer, inner, and swap counts. + * @returns A new sorted array containing elements from left and right. + * + * @remarks + ** `countInner` is incremented every time we compare the first elements of left and right. + ** There is no actual "swap" in top-down mergesort, so `countSwap` might remain 0 if you are counting swaps strictly. If you want to count element copies, you can increment here. */ -function mergeCountersTopDown(left: number[], right: number[]): number[] { +function mergeCountersTopDown( + left: number[], + right: number[], + counters: MergeSortCounters, +): number[] { const array: number[] = []; - let nonFirst: number | undefined; while (left.length && right.length) { - countInner++; - left[0] < right[0] ? (nonFirst = left.shift()) : (nonFirst = right.shift()); - if (nonFirst) { - array.push(nonFirst); + // Each comparison increments countInner + counters.countInner++; + if (left[0] < right[0]) { + array.push(left.shift()!); + } else { + array.push(right.shift()!); } + // If you want to count element "copies" to the merged array, increment here: + // counters.countSwap++; } - return array.concat(left.slice()).concat(right.slice()); + return array.concat(left, right); } -export function mergeSortCountersTopDown(items: number[]): number[] { - countOuter++; +/** + * Recursively sorts an array using the top-down merge sort approach, + * while tracking the number of outer calls, inner comparisons, and swaps. + * + * @param items - The array of numbers to sort. + * @param counters - An optional counters object. If not provided, a new one will be created. + * @returns A new sorted array of numbers. + * + * @remarks + ** This function does NOT modify the original array (non-destructive). + ** countOuter` is incremented for each call to mergeSortCountersTopDown. + */ +export function mergeSortCountersTopDown( + items: number[], + counters?: MergeSortCounters, +): {sortedArray: number[]; counters: MergeSortCounters} { + // If counters not provided, initialize a new one + const localCounters = counters ?? { + countOuter: 0, + countInner: 0, + countSwap: 0, + }; + + localCounters.countOuter++; + if (items.length < 2) { - return items; + return {sortedArray: items, counters: localCounters}; } const middle: number = Math.floor(items.length / 2); const leftItems: number[] = items.slice(0, middle); const rightItems: number[] = items.slice(middle); - return mergeCountersTopDown( - mergeSortCountersTopDown(leftItems), - mergeSortCountersTopDown(rightItems), + const leftResult = mergeSortCountersTopDown(leftItems, localCounters); + const rightResult = mergeSortCountersTopDown(rightItems, localCounters); + + const merged = mergeCountersTopDown( + leftResult.sortedArray, + rightResult.sortedArray, + localCounters, ); + return {sortedArray: merged, counters: localCounters}; } -mergeSortCountersTopDown(randomArray.slice()); // => outer: 19 inner: 24 swap: 0 -console.log('outer:', countOuter, 'inner:', countInner, 'swap:', countSwap); -resetCounters(); - -mergeSortCountersTopDown(orderedArray.slice()); // => outer: 19 inner: 15 swap: 0 -console.log('outer:', countOuter, 'inner:', countInner, 'swap:', countSwap); -resetCounters(); - -mergeSortCountersTopDown(reversedArray.slice()); // => outer: 19 inner: 19 swap: 0 -console.log('outer:', countOuter, 'inner:', countInner, 'swap:', countSwap); -resetCounters(); - /** - * Bottom-up implementation + * Iteratively sorts an array using the bottom-up merge sort approach, + * while tracking counters. + * + * @param items - The array of numbers to sort. (in-place sort) + * @param counters - An optional counters object. If not provided, a new one will be created. + * @returns The same array (sorted), plus the updated counters. + * + * @remarks + ** This function modifies the original array in-place. + ** `countOuter` is incremented each time we increase the merge step. + ** `countInner` is incremented each time we merge a subarray. + ** `countSwap` is incremented each time we write back into the items array. */ -function mergeSortCountersBottomUp(items: number[]): number[] { +export function mergeSortCountersBottomUp( + items: number[], + counters?: MergeSortCounters, +): {sortedArray: number[]; counters: MergeSortCounters} { + const localCounters = counters ?? { + countOuter: 0, + countInner: 0, + countSwap: 0, + }; + let step = 1; while (step < items.length) { - countOuter++; + localCounters.countOuter++; let left = 0; while (left + step < items.length) { - countInner++; - mergeCountersBottomUp(items, left, step); + localCounters.countInner++; + + mergeCountersBottomUp(items, left, step, localCounters); left += step * 2; } step *= 2; } - return items; + return {sortedArray: items, counters: localCounters}; } -function mergeCountersBottomUp(items: number[], left: number, step: number): void { +/** + * Merges two subarrays in-place for the bottom-up approach, incrementing swap counts. + * + * @param items - The array to modify in place. + * @param left - The start index of the left subarray. + * @param step - The size of the subarray to merge. + * @param counters - The counters to update. + */ +function mergeCountersBottomUp( + items: number[], + left: number, + step: number, + counters: MergeSortCounters, +): void { const tmp: number[] = []; const right: number = left + step; const last: number = Math.min(left + step * 2 - 1, items.length - 1); @@ -93,21 +153,8 @@ function mergeCountersBottomUp(items: number[], left: number, step: number): voi moveRight++; } } - for (let j = left; j <= last; j++) { - countSwap++; + counters.countSwap++; // we are overwriting items[j] with tmp[j] items[j] = tmp[j]; } } - -mergeSortCountersBottomUp(randomArray.slice()); // => outer: 4 inner: 9 swap: 36 -console.log('outer:', countOuter, 'inner:', countInner, 'swap:', countSwap); -resetCounters(); - -mergeSortCountersBottomUp(orderedArray.slice()); // => outer: 4 inner: 9 swap: 36 -console.log('outer:', countOuter, 'inner:', countInner, 'swap:', countSwap); -resetCounters(); - -mergeSortCountersBottomUp(reversedArray.slice()); // => outer: 4 inner: 9 swap: 36 -console.log('outer:', countOuter, 'inner:', countInner, 'swap:', countSwap); -resetCounters(); diff --git a/algorithms/sorting/merge-sort/mergeSort.test.ts b/algorithms/sorting/merge-sort/mergeSort.test.ts index c42ca2b..aa29760 100644 --- a/algorithms/sorting/merge-sort/mergeSort.test.ts +++ b/algorithms/sorting/merge-sort/mergeSort.test.ts @@ -2,11 +2,42 @@ import {mergeSortTopDown, mergeSortBottomUp} from './mergeSort'; describe('merge-sort', () => { const array = [10, 2, 5, 6, 4, 3, 7, 9, 1, 8]; - const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - it('TopDown', () => { - expect(mergeSortTopDown(array)).toEqual(result); + const sorted = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + test('TopDown - should sort an unsorted array', () => { + expect(mergeSortTopDown(array.slice())).toEqual(sorted); }); - it('BottomUp', () => { - expect(mergeSortBottomUp(array)).toEqual(result); + test('BottomUp - should sort an unsorted array in place', () => { + const copy = array.slice(); + mergeSortBottomUp(copy); + expect(copy).toEqual(sorted); + }); + test('TopDown - should not mutate the original array', () => { + const original = array.slice(); + mergeSortTopDown(original); + expect(original).toEqual(array); + }); + test('BottomUp - should sort an empty array without errors', () => { + const empty: number[] = []; + expect(mergeSortBottomUp(empty)).toEqual([]); + }); + test('TopDown - should handle a single-element array', () => { + const single = [42]; + expect(mergeSortTopDown(single.slice())).toEqual([42]); + }); + test('BottomUp - should handle a sorted array (no changes)', () => { + const alreadySorted = [1, 2, 3, 4, 5]; + const copy = alreadySorted.slice(); + mergeSortBottomUp(copy); + expect(copy).toEqual(alreadySorted); + }); + test('TopDown - should handle a reverse-sorted array', () => { + const reverse = [5, 4, 3, 2, 1]; + expect(mergeSortTopDown(reverse)).toEqual([1, 2, 3, 4, 5]); + }); + test('BottomUp - should handle duplicates (stable sort)', () => { + const duplicates = [3, 1, 2, 2, 3]; + const copy = duplicates.slice(); + mergeSortBottomUp(copy); + expect(copy).toEqual([1, 2, 2, 3, 3]); }); }); diff --git a/algorithms/sorting/merge-sort/mergeSort.ts b/algorithms/sorting/merge-sort/mergeSort.ts index f89f9ee..1dd3022 100644 --- a/algorithms/sorting/merge-sort/mergeSort.ts +++ b/algorithms/sorting/merge-sort/mergeSort.ts @@ -1,33 +1,55 @@ /** - * Top-down implementation + * Merges two sorted arrays into one sorted array (Top-down merge). + * This function is used by mergeSortTopDown. + * + * @param left - A sorted array of numbers. + * @param right - A sorted array of numbers. + * @returns A new array containing all elements from left and right in sorted order. */ function mergeTopDown(left: number[], right: number[]): number[] { const array: number[] = []; - let nonFirst: number | undefined; - + // While both arrays have elements while (left.length && right.length) { - left[0] < right[0] ? (nonFirst = left.shift()) : (nonFirst = right.shift()); - if (nonFirst) { - array.push(nonFirst); + // If left[0] is smaller or equal, shift from left, otherwise from right + if (left[0] <= right[0]) { + array.push(left.shift()!); + } else { + array.push(right.shift()!); } } - return array.concat(left.slice()).concat(right.slice()); + // Concat remaining elements (only one of them will have leftovers) + return array.concat(left, right); } +/** + * Recursively sorts an array using the top-down merge sort approach. + * + * @param items - An array of numbers to sort. The original array is NOT modified. + * @returns A new sorted array (ascending). + * + * @remarks + ** Time complexity: O(n log n). + ** This is a stable sort, assuming the comparison uses `<=` for ties. + */ export function mergeSortTopDown(items: number[]): number[] { if (items.length < 2) { return items; } - const middle = Math.floor(items.length / 2); const leftItems = items.slice(0, middle); const rightItems = items.slice(middle); - return mergeTopDown(mergeSortTopDown(leftItems), mergeSortTopDown(rightItems)); } /** - * Bottom-up implementation + * Merges the subarrays [left..left+step-1] and [left+step..left+2*step-1] in-place. + * + * @param items - The array of numbers to partially merge (in-place). + * @param left - The starting index of the left subarray. + * @param step - The size of the subarray to merge. + * + * @remarks + ** This function modifies `items` directly. */ function mergeBottomUp(items: number[], left: number, step: number): void { const tmp: number[] = []; @@ -52,6 +74,17 @@ function mergeBottomUp(items: number[], left: number, step: number): void { } } +/** + * Iteratively sorts an array using the bottom-up merge sort approach. + * + * @param items - An array of numbers to sort. The array is sorted in-place. + * @returns The same array, now sorted in ascending order. + * + * @remarks + ** Time complexity: O(n log n). + ** This is a stable sort, assuming the comparison uses `<=` for ties. + ** `step` starts from 1, doubling each time until it covers the entire array. + */ export function mergeSortBottomUp(items: number[]): number[] { let step = 1; while (step < items.length) { diff --git a/algorithms/sorting/quick-sort/quickSort.test.ts b/algorithms/sorting/quick-sort/quickSort.test.ts index 87bafeb..1084a05 100644 --- a/algorithms/sorting/quick-sort/quickSort.test.ts +++ b/algorithms/sorting/quick-sort/quickSort.test.ts @@ -1,9 +1,33 @@ import {quickSort} from './quickSort'; describe('quickSort', () => { - it('normal', () => { + test('should sort an unsorted array of numbers (ascending)', () => { const array = [3, 2, 5, 6, 4, 9, 7, 10, 1, 8]; const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; expect(quickSort(array)).toEqual(result); + expect(array).toEqual([3, 2, 5, 6, 4, 9, 7, 10, 1, 8]); + }); + test('should handle an already-sorted array', () => { + const orderedArray = [1, 2, 3, 4, 5]; + expect(quickSort(orderedArray)).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle a reverse-sorted array', () => { + const reversed = [5, 4, 3, 2, 1]; + expect(quickSort(reversed)).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle an empty array', () => { + expect(quickSort([])).toEqual([]); + }); + test('should handle a single-element array', () => { + expect(quickSort([42])).toEqual([42]); + }); + test('should handle duplicate elements', () => { + const duplicates = [3, 1, 2, 3, 2]; + expect(quickSort(duplicates)).toEqual([1, 2, 2, 3, 3]); + }); + test('should allow a custom comparator (descending order)', () => { + const arr = [3, 1, 2]; + const descendingComparator = (a: number, b: number): boolean => a > b; + expect(quickSort(arr, descendingComparator)).toEqual([3, 2, 1]); }); }); diff --git a/algorithms/sorting/quick-sort/quickSort.ts b/algorithms/sorting/quick-sort/quickSort.ts index 250fd7b..dbc30e1 100644 --- a/algorithms/sorting/quick-sort/quickSort.ts +++ b/algorithms/sorting/quick-sort/quickSort.ts @@ -1,11 +1,45 @@ -export function quickSort(items: Array): Array { +/** + * Sorts an array in ascending order using the Quick Sort algorithm (non-destructive). + * This implementation picks a random pivot, partitions the array around it, and recurses. + * + * @typeParam T - The type of elements in the array. + * @param items - The array to sort (will not be modified). + * @param comparator - An optional comparison function. If not provided, the function will compare items using the built-in `<` or `>` operator. + * @returns A new sorted array. + * + * @remarks + ** Time Complexity: O(n log n) on average, but O(n^2) in the worst case. + ** This is a non-in-place algorithm: it creates new arrays (via filter). + ** To make it in-place, we'd need to partition the array without creating new arrays. + */ +export function quickSort(items: T[], comparator?: (a: T, b: T) => boolean): T[] { + const defaultComparator = (a: T, b: T): boolean => a < b; + const compare = comparator ?? defaultComparator; + if (items.length < 2) { return items; } - const pivot: T = items[0]; - const lesser = items.filter((item) => item < pivot); - const greater = items.filter((item) => item > pivot); + // Choose a random pivot to improve average performance + const pivotIndex = Math.floor(Math.random() * items.length); + const pivot = items[pivotIndex]; + + // Partition into lesser, equal, and greater + const lesser: T[] = []; + const equal: T[] = []; + const greater: T[] = []; + + for (const item of items) { + if (compare(item, pivot) && item !== pivot) { + lesser.push(item); + } else if (compare(pivot, item) && item !== pivot) { + greater.push(item); + } else { + equal.push(item); + } + } - return [...quickSort(lesser), pivot, ...quickSort(greater)]; + // Recursively sort lesser and greater, then concatenate + // The 'equal' array handles duplicates + return [...quickSort(lesser, comparator), ...equal, ...quickSort(greater, comparator)]; } diff --git a/algorithms/sorting/selection-sort-counters/selectionSortCounters.test.ts b/algorithms/sorting/selection-sort-counters/selectionSortCounters.test.ts index 6d3dcd8..9c5510d 100644 --- a/algorithms/sorting/selection-sort-counters/selectionSortCounters.test.ts +++ b/algorithms/sorting/selection-sort-counters/selectionSortCounters.test.ts @@ -1,11 +1,10 @@ import {selectionSortCounters} from './selectionSortCounters'; -describe('selection-sort-counters', () => { +describe('selectionSortCounters', () => { const randomArray = [9, 2, 5, 6, 4, 3, 7, 10, 1, 8]; const orderedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const reversedArray = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; - - it('normal', () => { + test('normal', () => { expect(selectionSortCounters(randomArray.slice())).toEqual({ countOuter: 10, countInner: 45, @@ -22,4 +21,35 @@ describe('selection-sort-counters', () => { countSwap: 5, }); }); + test('should handle an empty array', () => { + const empty: number[] = []; + const result = selectionSortCounters(empty.slice()); + expect(result).toEqual({ + countOuter: 0, + countInner: 0, + countSwap: 0, + }); + }); + test('should handle a single-element array', () => { + const single = [42]; + const result = selectionSortCounters(single.slice()); + expect(result).toEqual({ + countOuter: 1, + countInner: 0, + countSwap: 0, + }); + }); + test('should handle duplicates', () => { + const duplicates = [2, 3, 1, 2, 1]; + const sorted = duplicates.slice().sort((a, b) => a - b); + const arrForCounting = duplicates.slice(); + + selectionSortCounters(arrForCounting); + expect(arrForCounting).toEqual(sorted); + + const {countOuter, countInner, countSwap} = selectionSortCounters(duplicates.slice()); + expect(countOuter).toBe(duplicates.length); + expect(countInner).toBeGreaterThanOrEqual(0); + expect(countSwap).toBeGreaterThanOrEqual(0); + }); }); diff --git a/algorithms/sorting/selection-sort-counters/selectionSortCounters.ts b/algorithms/sorting/selection-sort-counters/selectionSortCounters.ts index 0edd849..8de3f0a 100644 --- a/algorithms/sorting/selection-sort-counters/selectionSortCounters.ts +++ b/algorithms/sorting/selection-sort-counters/selectionSortCounters.ts @@ -1,22 +1,30 @@ -import {CounterResults} from '../types'; +import {SortCounters} from '../types'; -export function selectionSortCounters(items: Array): CounterResults { +/** + * Performs Selection Sort on the given array, counting the number of outer loops, + * inner comparisons, and swaps. + * + * @typeParam T - The type of elements in the array (must be comparable with `<`). + * @param items - The array to be sorted in-place. + * @returns A SortCounters object containing + */ +export function selectionSortCounters(items: T[]): SortCounters { let countOuter = 0; let countInner = 0; let countSwap = 0; for (let i = 0; i < items.length; i++) { countOuter++; - let min = i; + let minIndex = i; for (let j = i + 1; j < items.length; j++) { countInner++; - if (items[j] < items[min]) { - min = j; + if (items[j] < items[minIndex]) { + minIndex = j; } } - if (i !== min) { + if (i !== minIndex) { countSwap++; - [items[i], items[min]] = [items[min], items[i]]; + [items[i], items[minIndex]] = [items[minIndex], items[i]]; } } return {countOuter, countInner, countSwap}; diff --git a/algorithms/sorting/selection-sort/selectionSort.test.ts b/algorithms/sorting/selection-sort/selectionSort.test.ts index 3710fc6..ba6151e 100644 --- a/algorithms/sorting/selection-sort/selectionSort.test.ts +++ b/algorithms/sorting/selection-sort/selectionSort.test.ts @@ -1,9 +1,32 @@ import {selectionSort} from './selectionSort'; -describe('selection-sort', () => { - it('normal', () => { +describe('selectionSort', () => { + test('should sort a general unsorted array of numbers', () => { const array = [7, 3, 5, 6, 4, 2, 9, 10, 1, 8]; const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - expect(selectionSort(array)).toEqual(result); + expect(selectionSort(array.slice())).toEqual(result); + }); + test('should handle an already-sorted array', () => { + const sorted = [1, 2, 3, 4, 5]; + expect(selectionSort(sorted.slice())).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle a reverse-sorted array', () => { + const reversed = [5, 4, 3, 2, 1]; + expect(selectionSort(reversed.slice())).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle an empty array', () => { + expect(selectionSort([])).toEqual([]); + }); + test('should handle a single-element array', () => { + expect(selectionSort([42])).toEqual([42]); + }); + test('should handle duplicate elements', () => { + const duplicates = [2, 3, 1, 2, 1]; + expect(selectionSort(duplicates.slice())).toEqual([1, 1, 2, 2, 3]); + }); + test('should allow a custom comparator (descending order)', () => { + const array = [3, 1, 2]; + const descendingComparator = (a: number, b: number): boolean => a > b; + expect(selectionSort(array.slice(), descendingComparator)).toEqual([3, 2, 1]); }); }); diff --git a/algorithms/sorting/selection-sort/selectionSort.ts b/algorithms/sorting/selection-sort/selectionSort.ts index 222cea2..ac14cce 100644 --- a/algorithms/sorting/selection-sort/selectionSort.ts +++ b/algorithms/sorting/selection-sort/selectionSort.ts @@ -1,13 +1,33 @@ -export function selectionSort(items: Array): Array { +/** + * Sorts an array in ascending order using the Selection Sort algorithm (in-place). + * + * @typeParam T - The type of elements in the array. + * @param items - The array to be sorted (will be modified in-place). + * @param comparator - An optional comparison function. If not provided, + * the function compares using the built-in `<` operator. + * Return `true` if `a` should come before `b`. + * @returns The same array, now sorted in ascending order (or according to the comparator). + * + * @remarks + * - Time Complexity: O(n^2) in the average and worst cases. + * - Selection Sort finds the minimum element (considering ascending order) from the unsorted part + * and puts it at the beginning of the unsorted part. + * - Since it always swaps once per outer loop iteration, it performs fewer writes than other + * O(n^2) sorts like Bubble Sort or Insertion Sort. + * - The array is sorted in-place, meaning the original array is modified. + */ +export function selectionSort(items: T[], comparator?: (a: T, b: T) => boolean): T[] { + const defaultComparator = (a: T, b: T): boolean => a < b; + const compare = comparator ?? defaultComparator; for (let i = 0; i < items.length; i++) { - let min: number = i; - for (let j: number = i + 1; j < items.length; j++) { - if (items[j] < items[min]) { - min = j; + let minIndex = i; + for (let j = i + 1; j < items.length; j++) { + if (compare(items[j], items[minIndex])) { + minIndex = j; } } - if (i !== min) { - [items[i], items[min]] = [items[min], items[i]]; + if (i !== minIndex) { + [items[i], items[minIndex]] = [items[minIndex], items[i]]; } } return items; diff --git a/algorithms/sorting/shell-sort-counters/shellSortCounters.test.ts b/algorithms/sorting/shell-sort-counters/shellSortCounters.test.ts index c1c0772..35f11e0 100644 --- a/algorithms/sorting/shell-sort-counters/shellSortCounters.test.ts +++ b/algorithms/sorting/shell-sort-counters/shellSortCounters.test.ts @@ -1,24 +1,22 @@ import {shellSortCounters} from './shellSortCounters'; -describe('shellSortCounters', () => { - const randomArray = [8, 2, 3, 6, 4, 5, 7, 10, 1, 9]; - const orderedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - const reversedArray = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; - it('normal', () => { - expect(shellSortCounters(randomArray)).toEqual({ - countOuter: 15, - countInner: 7, - countSwap: 7, - }); - expect(shellSortCounters(orderedArray)).toEqual({ - countOuter: 15, - countInner: 0, - countSwap: 0, - }); - expect(shellSortCounters(reversedArray)).toEqual({ - countOuter: 15, - countInner: 13, - countSwap: 13, - }); +describe('shellSortCounters additional tests', () => { + test('should handle an empty array', () => { + const empty: number[] = []; + const result = shellSortCounters(empty.slice()); + expect(result).toEqual({countOuter: 0, countInner: 0, countSwap: 0}); + }); + test('should handle a single-element array', () => { + const single = [42]; + const result = shellSortCounters(single.slice()); + expect(result).toEqual({countOuter: 0, countInner: 0, countSwap: 0}); + }); + test('should handle duplicate elements', () => { + const duplicates = [2, 3, 1, 2, 1]; + const sortedArray = duplicates.slice().sort((a, b) => a - b); + const arrToSort = duplicates.slice(); + const counters = shellSortCounters(arrToSort); + expect(arrToSort).toEqual(sortedArray); + expect(counters.countOuter).toBeGreaterThanOrEqual(1); }); }); diff --git a/algorithms/sorting/shell-sort-counters/shellSortCounters.ts b/algorithms/sorting/shell-sort-counters/shellSortCounters.ts index 48a9768..53010f0 100644 --- a/algorithms/sorting/shell-sort-counters/shellSortCounters.ts +++ b/algorithms/sorting/shell-sort-counters/shellSortCounters.ts @@ -1,8 +1,18 @@ -import {CounterResults} from '../types'; +import {SortCounters} from '../types'; +/** + * Predefined gap sequence (Ciura's sequence) for Shell Sort. + * It is relatively efficient for many practical cases. + */ const gaps = [701, 301, 132, 57, 23, 10, 4, 1]; -export function shellSortCounters(items: Array): CounterResults { +/** + * Performs Shell Sort on the given numeric array (in-place), counting various operations. + * + * @param items - The numeric array to be sorted. + * @returns A SortCounters object + */ +export function shellSortCounters(items: number[]): SortCounters { let countOuter = 0; let countInner = 0; let countSwap = 0; @@ -13,6 +23,7 @@ export function shellSortCounters(items: Array): CounterResults { countOuter++; const tmp = items[i]; let last = i; + // Shift elements until we find the correct position for 'tmp' for (let j = i; j >= gap && items[j - gap] > tmp; j -= gap) { countInner++; countSwap++; diff --git a/algorithms/sorting/shell-sort/shellSort.test.ts b/algorithms/sorting/shell-sort/shellSort.test.ts index 347fff8..fade151 100644 --- a/algorithms/sorting/shell-sort/shellSort.test.ts +++ b/algorithms/sorting/shell-sort/shellSort.test.ts @@ -1,9 +1,27 @@ import {shellSort} from './shellSort'; -describe('shell-sort', () => { - it('normal', () => { - const array: number[] = [10, 2, 5, 6, 4, 3, 7, 9, 1, 8]; - const result: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - expect(shellSort(array)).toEqual(result); +describe('shellSort', () => { + test('should sort an unsorted array of numbers', () => { + const array = [10, 2, 5, 6, 4, 3, 7, 9, 1, 8]; + const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + expect(shellSort(array.slice())).toEqual(result); + }); + test('should handle an already sorted array', () => { + const sorted = [1, 2, 3, 4, 5]; + expect(shellSort(sorted.slice())).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle a reverse-sorted array', () => { + const reversed = [5, 4, 3, 2, 1]; + expect(shellSort(reversed.slice())).toEqual([1, 2, 3, 4, 5]); + }); + test('should handle an empty array', () => { + expect(shellSort([])).toEqual([]); + }); + test('should handle a single-element array', () => { + expect(shellSort([42])).toEqual([42]); + }); + test('should handle an array with duplicate elements', () => { + const arr = [3, 1, 2, 3, 2]; + expect(shellSort(arr.slice())).toEqual([1, 2, 2, 3, 3]); }); }); diff --git a/algorithms/sorting/shell-sort/shellSort.ts b/algorithms/sorting/shell-sort/shellSort.ts index c180b55..4de5f7a 100644 --- a/algorithms/sorting/shell-sort/shellSort.ts +++ b/algorithms/sorting/shell-sort/shellSort.ts @@ -1,16 +1,26 @@ +/** + * Sorts an array of numbers using the Shell Sort algorithm (in-place), + * with a predefined gap sequence (Ciura's sequence). + * + * @param items - The array of numbers to sort (modified in-place). + * @returns The same array, now sorted in ascending order. + */ const gaps = [701, 301, 132, 57, 23, 10, 4, 1]; -export function shellSort(items: Array): Array { +export function shellSort(items: number[]): number[] { for (let g = 0; g < gaps.length; g++) { const gap = gaps[g]; + // Perform a gapped insertion sort for (let i = gap; i < items.length; i++) { const tmp = items[i]; - let last = i; - for (let j = i; j >= gap && items[j - gap] > tmp; j -= gap) { + let j = i; + // Shift earlier gap-sorted elements up until the correct location is found + while (j >= gap && items[j - gap] > tmp) { items[j] = items[j - gap]; - last -= gap; + j -= gap; } - items[last] = tmp; + // Put tmp in its correct location + items[j] = tmp; } } return items; diff --git a/algorithms/sorting/types.ts b/algorithms/sorting/types.ts index 9e96a35..dbfc4c9 100644 --- a/algorithms/sorting/types.ts +++ b/algorithms/sorting/types.ts @@ -1,4 +1,4 @@ -export type CounterResults = { +export type SortCounters = { countOuter: number; countInner: number; countSwap: number; diff --git a/data-structures/binary-tree/binaryTreeNode.test.ts b/data-structures/binary-tree/binaryTree.test.ts similarity index 87% rename from data-structures/binary-tree/binaryTreeNode.test.ts rename to data-structures/binary-tree/binaryTree.test.ts index b580dbd..5ea0ead 100644 --- a/data-structures/binary-tree/binaryTreeNode.test.ts +++ b/data-structures/binary-tree/binaryTree.test.ts @@ -4,13 +4,12 @@ describe('BinaryTree', () => { let tree: BinaryTree; beforeEach(() => { - tree = new BinaryTree(); + tree = new BinaryTree((a, b) => a - b); }); test('should create an empty tree', () => { expect(tree.root).toBeNull(); }); - test('should insert values into the tree', () => { tree.insert(3); tree.insert(1); @@ -19,7 +18,6 @@ describe('BinaryTree', () => { expect(tree.root!.left!.value).toBe(1); expect(tree.root!.left!.right!.value).toBe(2); }); - test('should check if value is contained in the tree', () => { tree.insert(3); tree.insert(1); @@ -28,7 +26,6 @@ describe('BinaryTree', () => { expect(tree.contains(4)).toBe(true); expect(tree.contains(5)).toBe(false); }); - test('should traverse the tree in-order and produce sorted output', () => { const values: number[] = []; tree.insert(3); @@ -38,7 +35,6 @@ describe('BinaryTree', () => { tree.inOrderTraverse((value) => values.push(value)); expect(values).toEqual([1, 2, 3, 4]); }); - test('should remove a leaf node from the tree', () => { tree.insert(3); tree.insert(1); @@ -47,7 +43,6 @@ describe('BinaryTree', () => { expect(tree.contains(4)).toBe(false); expect(tree.root!.right).toBeNull(); }); - test('should remove a node with one child from the tree', () => { tree.insert(3); tree.insert(1); @@ -57,7 +52,6 @@ describe('BinaryTree', () => { expect(tree.contains(4)).toBe(false); expect(tree.root!.right!.value).toBe(5); }); - test('should remove a node with two children from the tree', () => { tree.insert(3); tree.insert(1); @@ -68,20 +62,27 @@ describe('BinaryTree', () => { expect(tree.contains(3)).toBe(false); expect(tree.root!.value).not.toBe(3); // The value should have been replaced by the inorder successor }); - test('should handle removal of the root node', () => { tree.insert(3); tree.remove(3); expect(tree.root).toBeNull(); expect(tree.contains(3)).toBe(false); }); + test('should insert values into the tree', () => { + tree.insert(3); + tree.insert(1); + tree.insert(2); + expect(tree.root!.value).toBe(3); + expect(tree.root!.left!.value).toBe(1); + expect(tree.root!.left!.right!.value).toBe(2); + }); }); describe('BinaryTree Performance Test', () => { let tree: BinaryTree; beforeEach(() => { - tree = new BinaryTree(); + tree = new BinaryTree((a, b) => a - b); }); test('should handle large number of inserts', () => { @@ -94,7 +95,6 @@ describe('BinaryTree Performance Test', () => { console.log(`Time taken to insert ${size} items: ${end - start}ms`); expect(end - start).toBeLessThan(500); }); - test('should handle large number of searches', () => { const size = 1000; for (let i = 0; i < size; i++) { @@ -106,6 +106,6 @@ describe('BinaryTree Performance Test', () => { } const end = performance.now(); console.log(`Time taken to search ${size} items: ${end - start}ms`); - expect(end - start).toBeLessThan(500); + expect(end - start).toBeLessThan(1000); }); }); diff --git a/data-structures/binary-tree/binaryTree.ts b/data-structures/binary-tree/binaryTree.ts index 2c95b8b..a31370b 100644 --- a/data-structures/binary-tree/binaryTree.ts +++ b/data-structures/binary-tree/binaryTree.ts @@ -1,10 +1,31 @@ -import {BinaryTreeNode} from './binaryTreeNode'; +/** + * A binary search tree node that holds a value and pointers to its left and right children. + */ +class BinaryTreeNode { + public value: T; + public left: BinaryTreeNode | null; + public right: BinaryTreeNode | null; + + constructor(value: T) { + this.value = value; + this.left = null; + this.right = null; + } +} /** * A binary search tree class with methods to insert, search, traverse, and remove nodes. */ export class BinaryTree { public root: BinaryTreeNode | null = null; + private compareFn: (a: T, b: T) => number; + + /** + * @param compareFn A comparator function that returns negative if ab. + */ + constructor(compareFn: (a: T, b: T) => number) { + this.compareFn = compareFn; + } /** * Inserts a new value into the binary search tree. @@ -20,18 +41,22 @@ export class BinaryTree { } private insertNode(node: BinaryTreeNode, newNode: BinaryTreeNode): void { - if (newNode.value < node.value) { + const cmp = this.compareFn(newNode.value, node.value); + if (cmp < 0) { if (node.left === null) { node.left = newNode; } else { this.insertNode(node.left, newNode); } - } else { + } else if (cmp > 0) { if (node.right === null) { node.right = newNode; } else { this.insertNode(node.right, newNode); } + } else { + // if compareFn returns 0 => values are equal; define your behavior (ignore, etc.) + // Here, we might do nothing (ignore duplicates) or handle them specially. } } @@ -48,10 +73,10 @@ export class BinaryTree { if (node === null) { return null; } - - if (value < node.value) { + const cmp = this.compareFn(value, node.value); + if (cmp < 0) { return this.search(node.left, value); - } else if (value > node.value) { + } else if (cmp > 0) { return this.search(node.right, value); } else { return node; @@ -60,15 +85,15 @@ export class BinaryTree { /** * Traverses the binary search tree in order (left, root, right). - * @param {function} visit Function to call on each value. + * @param visit A function to call on each value. */ inOrderTraverse(visit: (value: T) => void): void { - function traverse(node: BinaryTreeNode | null): void { - if (node === null) return; - traverse(node.left); - visit(node.value); - traverse(node.right); - } + const traverse = (n: BinaryTreeNode | null): void => { + if (n === null) return; + traverse(n.left); + visit(n.value); + traverse(n.right); + }; traverse(this.root); } @@ -84,20 +109,22 @@ export class BinaryTree { if (node === null) { return null; } - if (value < node.value) { + + const cmp = this.compareFn(value, node.value); + if (cmp < 0) { node.left = this.removeNode(node.left, value); return node; - } else if (value > node.value) { + } else if (cmp > 0) { node.right = this.removeNode(node.right, value); return node; } else { - // Node with only one child or no child + // found the node to remove if (node.left === null) { return node.right; } else if (node.right === null) { return node.left; } - // Node with two children: Get the inorder successor (smallest in the right subtree) + // two children: get min from right subtree node.value = this.findMinValue(node.right); node.right = this.removeNode(node.right, node.value); return node; @@ -106,9 +133,10 @@ export class BinaryTree { private findMinValue(node: BinaryTreeNode): T { let minv = node.value; - while (node.left !== null) { - minv = node.left.value; - node = node.left; + let current = node; + while (current.left !== null) { + minv = current.left.value; + current = current.left; } return minv; } diff --git a/data-structures/binary-tree/binaryTreeNode.ts b/data-structures/binary-tree/binaryTreeNode.ts deleted file mode 100644 index e9aeb99..0000000 --- a/data-structures/binary-tree/binaryTreeNode.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * A binary search tree node that holds a value and pointers to its left and right children. - */ -export class BinaryTreeNode { - public value: T; - public left: BinaryTreeNode | null; - public right: BinaryTreeNode | null; - - constructor(value: T) { - this.value = value; - this.left = null; - this.right = null; - } -} diff --git a/data-structures/graph/graph.test.ts b/data-structures/graph/graph.test.ts index faa149c..2289bb1 100644 --- a/data-structures/graph/graph.test.ts +++ b/data-structures/graph/graph.test.ts @@ -10,20 +10,17 @@ describe('Graph', () => { test('A new graph should be empty', () => { expect(graph.size).toBe(0); }); - test('Adding nodes increases graph size', () => { graph.addNode('A'); expect(graph.size).toBe(1); graph.addNode('B'); expect(graph.size).toBe(2); }); - test('Adding edges does not increase graph size', () => { graph.addNode('A').addNode('B'); graph.addEdge('A', 'B', 1); expect(graph.size).toBe(2); }); - test('Can remove nodes and edges', () => { graph.addNode('A').addNode('B'); graph.addEdge('A', 'B', 1); @@ -32,12 +29,10 @@ describe('Graph', () => { graph.removeNode('A'); expect(graph.size).toBe(1); }); - test('Correctly retrieves edge weights', () => { graph.addNode('A').addNode('B').addEdge('A', 'B', 1); expect(graph.getEdgeWeight('A', 'B')).toBe(1); }); - test('Correctly identifies adjacent nodes', () => { graph.addNode('A').addNode('B').addEdge('A', 'B', 1); expect(graph.isAdjacent('A', 'B')).toBe(true); @@ -61,7 +56,6 @@ describe('Graph Performance', () => { const end = performance.now(); expect(end - start).toBeLessThan(500); }); - test('Performance for adding 10,000 edges', () => { for (let i = 0; i < 10000; i++) { graph.addNode(i); diff --git a/data-structures/hash-table/hashTable.test.ts b/data-structures/hash-table/hashTable.test.ts index 1b2f30a..c7f6b1b 100644 --- a/data-structures/hash-table/hashTable.test.ts +++ b/data-structures/hash-table/hashTable.test.ts @@ -11,30 +11,25 @@ describe('HashTable', () => { hashTable.set('key1', 1); expect(hashTable.get('key1')).toBe(1); }); - test('updates value for an existing key', () => { hashTable.set('key1', 1); hashTable.set('key1', 2); expect(hashTable.get('key1')).toBe(2); }); - test('removes a key-value pair', () => { hashTable.set('key1', 1); expect(hashTable.remove('key1')).toBe(true); expect(hashTable.get('key1')).toBeUndefined(); }); - test('removes a non-existing key', () => { expect(hashTable.remove('key1')).toBe(false); }); - test('returns correct size', () => { expect(hashTable.getSize()).toBe(0); hashTable.set('key1', 1); hashTable.set('key2', 2); expect(hashTable.getSize()).toBe(2); }); - test('handles hash collisions', () => { const keyA = 'keyA'; const keyB = 'keyB'; @@ -48,32 +43,28 @@ describe('HashTable', () => { }); }); -describe('HashTable with large data set', () => { +describe('HashTable - large data set', () => { let hashTable: HashTable; const largeSize = 100000; beforeEach(() => { hashTable = new HashTable(largeSize); }); - test('handles a large number of inserts', () => { for (let i = 0; i < largeSize; i++) { hashTable.set(i, i); } expect(hashTable.getSize()).toBe(largeSize); }); - test('retrieves values correctly after a large number of inserts', () => { for (let i = 0; i < largeSize; i++) { hashTable.set(i, i * 2); } - for (let i = 0; i < 100; i++) { const key = Math.floor(Math.random() * largeSize); expect(hashTable.get(key)).toBe(key * 2); } }); - test('insert performance for large data sets', () => { const startTime = performance.now(); for (let i = 0; i < largeSize; i++) { @@ -83,3 +74,38 @@ describe('HashTable with large data set', () => { console.log(`Inserting ${largeSize} items took ${endTime - startTime} milliseconds`); }); }); + +describe('HashTable - small table to force collisions', () => { + let hashTable: HashTable; + + beforeEach(() => { + hashTable = new HashTable(5); // small table to force collisions + }); + + test('multiple collisions in the same bucket', () => { + // Force same hash index by controlling the hash function or using certain keys + // For demonstration, let's just assume 'a', 'b', 'c' collide in a small table + hashTable.set('a', 1); + hashTable.set('b', 2); + hashTable.set('c', 3); + + expect(hashTable.get('a')).toBe(1); + expect(hashTable.get('b')).toBe(2); + expect(hashTable.get('c')).toBe(3); + }); + test('updating an existing key in a collision bucket', () => { + hashTable.set('a', 1); + hashTable.set('b', 2); + hashTable.set('a', 99); // update "a" + expect(hashTable.get('a')).toBe(99); + expect(hashTable.get('b')).toBe(2); + }); + test('removing from a collision bucket', () => { + hashTable.set('a', 1); + hashTable.set('b', 2); + hashTable.set('c', 3); + hashTable.remove('b'); + expect(hashTable.get('b')).toBeUndefined(); + expect(hashTable.getSize()).toBe(2); + }); +}); diff --git a/data-structures/hash-table/hashTable.ts b/data-structures/hash-table/hashTable.ts index 9ba22c3..3363da3 100644 --- a/data-structures/hash-table/hashTable.ts +++ b/data-structures/hash-table/hashTable.ts @@ -6,6 +6,7 @@ export class HashTable { private table: Array<{key: K; value: V}[]>; private size: number; + private loadFactorThreshold = 0.75; /** * Constructs a hash table. @@ -23,14 +24,30 @@ export class HashTable { */ private hash(key: K): number { let hashValue = 0; - const stringTypeKey = `${key}`; - - for (let i = 0; i < stringTypeKey.length; i++) { - const charCode = stringTypeKey.charCodeAt(i); - hashValue += charCode << (i * 8); + const stringKey = `${key}`; + for (let i = 0; i < stringKey.length; i++) { + hashValue = (hashValue << 5) + hashValue + stringKey.charCodeAt(i); } + return Math.abs(hashValue) % this.table.length; + } + + /** + * Resizes the table when the load factor exceeds loadFactorThreshold. + */ + private resize(): void { + const newSize = this.table.length * 2; + const oldTable = this.table; + + this.table = new Array(newSize); + this.size = 0; - return hashValue % this.table.length; + for (const bucket of oldTable) { + if (bucket) { + for (const item of bucket) { + this.set(item.key, item.value); + } + } + } } /** @@ -52,6 +69,11 @@ export class HashTable { bucket.push({key, value}); this.table[index] = bucket; this.size++; + + // After insertion, check the load factor and resize if necessary. + if (this.size / this.table.length > this.loadFactorThreshold) { + this.resize(); + } } /** @@ -72,7 +94,6 @@ export class HashTable { return item.value; } } - return undefined; } @@ -96,7 +117,6 @@ export class HashTable { return true; } } - return false; } diff --git a/data-structures/heap/minHeap.test.ts b/data-structures/heap/minHeap.test.ts index 524d32c..970dd8e 100644 --- a/data-structures/heap/minHeap.test.ts +++ b/data-structures/heap/minHeap.test.ts @@ -11,7 +11,6 @@ describe('MinHeap', () => { expect(heap.size()).toBe(0); expect(heap.isEmpty()).toBe(true); }); - test('should insert elements into the heap', () => { heap.insert(2); expect(heap.size()).toBe(1); @@ -25,7 +24,6 @@ describe('MinHeap', () => { expect(heap.size()).toBe(3); expect(heap.peek()).toBe(1); }); - test('should extract the minimum element from the heap', () => { heap.insert(2); heap.insert(3); @@ -41,7 +39,6 @@ describe('MinHeap', () => { expect(heap.size()).toBe(0); expect(heap.isEmpty()).toBe(true); }); - test('should handle extract on an empty heap', () => { expect(heap.extract()).toBeUndefined(); }); @@ -57,7 +54,6 @@ describe('MinHeap with large data set', () => { test('should maintain heap property on extract', () => { let previous = heap.extract(); - for (let i = 1; i < largeSize; i++) { const current = heap.extract(); if (typeof current === 'number' && typeof previous === 'number') { @@ -67,7 +63,6 @@ describe('MinHeap with large data set', () => { } expect(heap.isEmpty()).toBe(true); }); - test('should handle insert and extract operations efficiently', () => { const start = performance.now(); while (!heap.isEmpty()) { diff --git a/data-structures/heap/minHeap.ts b/data-structures/heap/minHeap.ts index b16b54c..9d5a1f6 100644 --- a/data-structures/heap/minHeap.ts +++ b/data-structures/heap/minHeap.ts @@ -1,140 +1,155 @@ /** - * Represents a min heap data structure. - * @template T The type of elements in the heap. + * A simple MinHeap data structure for demonstration. + * @template T - The type of elements stored in the heap. */ export class MinHeap { private heap: T[]; private compareFn: (a: T, b: T) => number; + /** + * Creates a new MinHeap. + * @param compareFn - A comparator function that should return a negative number if `a < b`, + * zero if `a === b`, or a positive number if `a > b`. + */ constructor(compareFn: (a: T, b: T) => number) { this.heap = []; this.compareFn = compareFn; } /** - * Gets the index of the left child of the node at the given index. - * @param {number} parentIndex The index of the parent node. - * @return {number} The index of the left child. + * Returns the number of elements in the heap. + * @returns The number of elements. */ - private getLeftChildIndex(parentIndex: number): number { - return 2 * parentIndex + 1; + size(): number { + return this.heap.length; } /** - * Gets the index of the right child of the node at the given index. - * @param {number} parentIndex The index of the parent node. - * @return {number} The index of the right child. + * Checks if the heap is empty. + * @returns True if the heap is empty; otherwise, false. */ - private getRightChildIndex(parentIndex: number): number { - return 2 * parentIndex + 2; + isEmpty(): boolean { + return this.heap.length === 0; } /** - * Gets the index of the parent of the node at the given index. - * @param {number} childIndex The index of the child node. - * @return {number} The index of the parent node. + * Returns the minimum element without removing it. + * @returns The minimum element, or undefined if the heap is empty. */ - private getParentIndex(childIndex: number): number { - return Math.floor((childIndex - 1) / 2); + peek(): T | undefined { + return this.heap[0]; } /** - * Swaps the elements at the two given indexes. - * @param {number} index1 The index of the first element. - * @param {number} index2 The index of the second element. + * Inserts a new element into the heap. + * @param value - The element to insert. */ - private swap(index1: number, index2: number): void { - [this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]]; + insert(value: T): void { + this.heap.push(value); + this.siftUp(this.heap.length - 1); } /** - * Restores the min heap property by sifting the element at the end of the heap upwards. + * Removes and returns the minimum element from the heap. + * @returns The minimum element, or undefined if the heap is empty. */ - private siftUp(): void { - let index = this.heap.length - 1; - while ( - index > 0 && - this.compareFn(this.heap[this.getParentIndex(index)], this.heap[index]) > 0 - ) { - const parentIndex = this.getParentIndex(index); - this.swap(index, parentIndex); - index = parentIndex; + extract(): T | undefined { + if (this.isEmpty()) { + return undefined; } + + // The root (index 0) is the minimum element + const minValue = this.heap[0]; + // Move the last element to the root and pop it from the array + const lastValue = this.heap.pop(); + // If there's still at least one element left + if (!this.isEmpty() && lastValue !== undefined) { + this.heap[0] = lastValue; + this.siftDown(0); + } + return minValue; } /** - * Restores the min heap property by sifting the element at the root of the heap downwards. + * Moves the element at 'index' upwards until the min-heap property is satisfied. + * @param index - The index to sift up from. */ - private siftDown(): void { - let index = 0; - let smallerChildIndex = this.getLeftChildIndex(index); - - while ( - smallerChildIndex < this.heap.length && - this.compareFn(this.heap[index], this.heap[smallerChildIndex]) > 0 - ) { - const rightChildIndex = this.getRightChildIndex(index); - if ( - rightChildIndex < this.heap.length && - this.compareFn(this.heap[rightChildIndex], this.heap[smallerChildIndex]) < 0 - ) { - smallerChildIndex = rightChildIndex; + private siftUp(index: number): void { + while (index > 0) { + const parentIndex = this.getParentIndex(index); + // If the parent is greater than the current element, swap them + if (this.compareFn(this.heap[parentIndex], this.heap[index]) > 0) { + this.swap(parentIndex, index); + index = parentIndex; + } else { + break; } - - this.swap(index, smallerChildIndex); - index = smallerChildIndex; - smallerChildIndex = this.getLeftChildIndex(index); } } /** - * Inserts a new element into the heap. - * @param {T} value The element to insert. + * Moves the element at 'index' downwards until the min-heap property is satisfied. + * @param index - The index to sift down from. */ - insert(value: T): void { - this.heap.push(value); - this.siftUp(); + private siftDown(index: number): void { + const length = this.heap.length; + while (true) { + const leftIndex = this.getLeftChildIndex(index); + const rightIndex = this.getRightChildIndex(index); + let smallest = index; + + // Check if the left child is smaller + if (leftIndex < length && this.compareFn(this.heap[leftIndex], this.heap[smallest]) < 0) { + smallest = leftIndex; + } + // Check if the right child is even smaller + if (rightIndex < length && this.compareFn(this.heap[rightIndex], this.heap[smallest]) < 0) { + smallest = rightIndex; + } + + // If the current element is not the smallest, swap and continue + if (smallest !== index) { + this.swap(index, smallest); + index = smallest; + } else { + break; // The heap property is satisfied + } + } } /** - * Extracts and returns the minimum element from the heap. - * @return {T | undefined} The minimum element or undefined if the heap is empty. + * Gets the index of the left child of the node at the given index. + * @param parentIndex - The index of the parent node. + * @returns The index of the left child. */ - extract(): T | undefined { - if (this.heap.length === 0) { - return undefined; - } - - const minValue = this.heap[0]; - const lastValue = this.heap.pop(); - if (this.heap.length > 0 && lastValue !== undefined) { - this.heap[0] = lastValue; - this.siftDown(); - } - return minValue; + private getLeftChildIndex(parentIndex: number): number { + return 2 * parentIndex + 1; } /** - * Returns the minimum element from the heap without removing it. - * @return {T | undefined} The minimum element or undefined if the heap is empty. + * Gets the index of the right child of the node at the given index. + * @param parentIndex - The index of the parent node. + * @returns The index of the right child. */ - peek(): T | undefined { - return this.heap[0]; + private getRightChildIndex(parentIndex: number): number { + return 2 * parentIndex + 2; } /** - * Returns the number of elements in the heap. - * @return {number} The number of elements. + * Gets the index of the parent of the node at the given index. + * @param childIndex - The index of the child node. + * @returns The index of the parent node. */ - size(): number { - return this.heap.length; + private getParentIndex(childIndex: number): number { + return Math.floor((childIndex - 1) / 2); } /** - * Determines whether the heap is empty. - * @return {boolean} True if the heap is empty; otherwise, false. + * Swaps the elements at the two given indexes. + * @param i - The index of the first element. + * @param j - The index of the second element. */ - isEmpty(): boolean { - return this.size() === 0; + private swap(i: number, j: number): void { + [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]; } } diff --git a/data-structures/list-node/linkedList.test.ts b/data-structures/list-node/linkedList.test.ts index 43593fe..932165b 100644 --- a/data-structures/list-node/linkedList.test.ts +++ b/data-structures/list-node/linkedList.test.ts @@ -12,25 +12,21 @@ describe('LinkedList', () => { expect(linkedList.head).toBeNull(); expect(linkedList.tail).toBeNull(); }); - test('append adds a new element to the list', () => { linkedList.append(5); expect(linkedList.length).toBe(1); expect(linkedList.head?.value).toBe(5); expect(linkedList.tail?.value).toBe(5); }); - test('prepend adds a new element to the start of the list', () => { linkedList.prepend(1); expect(linkedList.length).toBe(1); expect(linkedList.head?.value).toBe(1); expect(linkedList.tail?.value).toBe(1); }); - test('find returns null for an empty list', () => { expect(linkedList.find(5)).toBeNull(); }); - test('find returns the correct node if it exists', () => { linkedList.append(1); linkedList.append(2); @@ -38,7 +34,6 @@ describe('LinkedList', () => { expect(node).not.toBeNull(); expect(node?.value).toBe(2); }); - test('delete removes the correct element from the list', () => { linkedList.append(1); linkedList.append(2); @@ -47,14 +42,12 @@ describe('LinkedList', () => { expect(linkedList.find(2)).toBeNull(); expect(linkedList.length).toBe(2); }); - test('toArray returns an array of all elements', () => { linkedList.append(1); linkedList.append(2); linkedList.append(3); expect(linkedList.toArray()).toEqual([1, 2, 3]); }); - test('handles a mix of operations', () => { linkedList.append(1); linkedList.append(2); diff --git a/data-structures/list-node/linkedList.ts b/data-structures/list-node/linkedList.ts index 17a991a..8112474 100644 --- a/data-structures/list-node/linkedList.ts +++ b/data-structures/list-node/linkedList.ts @@ -1,4 +1,19 @@ -import {ListNode} from './listNode'; +/** + * Class representing a single node in a linked list. + * @template T - The type of the value. + */ +class ListNode { + public value: T; + public next: ListNode | null = null; + + /** + * Creates a list node. + * @param {T} value - The value stored in the node. + */ + constructor(value: T) { + this.value = value; + } +} /** * Class representing a singly linked list. @@ -61,23 +76,28 @@ export class LinkedList { */ delete(value: T): void { if (!this.head) return; - + // If the head is the target for deletion, if (this.head.value === value) { - this.head = this.head.next; if (this.head === this.tail) { + this.head = null; this.tail = null; + this._length--; + return; + } else { + // Delete the head and make the second one the new head. + this.head = this.head.next; + this._length--; + return; } - this._length--; - return; } - + // Other than the first. let currentNode = this.head; while (currentNode.next) { if (currentNode.next.value === value) { - currentNode.next = currentNode.next.next; - if (currentNode.next === null) { + if (currentNode.next === this.tail) { this.tail = currentNode; } + currentNode.next = currentNode.next.next; this._length--; return; } @@ -91,16 +111,13 @@ export class LinkedList { * @return {ListNode | null} The found node or null if not found. */ find(value: T): ListNode | null { - if (!this.head) return null; - - let currentNode: ListNode | null = this.head; + let currentNode = this.head; while (currentNode) { if (currentNode.value === value) { return currentNode; } currentNode = currentNode.next; } - return null; } diff --git a/data-structures/list-node/listNode.ts b/data-structures/list-node/listNode.ts deleted file mode 100644 index 1cf8697..0000000 --- a/data-structures/list-node/listNode.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Class representing a single node in a linked list. - * @template T - The type of the value. - */ -export class ListNode { - public value: T; - public next: ListNode | null = null; - - /** - * Creates a list node. - * @param {T} value - The value stored in the node. - */ - constructor(value: T) { - this.value = value; - } -} diff --git a/data-structures/queue/circularQueue.test.ts b/data-structures/queue/circularQueue.test.ts new file mode 100644 index 0000000..ad10f95 --- /dev/null +++ b/data-structures/queue/circularQueue.test.ts @@ -0,0 +1,100 @@ +import {CircularQueue} from './circularQueue'; + +describe('CircularQueue', () => { + test('should throw an error if constructed with capacity <= 0', () => { + expect(() => new CircularQueue(0)).toThrow(); + expect(() => new CircularQueue(-1)).toThrow(); + }); + test('should create an empty queue with given capacity', () => { + const queue = new CircularQueue(5); + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBe(true); + expect(queue.isFull()).toBe(false); + }); + test('should enqueue items until full', () => { + const capacity = 3; + const queue = new CircularQueue(capacity); + queue.enqueue(1); + queue.enqueue(2); + expect(queue.size()).toBe(2); + expect(queue.isFull()).toBe(false); + queue.enqueue(3); + expect(queue.size()).toBe(capacity); + expect(queue.isFull()).toBe(true); + }); + test('should throw error when enqueue on a full queue', () => { + const capacity = 2; + const queue = new CircularQueue(capacity); + queue.enqueue(10); + queue.enqueue(20); + expect(() => queue.enqueue(30)).toThrow('Queue is full'); + }); + test('should dequeue items in correct order', () => { + const queue = new CircularQueue(3); + queue.enqueue(10); + queue.enqueue(20); + queue.enqueue(30); + expect(queue.dequeue()).toBe(10); + expect(queue.dequeue()).toBe(20); + expect(queue.dequeue()).toBe(30); + // now queue should be empty + expect(queue.dequeue()).toBeUndefined(); + expect(queue.isEmpty()).toBe(true); + }); + test('should allow enqueue/dequeue in a circular manner', () => { + const queue = new CircularQueue(3); + queue.enqueue(1); + queue.enqueue(2); + queue.enqueue(3); + expect(queue.isFull()).toBe(true); + // remove one element + expect(queue.dequeue()).toBe(1); + expect(queue.isFull()).toBe(false); + // now we can enqueue again + queue.enqueue(4); + // queue should be [2,3,4], in circular form + expect(queue.isFull()).toBe(true); + expect(queue.dequeue()).toBe(2); + expect(queue.dequeue()).toBe(3); + expect(queue.dequeue()).toBe(4); + expect(queue.isEmpty()).toBe(true); + }); + test('should return undefined when dequeue on empty queue', () => { + const queue = new CircularQueue(3); + expect(queue.dequeue()).toBeUndefined(); + }); + test('should return the front element with peek()', () => { + const queue = new CircularQueue(3); + queue.enqueue('apple'); + queue.enqueue('banana'); + expect(queue.peek()).toBe('apple'); + expect(queue.size()).toBe(2); + expect(queue.dequeue()).toBe('apple'); + expect(queue.peek()).toBe('banana'); + }); + test('should clear all elements', () => { + const queue = new CircularQueue(4); + queue.enqueue(10); + queue.enqueue(20); + queue.enqueue(30); + queue.clear(); + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBe(true); + expect(queue.dequeue()).toBeUndefined(); + }); + test('should handle large enqueue/dequeue sequences without performance issues', () => { + const queue = new CircularQueue(100000); + const dataSize = 50000; + + for (let i = 0; i < dataSize; i++) { + queue.enqueue(i); + } + expect(queue.size()).toBe(dataSize); + + for (let i = 0; i < dataSize; i++) { + const item = queue.dequeue(); + expect(item).toBe(i); + } + expect(queue.isEmpty()).toBe(true); + }); +}); diff --git a/data-structures/queue/circularQueue.ts b/data-structures/queue/circularQueue.ts new file mode 100644 index 0000000..dcc3f13 --- /dev/null +++ b/data-structures/queue/circularQueue.ts @@ -0,0 +1,101 @@ +/** + * A fixed-size queue implemented using a circular buffer. + * Enqueue and dequeue operations run in O(1) time. + * @template T The type of elements held in the queue. + */ +export class CircularQueue { + private buffer: Array; + private head: number; + private tail: number; + private count: number; + private readonly capacity: number; + + /** + * Creates a new circular queue with the specified capacity. + * @param {number} capacity - The maximum number of elements this queue can hold. + */ + constructor(capacity: number) { + if (capacity <= 0) { + throw new Error('Capacity must be greater than 0'); + } + this.capacity = capacity; + this.buffer = new Array(capacity).fill(null); + this.head = 0; + this.tail = 0; + this.count = 0; + } + + /** + * Returns the current number of elements in the queue. + * @return {number} The size of the queue. + */ + size(): number { + return this.count; + } + + /** + * Checks if the queue is empty. + * @return {boolean} True if the queue is empty; otherwise, false. + */ + isEmpty(): boolean { + return this.count === 0; + } + + /** + * Checks if the queue is full (i.e. has reached its capacity). + * @return {boolean} True if the queue is full; otherwise, false. + */ + isFull(): boolean { + return this.count === this.capacity; + } + + /** + * Adds an element to the back of the queue in O(1) time. + * @param {T} item - The element to be enqueued. + * @throws {Error} If the queue is already full. + */ + enqueue(item: T): void { + if (this.isFull()) { + throw new Error('Queue is full'); + } + this.buffer[this.tail] = item; + this.tail = (this.tail + 1) % this.capacity; + this.count++; + } + + /** + * Removes and returns the front element of the queue in O(1) time. + * @return {T | undefined} The front element, or undefined if the queue is empty. + */ + dequeue(): T | undefined { + if (this.isEmpty()) { + return undefined; + } + const frontItem = this.buffer[this.head] as T; + this.buffer[this.head] = null; // not strictly required, but can help with debugging/garbage collection + this.head = (this.head + 1) % this.capacity; + this.count--; + return frontItem; + } + + /** + * Returns the front element without removing it. + * @return {T | undefined} The front element, or undefined if the queue is empty. + */ + peek(): T | undefined { + if (this.isEmpty()) { + return undefined; + } + return this.buffer[this.head] as T; + } + + /** + * Removes all elements from the queue. + */ + clear(): void { + this.buffer.fill(null); + this.head = 0; + this.tail = 0; + this.count = 0; + } +} diff --git a/data-structures/queue/queue.test.ts b/data-structures/queue/queue.test.ts index d86dfaa..314b9a4 100644 --- a/data-structures/queue/queue.test.ts +++ b/data-structures/queue/queue.test.ts @@ -11,7 +11,6 @@ describe('Queue', () => { queue.enqueue(1); expect(queue.size()).toBe(1); }); - test('dequeue should remove and return the first item', () => { queue.enqueue(1); queue.enqueue(2); @@ -19,7 +18,6 @@ describe('Queue', () => { expect(item).toBe(1); expect(queue.size()).toBe(1); }); - test('peek should return the first item without removing it', () => { queue.enqueue(1); queue.enqueue(2); @@ -27,31 +25,25 @@ describe('Queue', () => { expect(item).toBe(1); expect(queue.size()).toBe(2); }); - test('isEmpty should return true for a new queue', () => { expect(queue.isEmpty()).toBeTruthy(); }); - test('isEmpty should return false for a queue with items', () => { queue.enqueue(1); expect(queue.isEmpty()).toBeFalsy(); }); - test('isFull should return false for a new queue', () => { expect(queue.isFull()).toBeFalsy(); }); - test('isFull should return true for a queue that has reached capacity', () => { const limitedQueue = new Queue(1); limitedQueue.enqueue(1); expect(limitedQueue.isFull()).toBeTruthy(); }); - test('dequeue should return undefined when called on an empty queue', () => { const item = queue.dequeue(); expect(item).toBeUndefined(); }); - test('enqueue should throw an error when trying to add items beyond capacity', () => { const limitedQueue = new Queue(1); limitedQueue.enqueue(1); @@ -59,7 +51,6 @@ describe('Queue', () => { limitedQueue.enqueue(2); }).toThrow('Queue is full'); }); - test('clear should remove all items from the queue', () => { queue.enqueue(1); queue.enqueue(2); @@ -67,7 +58,6 @@ describe('Queue', () => { expect(queue.size()).toBe(0); expect(queue.isEmpty()).toBeTruthy(); }); - test('enqueue performance for 10000 items', () => { const start = performance.now(); for (let i = 0; i < 10000; i++) { @@ -77,12 +67,10 @@ describe('Queue', () => { console.log(`Time taken to enqueue 10000 items: ${end - start}ms`); expect(end - start).toBeLessThan(100); }); - test('dequeue performance for 10000 items', () => { for (let i = 0; i < 10000; i++) { queue.enqueue(i); } - const start = performance.now(); while (!queue.isEmpty()) { queue.dequeue(); diff --git a/data-structures/stack/stack.test.ts b/data-structures/stack/stack.test.ts index 1efb6c2..90a6296 100644 --- a/data-structures/stack/stack.test.ts +++ b/data-structures/stack/stack.test.ts @@ -7,79 +7,79 @@ describe('Stack', () => { stack = new Stack(); }); - test('new stack should be empty', () => { + test('should create a new stack that is empty', () => { expect(stack.isEmpty()).toBe(true); + expect(stack.size()).toBe(0); }); - - test('push should add an item to the stack', () => { + test('should push an item onto the stack', () => { stack.push(1); expect(stack.size()).toBe(1); + expect(stack.peek()).toBe(1); }); - - test('push should add items in LIFO order', () => { + test('should push items in LIFO order', () => { stack.push(1); stack.push(2); - expect(stack.peek()).toBe(2); + expect(stack.peek()).toBe(2); // The last pushed item is on top }); - - test('pop should remove the last item', () => { + test('should pop the top item', () => { stack.push(1); stack.push(2); - const item = stack.pop(); - expect(item).toBe(2); + const popped = stack.pop(); + expect(popped).toBe(2); expect(stack.size()).toBe(1); + expect(stack.peek()).toBe(1); }); - - test('pop should return undefined for an empty stack', () => { + test('should return undefined when popping from an empty stack', () => { expect(stack.pop()).toBeUndefined(); }); - - test('peek should return the last item without removing it', () => { + test('should peek the top item without removing it', () => { stack.push(1); - expect(stack.peek()).toBe(1); - expect(stack.size()).toBe(1); + const top = stack.peek(); + expect(top).toBe(1); + expect(stack.size()).toBe(1); // Size unchanged }); - - test('size should return the number of items in the stack', () => { + test('should return the current size of the stack', () => { stack.push(1); stack.push(2); expect(stack.size()).toBe(2); }); + test('should push and pop items up to capacity', () => { + const capacityStack = new Stack(3); + capacityStack.push(10); + capacityStack.push(20); + expect(capacityStack.size()).toBe(2); + expect(capacityStack.peek()).toBe(20); - test('isEmpty should return false for stack with items', () => { - stack.push(1); - expect(stack.isEmpty()).toBe(false); + const popped = capacityStack.pop(); + expect(popped).toBe(20); + expect(capacityStack.peek()).toBe(10); + expect(capacityStack.size()).toBe(1); }); - - test('isFull should return true when stack reaches capacity', () => { + test('should throw an error when pushing beyond capacity', () => { const limitedStack = new Stack(2); limitedStack.push(1); limitedStack.push(2); - expect(limitedStack.isFull()).toBe(true); + expect(() => limitedStack.push(3)).toThrow('Stack is full'); }); - - test('push should throw an error when trying to add items beyond capacity', () => { - const limitedStack = new Stack(1); - limitedStack.push(1); - expect(() => { - limitedStack.push(2); - }).toThrow('Stack overflow'); + test('should clear the stack', () => { + const anotherStack = new Stack(5); + anotherStack.push(10); + anotherStack.push(20); + anotherStack.clear(); + expect(anotherStack.isEmpty()).toBe(true); + expect(anotherStack.peek()).toBeUndefined(); }); - - test('clear should remove all items from the stack', () => { - stack.push(1); - stack.push(2); - stack.clear(); - expect(stack.isEmpty()).toBe(true); + test('should throw a RangeError if capacity is <= 0', () => { + expect(() => new Stack(0)).toThrow(RangeError); + expect(() => new Stack(-1)).toThrow(RangeError); }); - - test('push performance for 10000 items', () => { + test('should push 10000 items efficiently', () => { const start = performance.now(); for (let i = 0; i < 10000; i++) { stack.push(i); } const end = performance.now(); console.log(`Time taken to push 10000 items: ${end - start}ms`); - expect(end - start).toBeLessThan(10); + expect(end - start).toBeLessThan(100); }); }); diff --git a/data-structures/stack/stack.ts b/data-structures/stack/stack.ts index 94a82a0..ef7c9b9 100644 --- a/data-structures/stack/stack.ts +++ b/data-structures/stack/stack.ts @@ -1,27 +1,38 @@ /** - * Class representing a stack data structure. + * Represents a stack data structure with an optional capacity limit. * @template T - The type of elements held in the stack. */ export class Stack { private storage: T[] = []; + private readonly capacity: number; - constructor(private capacity: number = Infinity) {} + /** + * Creates a new stack. + * @param capacity - The maximum number of items the stack can hold (default: Infinity). + * @throws {RangeError} if capacity <= 0. + */ + constructor(capacity: number = Infinity) { + if (capacity <= 0) { + throw new RangeError('Stack capacity must be greater than 0.'); + } + this.capacity = capacity; + } /** * Adds an element to the top of the stack. - * @param {T} item - The item to be added to the stack. + * @param item - The item to be added. * @throws {Error} if the stack has reached its capacity limit. */ push(item: T): void { if (this.size() === this.capacity) { - throw new Error('Stack overflow'); + throw new Error('Stack is full'); } this.storage.push(item); } /** * Removes the element from the top of the stack and returns it. - * @return {T | undefined} The element at the top of the stack or undefined if the stack is empty. + * @returns The popped element, or undefined if the stack is empty. */ pop(): T | undefined { return this.storage.pop(); @@ -29,15 +40,15 @@ export class Stack { /** * Returns the element at the top of the stack without removing it. - * @return {T | undefined} The element at the top of the stack or undefined if the stack is empty. + * @returns The top element, or undefined if the stack is empty. */ peek(): T | undefined { - return this.storage[this.size() - 1]; + return this.storage[this.storage.length - 1]; } /** * Returns the number of elements in the stack. - * @return {number} The size of the stack. + * @returns The size of the stack. */ size(): number { return this.storage.length; @@ -45,22 +56,22 @@ export class Stack { /** * Checks if the stack is empty. - * @return {boolean} True if the stack is empty, false otherwise. + * @returns True if the stack is empty, false otherwise. */ isEmpty(): boolean { - return this.size() === 0; + return this.storage.length === 0; } /** - * Checks if the stack has reached its capacity. - * @return {boolean} True if the stack is full, false otherwise. + * Checks if the stack has reached its capacity limit. + * @returns True if the stack is full, false otherwise. */ isFull(): boolean { return this.size() === this.capacity; } /** - * Empties the stack of all elements. + * Removes all elements from the stack. */ clear(): void { this.storage = []; diff --git a/math/eulerTotient/eulerTotient.test.ts b/math/eulerTotient/eulerTotient.test.ts new file mode 100644 index 0000000..e244de2 --- /dev/null +++ b/math/eulerTotient/eulerTotient.test.ts @@ -0,0 +1,39 @@ +import {eulerTotient} from './eulerTotient'; + +describe('eulerTotient', () => { + test('n <= 0 => returns 0', () => { + expect(eulerTotient(0)).toBe(0); + expect(eulerTotient(-1)).toBe(0); + expect(eulerTotient(-100)).toBe(0); + }); + test('φ(1) = 1', () => { + expect(eulerTotient(1)).toBe(1); + }); + test('φ(2) = 1', () => { + expect(eulerTotient(2)).toBe(1); + }); + test('φ(3) = 2', () => { + expect(eulerTotient(3)).toBe(2); + }); + test('φ(4) = 2', () => { + expect(eulerTotient(4)).toBe(2); + }); + test('φ(6) = 2', () => { + expect(eulerTotient(6)).toBe(2); + }); + test('φ(10) = 4', () => { + expect(eulerTotient(10)).toBe(4); + }); + test('φ(12) = 4', () => { + expect(eulerTotient(12)).toBe(4); + }); + test('φ(36) = 12', () => { + expect(eulerTotient(36)).toBe(12); + }); + test('φ(100) = 40', () => { + expect(eulerTotient(100)).toBe(40); + }); + test('φ(9973)', () => { + expect(eulerTotient(9973)).toBe(9972); + }); +}); diff --git a/math/eulerTotient/eulerTotient.ts b/math/eulerTotient/eulerTotient.ts new file mode 100644 index 0000000..f7dacb0 --- /dev/null +++ b/math/eulerTotient/eulerTotient.ts @@ -0,0 +1,38 @@ +/** + * Calculates Euler's Totient Function φ(n). + * + * The Euler's Totient Function φ(n) is the number of positive integers up to n that are relatively prime to n. + * If n = p1^a1 * p2^a2 * ... * pk^ak (prime factorization), + * then φ(n) = n * (1 - 1/p1) * (1 - 1/p2) * ... * (1 - 1/pk). + * + * @param {number} n - The integer for which to calculate φ(n). If n <= 0, returns 0. + * @returns {number} The value of the Euler's Totient Function for n. + */ +export function eulerTotient(n: number): number { + if (n <= 0) return 0; + if (n === 1) return 1; + + let result = n; + let temp = n; + // As long as it can be divided by 2, remove factor 2 first. + if (temp % 2 === 0) { + result -= result / 2; + while (temp % 2 === 0) { + temp /= 2; + } + } + // Check for odd factors starting from 3. + for (let i = 3; i * i <= temp; i += 2) { + if (temp % i === 0) { + result -= result / i; + while (temp % i === 0) { + temp /= i; + } + } + } + // If the last remaining number is greater than 1, it is a prime factor. + if (temp > 1) { + result -= result / temp; + } + return Math.floor(result); +} diff --git a/math/extendedGcd/extendedGcd.test.ts b/math/extendedGcd/extendedGcd.test.ts new file mode 100644 index 0000000..ebe974e --- /dev/null +++ b/math/extendedGcd/extendedGcd.test.ts @@ -0,0 +1,52 @@ +import {extendedGcd, modInverse} from './extendedGcd'; + +describe('extendedGcd', () => { + test('gcd(a, 0) => |a|', () => { + expect(extendedGcd(5, 0)).toEqual([5, 1, 0]); + expect(extendedGcd(-5, 0)).toEqual([5, -1, 0]); + }); + test('basic checks for gcd and coefficients', () => { + const [g1, x1, y1] = extendedGcd(15, 6); + expect(g1).toBe(3); + expect(15 * x1 + 6 * y1).toBe(3); + + const [g2, x2, y2] = extendedGcd(6, 15); + expect(g2).toBe(3); + expect(6 * x2 + 15 * y2).toBe(3); + }); + test('extendedGcd with negative inputs', () => { + const [g3, x3, y3] = extendedGcd(-9, 6); + expect(g3).toBe(3); + expect(-9 * x3 + 6 * y3).toBe(3); + + const [g4, x4, y4] = extendedGcd(9, -6); + expect(g4).toBe(3); + expect(9 * x4 + -6 * y4).toBe(3); + }); +}); + +describe('modInverse', () => { + test('inverse of a = 1 mod m => should throw if gcd != 1', () => { + expect(() => modInverse(6, 9)).toThrow(); + }); + test('modInverse(3, 7)', () => { + const inv = modInverse(3, 7); + expect(inv).toBe(5); + expect((3 * inv) % 7).toBe(1); + }); + test('modInverse(10, 17)', () => { + const inv = modInverse(10, 17); + expect(inv).toBe(12); + expect((10 * inv) % 17).toBe(1); + }); + test('modInverse(3, 11)', () => { + const inv = modInverse(3, 11); + expect(inv).toBe(4); + expect((3 * inv) % 11).toBe(1); + }); + test('modInverse(100, 3)', () => { + const inv = modInverse(100, 3); + expect(inv).toBe(1); + expect((100 * inv) % 3).toBe(1); + }); +}); diff --git a/math/extendedGcd/extendedGcd.ts b/math/extendedGcd/extendedGcd.ts new file mode 100644 index 0000000..d9ee508 --- /dev/null +++ b/math/extendedGcd/extendedGcd.ts @@ -0,0 +1,45 @@ +/** + * Extended Euclidean Algorithm + * Returns [g, x, y] where g = gcd(a, b) and a*x + b*y = g. + * + * @param a The first number + * @param b The second number + * @returns [g, x, y] where g = gcd(a, b) and a*x + b*y = g + */ +export function extendedGcd(a: number, b: number): [number, number, number] { + if (b === 0) { + // gcd(a, 0) = |a| + // a*x + b*y = a*(a<0?-1:1) + 0*0 => ±a + return [Math.abs(a), a < 0 ? -1 : 1, 0]; + } + + const [g, x1, y1] = extendedGcd(b, a % b); + + // Truncation-based quotient + const q = Math.trunc(a / b); + + // x = y1 + // y = x1 - q * y1 + const x = y1; + const y = x1 - q * y1; + + return [g, x, y]; +} + +/** + * Compute modular inverse of a under modulo m (assuming gcd(a,m)=1). + * a^{-1} s.t. a * a^{-1} ≡ 1 (mod m) + * + * @param a The number whose inverse is to be found + * @param m The modulo + * @returns The modular inverse of a under modulo m + */ +export function modInverse(a: number, m: number): number { + const [g, x] = extendedGcd(a, m); + if (g !== 1) { + // If extendedGcd(a, m) is not 1, then the inverse does not exist. + throw new Error(`${a} and ${m} are not co-prime, so inverse doesn't exist.`); + } + // Normalize x to the range [0, m-1] and return it. + return ((x % m) + m) % m; +} diff --git a/math/fibMatrix/fibMatrix.test.ts b/math/fibMatrix/fibMatrix.test.ts new file mode 100644 index 0000000..fbd3c45 --- /dev/null +++ b/math/fibMatrix/fibMatrix.test.ts @@ -0,0 +1,25 @@ +import {fibMatrix} from './fibMatrix'; + +describe('fibMatrix', () => { + test('F(0) = 0', () => { + expect(fibMatrix(0)).toBe(0); + }); + test('F(1) = 1', () => { + expect(fibMatrix(1)).toBe(1); + }); + test('F(2) = 1', () => { + expect(fibMatrix(2)).toBe(1); + }); + test('F(5) = 5', () => { + expect(fibMatrix(5)).toBe(5); + }); + test('F(10) = 55', () => { + expect(fibMatrix(10)).toBe(55); + }); + test('F(20) = 6765', () => { + expect(fibMatrix(20)).toBe(6765); + }); + test('should throw on negative input', () => { + expect(() => fibMatrix(-1)).toThrow(RangeError); + }); +}); diff --git a/math/fibMatrix/fibMatrix.ts b/math/fibMatrix/fibMatrix.ts new file mode 100644 index 0000000..aeafe59 --- /dev/null +++ b/math/fibMatrix/fibMatrix.ts @@ -0,0 +1,72 @@ +/** + * Represents a 2x2 matrix of numbers. + */ +type Matrix2x2 = [[number, number], [number, number]]; + +/** + * Multiplies two 2x2 matrices (a and b). + * @param {Matrix2x2} a - The first 2x2 matrix. + * @param {Matrix2x2} b - The second 2x2 matrix. + * @returns {Matrix2x2} The resulting 2x2 matrix after multiplication. + */ +function multiplyMatrix2x2(a: Matrix2x2, b: Matrix2x2): Matrix2x2 { + return [ + [a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1]], + [a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]], + ]; +} + +/** + * Raises a 2x2 matrix A to the power n using exponentiation by squaring. + * @param {Matrix2x2} A - The base 2x2 matrix. + * @param {number} n - The exponent (non-negative integer). + * @returns {Matrix2x2} The result of A^n. + */ +function matrixPower(A: Matrix2x2, n: number): Matrix2x2 { + // Identity matrix + let result: Matrix2x2 = [ + [1, 0], + [0, 1], + ]; + let base = A; + + let e = n; + while (e > 0) { + if (e & 1) { + result = multiplyMatrix2x2(result, base); + } + base = multiplyMatrix2x2(base, base); + e >>= 1; + } + + return result; +} + +/** + * Computes the n-th Fibonacci number using matrix exponentiation. + * F(0) = 0, F(1) = 1, and F(n) = F(n-1) + F(n-2). + * @param {number} n - The index of the Fibonacci sequence (non-negative integer). + * @returns {number} The n-th Fibonacci number. + */ +export function fibMatrix(n: number): number { + if (n < 0) { + throw new RangeError('fibMatrix: n must be a non-negative integer.'); + } + if (n < 2) { + return n; + } + + // The standard Fibonacci transformation matrix: + // | 1 1 | + // | 1 0 | + const F: Matrix2x2 = [ + [1, 1], + [1, 0], + ]; + + // F^(n-1) multiplied by the initial vector [F(1), F(0)]^T = [1, 0]^T + // effectively gives us F(n). + // But we only need the top-left element [0][0] of F^(n-1]. + const F_n = matrixPower(F, n - 1); + return F_n[0][0]; +} diff --git a/math/gcd/gcb.test.ts b/math/gcd/gcb.test.ts index eaf86ce..4e138cf 100644 --- a/math/gcd/gcb.test.ts +++ b/math/gcd/gcb.test.ts @@ -1,16 +1,28 @@ import {gcd} from './gcd'; -describe('gcd', () => { - test('should return 2 when passed 4 and 6', () => { - expect(gcd(4, 6)).toBe(2); +describe('gcd function', () => { + test('gcd(0, 0) should return throw', () => { + expect(() => gcd(0, 0)).toThrow(); }); - test('should return 6 when passed 30 and 42', () => { - expect(gcd(30, 42)).toBe(6); + test('gcd(0, 5) = 5', () => { + expect(gcd(0, 5)).toBe(5); }); - test('should return 4 when passed -8 and 12', () => { - expect(gcd(-8, 12)).toBe(4); + test('gcd(5, 0) = 5', () => { + expect(gcd(5, 0)).toBe(5); }); - test('should return 12 when passed 0 and 12', () => { - expect(gcd(0, 12)).toBe(12); + test('gcd(1, 1) = 1', () => { + expect(gcd(1, 1)).toBe(1); + }); + test('gcd(12, 4) = 4', () => { + expect(gcd(12, 4)).toBe(4); + }); + test('gcd(18, 24) = 6', () => { + expect(gcd(18, 24)).toBe(6); + }); + test('gcd(-6, 15) = 3 (if we handle negatives as absolute)', () => { + expect(gcd(-6, 15)).toBe(3); + }); + test('gcd(18, -24) = 6 (if we handle negatives as absolute)', () => { + expect(gcd(18, -24)).toBe(6); }); }); diff --git a/math/gcd/gcd.ts b/math/gcd/gcd.ts index 1d0629a..08435e7 100644 --- a/math/gcd/gcd.ts +++ b/math/gcd/gcd.ts @@ -2,9 +2,12 @@ * Calculate the greatest common divisor (GCD) of two integers. * @param {number} a - The first integer * @param {number} b - The second integer - * @returns {number} The greatest common divisor of a and b + * @returns {number} The greatest common divisor of a and b (non-negative) */ export function gcd(a: number, b: number): number { + if (a === 0 && b === 0) { + throw new Error('gcd(0,0) is undefined'); + } if (b === 0) { return Math.abs(a); } diff --git a/math/modPow/modPow.test.ts b/math/modPow/modPow.test.ts new file mode 100644 index 0000000..99a0ac5 --- /dev/null +++ b/math/modPow/modPow.test.ts @@ -0,0 +1,28 @@ +import {modPow} from './modPow'; + +describe('modPow function', () => { + test('base^0 => always 1 (mod m) when m > 1', () => { + expect(modPow(2, 0, 5)).toBe(1); + expect(modPow(10, 0, 7)).toBe(1); + }); + test('modulus=1 => result is 0', () => { + expect(modPow(5, 10, 1)).toBe(0); + }); + test('simple cases', () => { + expect(modPow(2, 3, 5)).toBe(3); + expect(modPow(3, 3, 13)).toBe(1); + }); + test('larger exponent', () => { + // 2^10=1024 => 1024 % 7=2 + expect(modPow(2, 10, 7)).toBe(2); + // 3^13=1594323 => 1594323 % 100=23 + expect(modPow(3, 13, 100)).toBe(23); + }); + test('should handle negative exponent if we disallow it', () => { + expect(() => modPow(2, -1, 5)).toThrow(RangeError); + }); + test('should handle zero or negative modulus if we disallow it', () => { + expect(() => modPow(2, 3, 0)).toThrow(RangeError); + expect(() => modPow(2, 3, -5)).toThrow(RangeError); + }); +}); diff --git a/math/modPow/modPow.ts b/math/modPow/modPow.ts new file mode 100644 index 0000000..69c8d07 --- /dev/null +++ b/math/modPow/modPow.ts @@ -0,0 +1,32 @@ +/** + * Computes (base^exponent) mod modulus using exponentiation by squaring. + * @param {number} base - The base integer. + * @param {number} exponent - The exponent (non-negative integer). + * @param {number} modulus - The modulus (positive integer). + * @returns {number} (base^exponent) % modulus + * @throws {RangeError} If exponent < 0 or modulus <= 0. + */ +export function modPow(base: number, exponent: number, modulus: number): number { + if (exponent < 0) { + throw new RangeError('exponent must be a non-negative integer.'); + } + if (modulus <= 0) { + throw new RangeError('modulus must be a positive integer.'); + } + if (modulus === 1) { + return 0; + } + + let result = 1; + let current = base % modulus; + let e = exponent; + + while (e > 0) { + if (e & 1) { + result = (result * current) % modulus; + } + current = (current * current) % modulus; + e >>= 1; + } + return result; +} diff --git a/math/primeFactors/primeFactors.test.ts b/math/primeFactors/primeFactors.test.ts index 9678064..a7d5fc4 100644 --- a/math/primeFactors/primeFactors.test.ts +++ b/math/primeFactors/primeFactors.test.ts @@ -1,19 +1,25 @@ import {primeFactors} from './primeFactors'; describe('primeFactors', () => { - test('should return an empty array when passed 1', () => { - expect(primeFactors(1)).toEqual([]); + test('should return throw', () => { + expect(() => primeFactors(1)).toThrow(); }); - test('should return an array containing 2 and 2 when passed 4', () => { + test('primeFactors(2) => [2]', () => { + expect(primeFactors(2)).toEqual([2]); + }); + test('primeFactors(3) => [3]', () => { + expect(primeFactors(3)).toEqual([3]); + }); + test('primeFactors(4) => [2, 2]', () => { expect(primeFactors(4)).toEqual([2, 2]); }); - test('should return an array containing 2, 3, and 5 when passed 30', () => { - expect(primeFactors(30)).toEqual([2, 3, 5]); + test('primeFactors(18) => [2, 3, 3]', () => { + expect(primeFactors(18)).toEqual([2, 3, 3]); }); - test('should return an array containing 2, 2, 2, and 3 when passed 24', () => { - expect(primeFactors(24)).toEqual([2, 2, 2, 3]); + test('primeFactors(100) => [2, 2, 5, 5]', () => { + expect(primeFactors(100)).toEqual([2, 2, 5, 5]); }); - test('should return an array containing 2 and 7 when passed 14', () => { - expect(primeFactors(14)).toEqual([2, 7]); + test('primeFactors(9973) => [9973] (9973 is prime)', () => { + expect(primeFactors(9973)).toEqual([9973]); }); }); diff --git a/math/primeFactors/primeFactors.ts b/math/primeFactors/primeFactors.ts index 4c852ba..27c7c9d 100644 --- a/math/primeFactors/primeFactors.ts +++ b/math/primeFactors/primeFactors.ts @@ -1,16 +1,21 @@ /** - * Find the prime factors of a given integer. + * Find the prime factors of a given integer (n > 1). * @param {number} n - The integer to factorize. - * @returns {Array} An array of prime factors. + * @returns {number[]} An array of prime factors. */ -export function primeFactors(n: number): Array { - const factors: Array = []; - for (let i = 2; i <= n / i; i++) { +export function primeFactors(n: number): number[] { + if (n <= 1) { + throw new Error('primeFactors is undefined for n <= 1.'); + } + + const factors: number[] = []; + for (let i = 2; i * i <= n; i++) { while (n % i === 0) { factors.push(i); n /= i; } } + // If there are more than 1 remaining, that is the last prime factor. if (n > 1) { factors.push(n); } diff --git a/math/sieveOfEratosthenes/sieveOfEratosthenes.test.ts b/math/sieveOfEratosthenes/sieveOfEratosthenes.test.ts index a307450..6bbd5a3 100644 --- a/math/sieveOfEratosthenes/sieveOfEratosthenes.test.ts +++ b/math/sieveOfEratosthenes/sieveOfEratosthenes.test.ts @@ -1,19 +1,26 @@ import {sieveOfEratosthenes} from './sieveOfEratosthenes'; describe('sieveOfEratosthenes', () => { - test('should return an empty array when passed 1', () => { + test('returns [] for n < 2', () => { expect(sieveOfEratosthenes(1)).toEqual([]); + expect(sieveOfEratosthenes(0)).toEqual([]); + expect(sieveOfEratosthenes(-5)).toEqual([]); }); - test('should return an array containing 2, 3, 5, and 7 when passed 10', () => { - expect(sieveOfEratosthenes(10)).toEqual([2, 3, 5, 7]); + test('returns [2] for n = 2', () => { + expect(sieveOfEratosthenes(2)).toEqual([2]); }); - test('should return an array containing 2, 3, 5, 7, 11, and 13 when passed 15', () => { - expect(sieveOfEratosthenes(15)).toEqual([2, 3, 5, 7, 11, 13]); + test('returns prime list for n = 10', () => { + // primes up to 10 => 2, 3, 5, 7 + expect(sieveOfEratosthenes(10)).toEqual([2, 3, 5, 7]); }); - test('should return an array containing 2, 3, 5, 7, 11, and 13 when passed 20', () => { - expect(sieveOfEratosthenes(20)).toEqual([2, 3, 5, 7, 11, 13, 17, 19]); + test('returns prime list for n = 11', () => { + // primes up to 11 => 2, 3, 5, 7, 11 + expect(sieveOfEratosthenes(11)).toEqual([2, 3, 5, 7, 11]); }); - test('should return an array containing 2, 3, 5, 7, 11, 13, 17, 19, 23, and 29 when passed 30', () => { - expect(sieveOfEratosthenes(30)).toEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]); + test('returns prime list for n = 1_000 (sanity check)', () => { + const primesUpTo1000 = sieveOfEratosthenes(1000); + expect(primesUpTo1000[0]).toBe(2); + // A quick check: 997 is prime, so it should be in the array + expect(primesUpTo1000).toContain(997); }); }); diff --git a/math/sieveOfEratosthenes/sieveOfEratosthenes.ts b/math/sieveOfEratosthenes/sieveOfEratosthenes.ts index 7709d02..2c317c3 100644 --- a/math/sieveOfEratosthenes/sieveOfEratosthenes.ts +++ b/math/sieveOfEratosthenes/sieveOfEratosthenes.ts @@ -1,15 +1,19 @@ /** * Use the Sieve of Eratosthenes algorithm to generate a list of primes up to n. * @param {number} n - The upper limit for prime numbers. - * @returns {Array} An array of prime numbers up to n. + * @returns {number[]} An array of prime numbers up to n. */ -export function sieveOfEratosthenes(n: number): Array { - // Create an array containing all integers from 2 to n - const primes = new Array(n + 1); - primes.fill(true); +export function sieveOfEratosthenes(n: number): number[] { + if (n < 2) { + return []; + } + + const primes = new Array(n + 1).fill(true); + // 0 and 1 are not prime numbers + primes[0] = false; + primes[1] = false; - // Mark all multiples of each prime as composite - for (let i = 2; i <= Math.sqrt(n); i++) { + for (let i = 2; i * i <= n; i++) { if (primes[i]) { for (let j = i * i; j <= n; j += i) { primes[j] = false; @@ -17,8 +21,7 @@ export function sieveOfEratosthenes(n: number): Array { } } - // Collect all remaining prime numbers - const result: Array = []; + const result: number[] = []; for (let i = 2; i <= n; i++) { if (primes[i]) { result.push(i);