Skip to content

Enhance Algorithms and Data Structures with New Features and Test Coverage #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions algorithms/dp/coinChange/coinChange.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
64 changes: 64 additions & 0 deletions algorithms/dp/coinChange/coinChange.ts
Original file line number Diff line number Diff line change
@@ -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];
}
File renamed without changes.
File renamed without changes.
30 changes: 30 additions & 0 deletions algorithms/dp/lcs/lcs.test.ts
Original file line number Diff line number Diff line change
@@ -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('');
});
});
58 changes: 58 additions & 0 deletions algorithms/dp/lcs/lcs.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
41 changes: 34 additions & 7 deletions algorithms/search/binary-search/binarySearch.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
32 changes: 28 additions & 4 deletions algorithms/search/binary-search/binarySearch.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
export function binarySearch<T>(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<T>(
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;
Expand Down
30 changes: 27 additions & 3 deletions algorithms/search/interpolation-search/interpolationSearch.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
32 changes: 26 additions & 6 deletions algorithms/search/interpolation-search/interpolationSearch.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading