From 9dde8a780832a29c508deb3382410e36f127fdf1 Mon Sep 17 00:00:00 2001 From: Anup Omkar <57665180+anupomkar@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:04:05 +0530 Subject: [PATCH] Add `MatrixRank` (#4571) * feat: adding matrix rank algorithm * fix: formatting * fix: adding comments, refactor and handling edge cases * refactor: minor refactor * enhancement: check matrix validity * refactor: minor refactor and fixes * Update src/main/java/com/thealgorithms/maths/MatrixRank.java * feat: add unit test to check if input matrix is not modified while calculating the rank --------- Co-authored-by: Anup Omkar Co-authored-by: Piotr Idzik <65706193+vil02@users.noreply.github.com> Co-authored-by: Andrii Siriak --- .../com/thealgorithms/maths/MatrixRank.java | 164 ++++++++++++++++++ .../thealgorithms/maths/MatrixRankTest.java | 45 +++++ 2 files changed, 209 insertions(+) create mode 100644 src/main/java/com/thealgorithms/maths/MatrixRank.java create mode 100644 src/test/java/com/thealgorithms/maths/MatrixRankTest.java 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]; + } + } + } + } +} 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 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 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)); + } +}