mirror of
https://github.com/TheAlgorithms/JavaScript.git
synced 2025-07-04 15:39:42 +08:00
feat: add row echelon matrix algorithm (#1454)
* feat: add row echelon matrix algorithm * test: add self-tests for row echelon algorithm * fix: replace rounding with float tolerance * chore: use correct style * fix: use error tolerance and segregate testcases * chore: add necessary explaining comments
This commit is contained in:
150
Maths/RowEchelon.js
Normal file
150
Maths/RowEchelon.js
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Given a two dimensional matrix, find its row echelon form.
|
||||
*
|
||||
* For more info: https://en.wikipedia.org/wiki/Row_echelon_form
|
||||
*
|
||||
* @param {number[[]]} matrix - Two dimensional array of rational numbers.
|
||||
* @returns {number[[]]} - Two dimensional array of rational numbers (row echelon form).
|
||||
*
|
||||
* @example
|
||||
* const matrix = [
|
||||
* [2,3,4,5,7],
|
||||
* [9,8,4,0,9],
|
||||
* [5,7,4,3,9],
|
||||
* [3,4,0,2,1]
|
||||
* ]
|
||||
*
|
||||
* const result = rowEchelon(matrix)
|
||||
*
|
||||
* // The function returns the corresponding row echelon form:
|
||||
* // result:
|
||||
* // [
|
||||
* // [1, 1.5, 2, 2.5, 3.5],
|
||||
* // [0, 1, 2.54545, 4.09091, 4.09091],
|
||||
* // [0, 0, 1, 1.57692, 1.36539],
|
||||
* // [0, 0, 0, 1, -0.25]
|
||||
* // ]
|
||||
*/
|
||||
|
||||
// Set a tolerance value for floating-point comparisons
|
||||
const tolerance = 0.000001
|
||||
|
||||
// Check if all the rows have same length of elements
|
||||
const isMatrixValid = (matrix) => {
|
||||
let numRows = matrix.length
|
||||
let numCols = matrix[0].length
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
if (numCols !== matrix[i].length) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check for input other than a 2D matrix
|
||||
if (
|
||||
!Array.isArray(matrix) ||
|
||||
matrix.length === 0 ||
|
||||
!Array.isArray(matrix[0])
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const checkNonZero = (currentRow, currentCol, matrix) => {
|
||||
let numRows = matrix.length
|
||||
for (let i = currentRow; i < numRows; i++) {
|
||||
// Checks if the current element is not very near to zero.
|
||||
if (!isTolerant(0, matrix[i][currentCol], tolerance)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const swapRows = (currentRow, withRow, matrix) => {
|
||||
let numCols = matrix[0].length
|
||||
let tempValue = 0
|
||||
for (let j = 0; j < numCols; j++) {
|
||||
tempValue = matrix[currentRow][j]
|
||||
matrix[currentRow][j] = matrix[withRow][j]
|
||||
matrix[withRow][j] = tempValue
|
||||
}
|
||||
}
|
||||
|
||||
// Select a pivot element in the current column to facilitate row operations.
|
||||
// Pivot element is the first non-zero element found from the current row
|
||||
// down to the last row.
|
||||
const selectPivot = (currentRow, currentCol, matrix) => {
|
||||
let numRows = matrix.length
|
||||
for (let i = currentRow; i < numRows; i++) {
|
||||
if (matrix[i][currentCol] !== 0) {
|
||||
swapRows(currentRow, i, matrix)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply each element of the given row with a factor.
|
||||
const scalarMultiplication = (currentRow, factor, matrix) => {
|
||||
let numCols = matrix[0].length
|
||||
for (let j = 0; j < numCols; j++) {
|
||||
matrix[currentRow][j] *= factor
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract one row from another row
|
||||
const subtractRow = (currentRow, fromRow, matrix) => {
|
||||
let numCols = matrix[0].length
|
||||
for (let j = 0; j < numCols; j++) {
|
||||
matrix[fromRow][j] -= matrix[currentRow][j]
|
||||
}
|
||||
}
|
||||
|
||||
// Check if two numbers are equal within a given tolerance
|
||||
const isTolerant = (a, b, tolerance) => {
|
||||
const absoluteDifference = Math.abs(a - b)
|
||||
return absoluteDifference <= tolerance
|
||||
}
|
||||
|
||||
const rowEchelon = (matrix) => {
|
||||
// Check if the input matrix is valid; if not, throw an error.
|
||||
if (!isMatrixValid(matrix)) {
|
||||
throw new Error('Input is not a valid 2D matrix.')
|
||||
}
|
||||
|
||||
let numRows = matrix.length
|
||||
let numCols = matrix[0].length
|
||||
let result = matrix
|
||||
|
||||
// Iterate through the rows (i) and columns (j) of the matrix.
|
||||
for (let i = 0, j = 0; i < numRows && j < numCols; ) {
|
||||
// If the current column has all zero elements below the current row,
|
||||
// move to the next column.
|
||||
if (!checkNonZero(i, j, result)) {
|
||||
j++
|
||||
continue
|
||||
}
|
||||
|
||||
// Select a pivot element and normalize the current row.
|
||||
selectPivot(i, j, result)
|
||||
let factor = 1 / result[i][j]
|
||||
scalarMultiplication(i, factor, result)
|
||||
|
||||
// Make elements below the pivot element zero by performing
|
||||
// row operations on subsequent rows.
|
||||
for (let x = i + 1; x < numRows; x++) {
|
||||
factor = result[x][j]
|
||||
if (isTolerant(0, factor, tolerance)) {
|
||||
continue
|
||||
}
|
||||
scalarMultiplication(i, factor, result)
|
||||
subtractRow(i, x, result)
|
||||
factor = 1 / factor
|
||||
scalarMultiplication(i, factor, result)
|
||||
}
|
||||
i++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export { rowEchelon }
|
89
Maths/test/RowEchelon.test.js
Normal file
89
Maths/test/RowEchelon.test.js
Normal file
@ -0,0 +1,89 @@
|
||||
import { rowEchelon } from '../RowEchelon'
|
||||
describe('Determinant', () => {
|
||||
const tolerance = 0.000001
|
||||
test.each([
|
||||
[
|
||||
[
|
||||
[8, 1, 3, 5],
|
||||
[4, 6, 8, 2],
|
||||
[3, 5, 6, 8]
|
||||
],
|
||||
[
|
||||
[1, 0.125, 0.375, 0.625],
|
||||
[0, 1, 1.18182, -0.09091],
|
||||
[0, 0, 1, -11.0769]
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
[6, 8, 1, 3, 5],
|
||||
[1, 4, 6, 8, 2],
|
||||
[0, 3, 5, 6, 8],
|
||||
[2, 5, 9, 7, 8],
|
||||
[5, 5, 7, 0, 1]
|
||||
],
|
||||
[
|
||||
[1, 1.33333, 0.16667, 0.5, 0.83333],
|
||||
[0, 1, 2.1875, 2.8125, 0.4375],
|
||||
[0, 0, 1, 1.56, -4.28003],
|
||||
[0, 0, 0, 1, -3.3595],
|
||||
[0, 0, 0, 0, 1]
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
[1, 3, 5],
|
||||
[6, 8, 2],
|
||||
[5, 6, 8],
|
||||
[7, 9, 9],
|
||||
[5, 0, 6]
|
||||
],
|
||||
[
|
||||
[1, 3, 5],
|
||||
[0, 1, 2.8],
|
||||
[0, 0, 1],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
[0, 7, 8, 1, 3, 5],
|
||||
[0, 6, 4, 6, 8, 2],
|
||||
[0, 7, 3, 5, 6, 8],
|
||||
[6, 8, 1, 0, 0, 4],
|
||||
[3, 3, 5, 7, 3, 1],
|
||||
[1, 2, 1, 0, 9, 7],
|
||||
[8, 8, 0, 2, 3, 1]
|
||||
],
|
||||
[
|
||||
[1, 1.33333, 0.16667, 0, 0, 0.66667],
|
||||
[0, 1, 0.66667, 1, 1.33333, 0.33333],
|
||||
[0, 0, 1, 1.2, 1.99999, -3.4],
|
||||
[0, 0, 0, 1, 1.3, -1.4],
|
||||
[0, 0, 0, 0, 1, -2.32854],
|
||||
[0, 0, 0, 0, 0, 1],
|
||||
[0, 0, 0, 0, 0, 0]
|
||||
]
|
||||
]
|
||||
])('Should return the matrix in row echelon form.', (matrix, expected) => {
|
||||
for (let i = 0; i < matrix.length; i++) {
|
||||
for (let j = 0; j < matrix[i].length; j++) {
|
||||
expect(rowEchelon(matrix)[i][j]).toBeCloseTo(expected[i][j], tolerance)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test.each([
|
||||
[
|
||||
[
|
||||
[8, 1, 3, 5],
|
||||
[4, 6, 8, 2, 7],
|
||||
[3, 5, 6, 8]
|
||||
],
|
||||
'Input is not a valid 2D matrix.'
|
||||
]
|
||||
])('Should return the error message.', (matrix, expected) => {
|
||||
expect(() => rowEchelon(matrix)).toThrowError(expected)
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user