diff --git a/src/main/java/com/thealgorithms/graph/HopcroftKarp.java b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java
new file mode 100644
index 000000000000..76f7f3eaa3a7
--- /dev/null
+++ b/src/main/java/com/thealgorithms/graph/HopcroftKarp.java
@@ -0,0 +1,103 @@
+package com.thealgorithms.graph;
+
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * Hopcroft–Karp algorithm for maximum bipartite matching.
+ *
+ * Left part: vertices [0,nLeft-1], Right part: [0,nRight-1].
+ * Adjacency list: for each left vertex u, list right vertices v it connects to.
+ *
+ * Time complexity: O(E * sqrt(V)).
+ *
+ * @see
+ * Wikipedia: Hopcroft–Karp algorithm
+ * @author ptzecher
+ */
+public class HopcroftKarp {
+
+ private final int nLeft;
+ private final List> adj;
+
+ private final int[] pairU;
+ private final int[] pairV;
+ private final int[] dist;
+
+ public HopcroftKarp(int nLeft, int nRight, List> adj) {
+ this.nLeft = nLeft;
+ this.adj = adj;
+
+ this.pairU = new int[nLeft];
+ this.pairV = new int[nRight];
+ this.dist = new int[nLeft];
+
+ Arrays.fill(pairU, -1);
+ Arrays.fill(pairV, -1);
+ }
+
+ /** Returns the size of the maximum matching. */
+ public int maxMatching() {
+ int matching = 0;
+ while (bfs()) {
+ for (int u = 0; u < nLeft; u++) {
+ if (pairU[u] == -1 && dfs(u)) {
+ matching++;
+ }
+ }
+ }
+ return matching;
+ }
+
+ // BFS to build layers
+ private boolean bfs() {
+ Queue queue = new ArrayDeque<>();
+ Arrays.fill(dist, -1);
+
+ for (int u = 0; u < nLeft; u++) {
+ if (pairU[u] == -1) {
+ dist[u] = 0;
+ queue.add(u);
+ }
+ }
+
+ boolean foundAugPath = false;
+ while (!queue.isEmpty()) {
+ int u = queue.poll();
+ for (int v : adj.get(u)) {
+ int matchedLeft = pairV[v];
+ if (matchedLeft == -1) {
+ foundAugPath = true;
+ } else if (dist[matchedLeft] == -1) {
+ dist[matchedLeft] = dist[u] + 1;
+ queue.add(matchedLeft);
+ }
+ }
+ }
+ return foundAugPath;
+ }
+
+ // DFS to find augmenting paths within the BFS layering
+ private boolean dfs(int u) {
+ for (int v : adj.get(u)) {
+ int matchedLeft = pairV[v];
+ if (matchedLeft == -1 || (dist[matchedLeft] == dist[u] + 1 && dfs(matchedLeft))) {
+ pairU[u] = v;
+ pairV[v] = u;
+ return true;
+ }
+ }
+ dist[u] = -1;
+ return false;
+ }
+
+ public int[] getLeftMatches() {
+ return pairU.clone();
+ }
+
+ public int[] getRightMatches() {
+ return pairV.clone();
+ }
+}
diff --git a/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java b/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java
new file mode 100644
index 000000000000..2a5b1cdac795
--- /dev/null
+++ b/src/test/java/com/thealgorithms/graph/HopcroftKarpTest.java
@@ -0,0 +1,133 @@
+package com.thealgorithms.graph;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for Hopcroft–Karp algorithm.
+ *
+ * @author ptzecher
+ */
+class HopcroftKarpTest {
+
+ private static List> adj(int nLeft) {
+ List> g = new ArrayList<>(nLeft);
+ for (int i = 0; i < nLeft; i++) { // braces required by Checkstyle
+ g.add(new ArrayList<>());
+ }
+ return g;
+ }
+
+ @Test
+ @DisplayName("Empty graph has matching 0")
+ void emptyGraph() {
+ List> g = adj(3);
+ HopcroftKarp hk = new HopcroftKarp(3, 4, g);
+ assertEquals(0, hk.maxMatching());
+ }
+
+ @Test
+ @DisplayName("Single edge gives matching 1")
+ void singleEdge() {
+ List> g = adj(1);
+ g.get(0).add(0);
+ HopcroftKarp hk = new HopcroftKarp(1, 1, g);
+ assertEquals(1, hk.maxMatching());
+
+ int[] leftMatch = hk.getLeftMatches();
+ int[] rightMatch = hk.getRightMatches();
+ assertEquals(0, leftMatch[0]);
+ assertEquals(0, rightMatch[0]);
+ }
+
+ @Test
+ @DisplayName("Disjoint edges match perfectly")
+ void disjointEdges() {
+ // L0-R0, L1-R1, L2-R2
+ List> g = adj(3);
+ g.get(0).add(0);
+ g.get(1).add(1);
+ g.get(2).add(2);
+
+ HopcroftKarp hk = new HopcroftKarp(3, 3, g);
+ assertEquals(3, hk.maxMatching());
+
+ int[] leftMatch = hk.getLeftMatches();
+ int[] rightMatch = hk.getRightMatches();
+ for (int i = 0; i < 3; i++) {
+ assertEquals(i, leftMatch[i]);
+ assertEquals(i, rightMatch[i]);
+ }
+ }
+
+ @Test
+ @DisplayName("Complete bipartite K(3,4) matches min(3,4)=3")
+ void completeK34() {
+ int nLeft = 3;
+ int nRight = 4; // split declarations
+ List> g = adj(nLeft);
+ for (int u = 0; u < nLeft; u++) {
+ g.get(u).addAll(Arrays.asList(0, 1, 2, 3));
+ }
+ HopcroftKarp hk = new HopcroftKarp(nLeft, nRight, g);
+ assertEquals(3, hk.maxMatching());
+
+ // sanity: no two left vertices share the same matched right vertex
+ int[] leftMatch = hk.getLeftMatches();
+ boolean[] used = new boolean[nRight];
+ for (int u = 0; u < nLeft; u++) {
+ int v = leftMatch[u];
+ if (v != -1) {
+ assertFalse(used[v]);
+ used[v] = true;
+ }
+ }
+ }
+
+ @Test
+ @DisplayName("Rectangular, sparse graph")
+ void rectangularSparse() {
+ // Left: 5, Right: 2
+ // edges: L0-R0, L1-R1, L2-R0, L3-R1 (max matching = 2)
+ List> g = adj(5);
+ g.get(0).add(0);
+ g.get(1).add(1);
+ g.get(2).add(0);
+ g.get(3).add(1);
+ // L4 isolated
+
+ HopcroftKarp hk = new HopcroftKarp(5, 2, g);
+ assertEquals(2, hk.maxMatching());
+
+ int[] leftMatch = hk.getLeftMatches();
+ int[] rightMatch = hk.getRightMatches();
+
+ // Check consistency: if leftMatch[u]=v then rightMatch[v]=u
+ for (int u = 0; u < 5; u++) {
+ int v = leftMatch[u];
+ if (v != -1) {
+ assertEquals(u, rightMatch[v]);
+ }
+ }
+ }
+
+ @Test
+ @DisplayName("Layering advantage case (short augmenting paths)")
+ void layeringAdvantage() {
+ // Left 4, Right 4
+ List> g = adj(4);
+ g.get(0).addAll(Arrays.asList(0, 1));
+ g.get(1).addAll(Arrays.asList(1, 2));
+ g.get(2).addAll(Arrays.asList(2, 3));
+ g.get(3).addAll(Arrays.asList(0, 3));
+
+ HopcroftKarp hk = new HopcroftKarp(4, 4, g);
+ assertEquals(4, hk.maxMatching()); // perfect matching exists
+ }
+}