Skip to content

feat: Add GaleShapley in a new folder Greedy #1714

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

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 2 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@
* [NodeNeighbors](Graphs/NodeNeighbors.js)
* [NumberOfIslands](Graphs/NumberOfIslands.js)
* [PrimMST](Graphs/PrimMST.js)
* **Greedy**
* [GaleShapley](Greedy/GaleShapley.js)
* **Hashes**
* [MD5](Hashes/MD5.js)
* [SHA1](Hashes/SHA1.js)
Expand Down
78 changes: 78 additions & 0 deletions Greedy/GaleShapley.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* The Gale-Shapley algorithm is used to find a stable matching between two sets
* of equal size (e.g., donors and recipients) where no two elements prefer each
* other over their current partners. It ensures a stable matching by proposing
* from one side to the other until all are matched.
*
* Complexity:
* Worst-case performance O(n^2)
* Best-case performance O(n^2)
*
* Reference:
* https://en.wikipedia.org/wiki/Stable_marriage_problem
* https://www.youtube.com/watch?v=Qcv1IqHWAzg (Numberphile)
*
*/

/**
*
* @param {number[][]} donorPref Preferences of donors, where each donor has an ordered list of recipients.
* @param {number[][]} recipientPref Preferences of recipients, where each recipient has an ordered list of donors.
* @returns {number[]} Array where the index is the donor, and the value at the index is the matched recipient.
*
* @example
* const donorPref = [[0, 1, 3, 2], [0, 2, 3, 1], [1, 0, 2, 3], [0, 3, 1, 2]];
* const recipientPref = [[3, 1, 2, 0], [3, 1, 0, 2], [0, 3, 1, 2], [1, 0, 3, 2]];
* stableMatching(donorPref, recipientPref); // Output: [1, 2, 3, 0]
*/
function stableMatching(donorPref, recipientPref) {
// Initialize the number of donors and create a list of unmatched donors
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use this array as a stack because that's efficient.

let n = donorPref.length
let unmatchedDonors = Array.from({ length: n }, (_, i) => i)

// Records of which recipient each donor is paired with and vice versa
let donorRecord = Array(n).fill(-1) // Donor to recipient
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd compute donorRecord at the end (or not at all even and instead just return recRecord and document that because the two are equivalent). That way you save yourself having to remember to update it during the algorithm, and the constant factor of the $O(n^2)$ many operations is better.

let recRecord = Array(n).fill(-1) // Recipient to donor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think it'd be idiomatic to use null rather than -1 to signal the absence of a value.


// Array to track how many recipients each donor has proposed to
let numDonations = Array(n).fill(0)

// While there are unmatched donors
while (unmatchedDonors.length > 0) {
// Take the first unmatched donor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't. Take the last one instead; use the array as a stack (you could also use a queue, but why do that when a stack suffices). If you take the first one, you have to do a costly $O(n)$ unshift / splice later on to remove it (arguably you could also swap with the last one and pop, but that's just unnecessary complexity when you can just pop here), which wrecks your time complexity: You get $O(n^3)$ instead of $O(n^2)$.

let donor = unmatchedDonors[0]
let donorPreference = donorPref[donor]

// Find the next recipient this donor prefers
let recipient = donorPreference[numDonations[donor]]
numDonations[donor] += 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
numDonations[donor] += 1
numDonations[donor]++


// Get recipient's preference list and check the current match
let recPreference = recipientPref[recipient]
let prevDonor = recRecord[recipient]

// If recipient is already matched, check if they prefer the new donor
if (prevDonor !== -1) {
if (recPreference.indexOf(prevDonor) > recPreference.indexOf(donor)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also wrecks time complexity by adding a linear factor. Instead you should precompute the inverse mappings initially as lookup tables (this is what's responsible for the $O(n^2)$ best case): For each receiver, compute the mapping from donor -> preference ahead of time.

// If the new donor is preferred, match them and unmatch the previous donor
recRecord[recipient] = donor
donorRecord[donor] = recipient
unmatchedDonors.push(prevDonor)
unmatchedDonors.splice(unmatchedDonors.indexOf(donor), 1) // Remove the current donor from unmatched
}
} else {
// If the recipient is not matched, pair them with the current donor
recRecord[recipient] = donor
donorRecord[donor] = recipient
unmatchedDonors.splice(unmatchedDonors.indexOf(donor), 1) // Remove the current donor from unmatched
}
}

return donorRecord
}

// // Example usage:
// const donorPref = [[0, 1, 3, 2], [0, 2, 3, 1], [1, 0, 2, 3], [0, 3, 1, 2]];
// const recipientPref = [[3, 1, 2, 0], [3, 1, 0, 2], [0, 3, 1, 2], [1, 0, 3, 2]];
// console.log(stableMatching(donorPref, recipientPref)); // Output: [1, 2, 3, 0]
export { stableMatching }
36 changes: 36 additions & 0 deletions Greedy/test/GaleShapley.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, test } from 'vitest'
import { stableMatching } from '../GaleShapley'

test('Test Case 1', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test case structure like this isn't helpful. Please see https://github.com/TheAlgorithms/TypeScript/blob/master/CONTRIBUTING.md#writing-good-tests (it's for the TS repo, but translates to JS just as well).

TL;DR: Use a describe block for Gale-Shapley and it.each for individual test cases if you have no descriptive labels for them.

const donorPref = [
[0, 1, 3, 2],
[0, 2, 3, 1],
[1, 0, 2, 3],
[0, 3, 1, 2]
]
const recipientPref = [
[3, 1, 2, 0],
[3, 1, 0, 2],
[0, 3, 1, 2],
[1, 0, 3, 2]
]
expect(stableMatching(donorPref, recipientPref)).toEqual([1, 2, 3, 0])
})
test('Test Case 2', () => {
const donorPref = [
[0, 1, 2],
[0, 1, 2],
[0, 1, 2]
]
const recipientPref = [
[0, 1, 2],
[0, 1, 2],
[0, 1, 2]
]
expect(stableMatching(donorPref, recipientPref)).toEqual([0, 1, 2])
})
test('Test Case 3', () => {
const donorPref = []
const recipientPref = []
expect(stableMatching(donorPref, recipientPref)).toEqual([])
})