Repository: TheAlgorithms/Java (java)

Issue:
[FEATURE REQUEST] Addition of Rank Of Matrix Algorithm
### What would you like to Propose?

New Algorithm under
https://github.com/TheAlgorithms/Java/tree/master/src/main/java/com/thealgorithms/maths

File Name Proposed: MatrixRank.java

What?
An implementation of Rank of a Matrix.

### Issue details

More about rank of a matrix: https://en.wikipedia.org/wiki/Rank_(linear_algebra)

Algorithm complexity:
1. Time Complexity: O(M*N*rank)
2. Space Complexity: O(N)

### Additional Information

**Note: This issue is self assigned and PR is raised as a part of hacktoberfest**

Pull request:
Add `MatrixRank`
<!--
Thank you for your contribution!
In order to reduce the number of notifications sent to the maintainers, please:
- create your PR as draft, cf. https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests,
- make sure that all of the CI checks pass,
- mark your PR as ready for review, cf. https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review
-->

<!-- For completed items, change [ ] to [x] -->

- [x] I have read [CONTRIBUTING.md](https://github.com/TheAlgorithms/Java/blob/master/CONTRIBUTING.md).
- [x] This pull request is all my own work -- I have not plagiarized it.
- [x] All filenames are in PascalCase.
- [x] All functions and variable names follow Java naming conventions.
- [x] All new algorithms have a URL in their comments that points to Wikipedia or other similar explanations.

closes #4570


Patch:
diff --git a/src/main/java/com/thealgorithms/maths/MatrixRank.java b/src/main/java/com/thealgorithms/maths/MatrixRank.java
new file mode 100644
index 00000000..7a628b92
--- /dev/null
+++ b/src/main/java/com/thealgorithms/maths/MatrixRank.java
@@ -0,0 +1,164 @@
+package com.thealgorithms.maths;
+
+/**
+ * This class provides a method to compute the rank of a matrix.
+ * In linear algebra, the rank of a matrix is the maximum number of linearly independent rows or columns in the matrix.
+ * For example, consider the following 3x3 matrix:
+ * 1 2 3
+ * 2 4 6
+ * 3 6 9
+ * Despite having 3 rows and 3 columns, this matrix only has a rank of 1 because all rows (and columns) are multiples of each other.
+ * It's a fundamental concept that gives key insights into the structure of the matrix.
+ * It's important to note that the rank is not only defined for square matrices but for any m x n matrix.
+ *
+ * @author Anup Omkar
+ */
+public final class MatrixRank {
+
+    private MatrixRank() {
+    }
+
+    private static final double EPSILON = 1e-10;
+
+    /**
+     * @brief Computes the rank of the input matrix
+     *
+     * @param matrix The input matrix
+     * @return The rank of the input matrix
+     */
+    public static int computeRank(double[][] matrix) {
+        validateInputMatrix(matrix);
+
+        int numRows = matrix.length;
+        int numColumns = matrix[0].length;
+        int rank = 0;
+
+        boolean[] rowMarked = new boolean[numRows];
+
+        double[][] matrixCopy = deepCopy(matrix);
+
+        for (int colIndex = 0; colIndex < numColumns; ++colIndex) {
+            int pivotRow = findPivotRow(matrixCopy, rowMarked, colIndex);
+            if (pivotRow != numRows) {
+                ++rank;
+                rowMarked[pivotRow] = true;
+                normalizePivotRow(matrixCopy, pivotRow, colIndex);
+                eliminateRows(matrixCopy, pivotRow, colIndex);
+            }
+        }
+        return rank;
+    }
+
+    private static boolean isZero(double value) {
+        return Math.abs(value) < EPSILON;
+    }
+
+    private static double[][] deepCopy(double[][] matrix) {
+        int numRows = matrix.length;
+        int numColumns = matrix[0].length;
+        double[][] matrixCopy = new double[numRows][numColumns];
+        for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
+            System.arraycopy(matrix[rowIndex], 0, matrixCopy[rowIndex], 0, numColumns);
+        }
+        return matrixCopy;
+    }
+
+    private static void validateInputMatrix(double[][] matrix) {
+        if (matrix == null) {
+            throw new IllegalArgumentException("The input matrix cannot be null");
+        }
+        if (matrix.length == 0) {
+            throw new IllegalArgumentException("The input matrix cannot be empty");
+        }
+        if (!hasValidRows(matrix)) {
+            throw new IllegalArgumentException("The input matrix cannot have null or empty rows");
+        }
+        if (isJaggedMatrix(matrix)) {
+            throw new IllegalArgumentException("The input matrix cannot be jagged");
+        }
+    }
+
+    private static boolean hasValidRows(double[][] matrix) {
+        for (double[] row : matrix) {
+            if (row == null || row.length == 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @brief Checks if the input matrix is a jagged matrix.
+     * Jagged matrix is a matrix where the number of columns in each row is not the same.
+     *
+     * @param matrix The input matrix
+     * @return True if the input matrix is a jagged matrix, false otherwise
+     */
+    private static boolean isJaggedMatrix(double[][] matrix) {
+        int numColumns = matrix[0].length;
+        for (double[] row : matrix) {
+            if (row.length != numColumns) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @brief The pivot row is the row in the matrix that is used to eliminate other rows and reduce the matrix to its row echelon form.
+     * The pivot row is selected as the first row (from top to bottom) where the value in the current column (the pivot column) is not zero.
+     * This row is then used to "eliminate" other rows, by subtracting multiples of the pivot row from them, so that all other entries in the pivot column become zero.
+     * This process is repeated for each column, each time selecting a new pivot row, until the matrix is in row echelon form.
+     * The number of pivot rows (rows with a leading entry, or pivot) then gives the rank of the matrix.
+     *
+     * @param matrix The input matrix
+     * @param rowMarked An array indicating which rows have been marked
+     * @param colIndex The column index
+     * @return The pivot row index, or the number of rows if no suitable pivot row was found
+     */
+    private static int findPivotRow(double[][] matrix, boolean[] rowMarked, int colIndex) {
+        int numRows = matrix.length;
+        for (int pivotRow = 0; pivotRow < numRows; ++pivotRow) {
+            if (!rowMarked[pivotRow] && !isZero(matrix[pivotRow][colIndex])) {
+                return pivotRow;
+            }
+        }
+        return numRows;
+    }
+
+    /**
+     * @brief This method divides all values in the pivot row by the value in the given column.
+     * This ensures that the pivot value itself will be 1, which simplifies further calculations.
+     *
+     * @param matrix The input matrix
+     * @param pivotRow The pivot row index
+     * @param colIndex The column index
+     */
+    private static void normalizePivotRow(double[][] matrix, int pivotRow, int colIndex) {
+        int numColumns = matrix[0].length;
+        for (int nextCol = colIndex + 1; nextCol < numColumns; ++nextCol) {
+            matrix[pivotRow][nextCol] /= matrix[pivotRow][colIndex];
+        }
+    }
+
+    /**
+     * @brief This method subtracts multiples of the pivot row from all other rows,
+     * so that all values in the given column of other rows will be zero.
+     * This is a key step in reducing the matrix to row echelon form.
+     *
+     * @param matrix The input matrix
+     * @param pivotRow The pivot row index
+     * @param colIndex The column index
+     */
+    private static void eliminateRows(double[][] matrix, int pivotRow, int colIndex) {
+        int numRows = matrix.length;
+        int numColumns = matrix[0].length;
+        for (int otherRow = 0; otherRow < numRows; ++otherRow) {
+            if (otherRow != pivotRow && !isZero(matrix[otherRow][colIndex])) {
+                for (int col2 = colIndex + 1; col2 < numColumns; ++col2) {
+                    matrix[otherRow][col2] -= matrix[pivotRow][col2] * matrix[otherRow][colIndex];
+                }
+            }
+        }
+    }
+}


Test patch:
diff --git a/src/test/java/com/thealgorithms/maths/MatrixRankTest.java b/src/test/java/com/thealgorithms/maths/MatrixRankTest.java
new file mode 100644
index 00000000..415b84ec
--- /dev/null
+++ b/src/test/java/com/thealgorithms/maths/MatrixRankTest.java
@@ -0,0 +1,45 @@
+package com.thealgorithms.maths;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class MatrixRankTest {
+
+    private static Stream<Arguments> validInputStream() {
+        return Stream.of(Arguments.of(3, new double[][] {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}), Arguments.of(0, new double[][] {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}), Arguments.of(1, new double[][] {{1}}), Arguments.of(2, new double[][] {{1, 2}, {3, 4}}),
+            Arguments.of(2, new double[][] {{3, -1, 2}, {-3, 1, 2}, {-6, 2, 4}}), Arguments.of(3, new double[][] {{2, 3, 0, 1}, {1, 0, 1, 2}, {-1, 1, 1, -2}, {1, 5, 3, -1}}), Arguments.of(1, new double[][] {{1, 2, 3}, {3, 6, 9}}),
+            Arguments.of(2, new double[][] {{0.25, 0.5, 0.75, 2}, {1.5, 3, 4.5, 6}, {1, 2, 3, 4}}));
+    }
+
+    private static Stream<Arguments> invalidInputStream() {
+        return Stream.of(Arguments.of((Object) new double[][] {{1, 2}, {10}, {100, 200, 300}}), // jagged array
+            Arguments.of((Object) new double[][] {}), // empty matrix
+            Arguments.of((Object) new double[][] {{}, {}}), // empty row
+            Arguments.of((Object) null), // null matrix
+            Arguments.of((Object) new double[][] {{1, 2}, null}) // null row
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("validInputStream")
+    void computeRankTests(int expectedRank, double[][] matrix) {
+        int originalHashCode = Arrays.deepHashCode(matrix);
+        int rank = MatrixRank.computeRank(matrix);
+        int newHashCode = Arrays.deepHashCode(matrix);
+
+        assertEquals(expectedRank, rank);
+        assertEquals(originalHashCode, newHashCode);
+    }
+
+    @ParameterizedTest
+    @MethodSource("invalidInputStream")
+    void computeRankWithInvalidMatrix(double[][] matrix) {
+        assertThrows(IllegalArgumentException.class, () -> MatrixRank.computeRank(matrix));
+    }
+}