65.9K
CodeProject 正在变化。 阅读更多。
Home

带数学方程解释的矩阵类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (15投票s)

2007 年 5 月 14 日

10分钟阅读

viewsIcon

135719

downloadIcon

3899

使用数学方程解释矩阵类的开发,包括基本算术函数和各种行列式方法

Screenshot - matrix1.jpg

引言

互联网上有很多关于矩阵不同实现的资源。我的想法不仅仅是开发一个矩阵类,而是利用数学方程系统地处理这类编程问题。这个矩阵类远非完整,但附带了完整的文档。

矩阵在科学的各个领域都有许多重要的应用,并用于描述线性方程。它们包含线性变换的系数,可以以各种方式相加、相乘和分解。这使它们成为线性代数和矩阵理论中的关键概念。它们在科学和数学中如此重要,以至于它们是许多商业软件应用(如 MATLAB)的构建模块。本文将介绍使用 VB.NET 开发矩阵类。将对每个要编写的函数提供正式的数学定义。

背景

矩阵是数字的矩形数组。更一般地说,它可以是一个可以进行代数运算的量的二维数组。一般而言,这些“量”可以是任何可以相加或相乘的抽象实体——例如,整数、分数、多项式等——但在本文中,我们通过使用 Double 作为这些量来考虑一个简单的情况。

开发代码

矩阵A可以组织为

我们选择表示矩阵的标准在这里对我们来说并不重要。以下是一些信息

至于矩阵的标准表示,我们可以将单个元素 ai,j 标识为第 i 行第 j 列的元素。但是,如果我们考虑水平轴为 x,垂直轴为 y,那么根据上面的组织,ai,j 是位于 x=i 和 y=j 的元素,如果遵循行和列的约定,这将与 aj,i 是同一个元素(注意下标颠倒)。换句话说,x=i 表示列而不是行,但 x 穿过行(x=0 表示第 0 列,x=1 表示第 1 列,依此类推,但 x 穿过行)。数学和计算机语言中的二维空间都指定为第二种表示。因此,它被选用于我们的目的,我们可以通过声明我们的类来继续

Public Class Matrix
    Implements ICloneable
    
    Private M As Integer ' Horizontal Size = No. of Columns
    Private N As Integer ' Vertical Size = No. of Rows
    Public val(,) As Double
    Private currentX As Integer
    Private currentY As Integer
End Class 

这里,val(,) 是一个包含矩阵元素的二维数组。在类的构造函数中,我们需要为我们的矩阵分配正确的维度

    Public Sub New(ByVal X As Integer, ByVal Y As Integer)
        SetDimensions(X, Y)
        currentX = 0
        currentY = 0
    End Sub

    Public Sub New(ByVal X As Integer)
        SetDimensions(X, X)
        currentX = 0
        currentY = 0
    End Sub
    Private Function SetDimensions(ByVal X As Integer, ByVal Y As Integer)
        M = X
        N = Y
        ReDim val(M - 1, N - 1)
    End Function

构造函数已重载,一个用于矩形矩阵情况,另一个用于更具体的方阵情况。请注意,由于我们的二维数组是基于零索引的,如果 M 是水平计数,则元素索引为 0 到 M-1。

矩阵加法

现在,我们将向我们的类添加一个函数,该函数将接收另一个矩阵并执行逐元素加法。如果 C 是加到矩阵 A 以获得矩阵 B 的矩阵,则

对于所有 i 和 j。换句话说

同样,A C 的维度必须匹配。

    Public Function Add(ByVal C As Matrix) As Matrix
        If M <> C.M Or N <> C.N Then
            Throw New Exception("Matrices size Mismatch.")
        End If
        Dim B As Matrix = New Matrix(M, N)
        For i As Integer = 0 To M - 1
            For j As Integer = 0 To N - 1
                B.val(i, j) = val(i, j) + C.val(i, j)
            Next
        Next
        Return B
    End Function

矩阵乘法

现在我们继续编写乘法函数。只有当第一个矩阵的列数 (M) 等于第二个矩阵的行数 (N) 时,才能对两个矩阵执行乘法。换句话说,如果矩阵 A C 相乘,那么对于 B=AC ,根据我们的变量,只有当 A.M = C.N 时才可能。所以

   Public Function Multiply(ByVal C As Matrix) As Matrix
        If M <> C.N Then
            Throw New Exception("Matrices size Mismatch.")
        End If
        ...
        ...
    End Function

记住矩阵乘法如下

所以,如果 B=ACA 的一整行(例如,上面示例中的第 0 行是 [1 0 2])可以寻址为 A.val(i,J),其中 i 是访问行中单个元素的索引,j=J 将是常数(j=J=0 对于第 0 行的情况)。类似地,矩阵 C 的一整列指定为 C.val(I,j)

为了进行乘法,我们注意到运行索引 i 和 j 之间存在一对一的对应关系。因此,我们写 i=j=k。

结果矩阵 B 的第一个元素 B.val(0,0) 可以通过对 [A.val(k,0) 和 C.val(0,k)] 进行所有 k 的求和得到

一般来说,对于矩阵 B 中 (i, j) 的任何元素


其中

        Dim B As Matrix = New Matrix(N, C.M)
        For j As Integer = 0 To N - 1
            For i As Integer = 0 To C.M - 1
                For k As Integer = 0 To M - 1 ' or 0 to C.N -1
                    B.val(i, j) += val(k, j) * C.val(i, k)
                Next
            Next
        Next
        Return B

矩阵行列式

有许多方法可以获得矩阵的行列式。最流行的是按特定列或行展开。其他方法包括高斯-约旦消元法和高斯消元法。

按行/列展开

按特定行或列展开是最简单的方法,我们将首先用它来编程。对于所有忘记或不知道此方法的人,请考虑计算以下矩阵的行列式,按第 0 行展开

然后

其中 从原始矩阵 A 中删除第 0 行和第 0 列得到。注意 +1、-2 和 +3——符号在正负之间交替。从上面的步骤可以看出,计算这个 3x3 矩阵的行列式的问题被简化为计算三个 2x2 矩阵的行列式的问题。如果原始矩阵是 4x4 大小,那么按第 0 行展开后,将得到四个 3x3 矩阵。然后对于这四个中的每一个,重复相同的过程将得到三个 2x2 矩阵,最终总共得到二十四个 1x1 矩阵。对这些 2x2 矩阵继续相同的过程将得到二十四个 1x1 矩阵。

要对这个过程进行编程,首先考虑一个函数 SubMatrix(x, y),该函数从原始矩阵生成一个矩阵 S,其中删除第 y 行和第 x 列。这可以表示为

是将元素添加到矩阵中的运算符,就像求和 是算术加法的运算符一样。首先,我们编写一个过程来将元素 val(i,j) 添加到矩阵 S 中,并首先填充行。

    Public Function AddElement(ByVal element As Double)
        If currentX > (M - 1) Then
            currentY += 1
            currentX = 0
        End If

        Try
            val(currentX, currentY) = element
        Catch e As Exception
            Throw New Exception("Matrix filled with values.")
        End Try
        currentX += 1
    End Function

结果矩阵不应包含出现在第 y 行和第 x 列的矩阵 A 中的元素。例如,如果

那么 SubMatrix(2,1) 将得到

    Public Function SubMatrix(ByVal x As Integer, ByVal y As Integer) As Matrix
        Dim S As Matrix = New Matrix(M - 1, N - 1)
        For j As Integer = 0 To N - 1
            For i As Integer = 0 To M - 1
                If (i <> x And j <> y) Then
                    S.AddElement(val(i, j))
                End If
            Next
        Next
        Return S
    End Function

在定义了 SubMatrix(x,y) 之后,我们可以从上面的过程轻松地推导出递归关系

对于

每个递归过程都有一个基本情况。上面 val(0,0) 的基本情况利用了一个事实,即 1x1 矩阵中唯一的元素是该矩阵的行列式。

    Public Function Determinant() As Double
        If M = 1 And N = 1 Then
            Return val(0, 0)
        End If

        Dim temp As Double
        Dim MySubMatrix As Matrix
        Dim j As Integer = 0
        For i As Integer = 0 To M - 1
            MySubMatrix = SubMatrix(i, j)
            temp += ((-1) ^ (i + j) * val(i, j) * MySubMatrix.Determinant())
        Next
        Return temp

    End Function

对于所有希望将其与理论和标准定义(如子式和代数余子式)联系起来的人,请注意:det(SubMatrix(i,j)) 是矩阵 A 的子式 M(i,j),而 A 的代数余子式 C(i,j) 定义为 (−1)i + j 乘以 A 的子式 M(i,j)。然后可以很容易地看出,上面的方程是拉普拉斯公式沿第 j 行的一个特定形式,当 j = 0 时。

高斯消元法

这种方法不利于良好的编程,因为它具有阶乘阶数,即 O(n!)。因此,它的效用对于较大的 n 值大大降低,因为它需要很长时间才能执行。为了避免这种情况,我们利用了矩阵的一些性质。其中一个在这里非常重要,它说明

将矩阵的一整行或一整列乘以一个常数,然后将由此产生的列从另一行或另一列中减去,不会改变矩阵的行列式。

通过对矩阵进行适当的操作,我们可以使任何行中的除一个元素外的所有元素都等于零。在这种情况下,设 j=Y 为一个常数行。从这里,我们需要展开矩阵。让我们将 k 定义为 Y 行中的第一个非零元素,其中我们必须让 Y = 0

        Dim Y As Integer = 0
        Dim k As Integer
        For i As Integer = 0 To M - 1
            If val(i, Y) <> 0 Then
                k = i
                Exit For
            End If
        Next

现在我们取这个非零元素 val(k,Y),并尝试使 Y 行中的所有其他元素都等于零。这可以通过利用一个属性来实现,并且最好通过一个例子来理解。设

在这里,取 Y = 0。那么 k = 1,因为元素 val(1,0) = 2 是 Y = 0 行中的第一个非零元素。现在我们分析这一整行。我们希望这一行中的所有元素(除一个外)都为零。因此,通过使用中间列,我们可以使所有其他列的顶端元素等于零(在这种情况下是元素 3)。换句话说,需要将列 i = k + 1 及之后的列置零,因为对于 j < k,元素已经为零。通过利用该属性,我们将以 2 为头的列乘以 (2/3)。这将使矩阵变为

注意上面的不等式。现在根据该属性,可以从最后一列减去中间列以恢复 A 。因此

请注意,中间列未更改;它保持与原始列相同。按第 0 行展开得到

这里,由于第一行中除一个元素外所有其他元素都为零,一个 3x3 矩阵只产生了一个 2x2 矩阵。这与前一种方法相比是一个很大的改进,在前一种方法中,一个 nxn 矩阵产生 n 个 (n-1xn-1) 矩阵。相反,这种方法总是从一个 nxn 矩阵产生一个 (n-1xn-1) 矩阵。

为了用数学方式(从而“以编程方式”)表达这一完整过程,我们写

对于所有 Screenshot - matrix61.gif

当然,如果一列的顶端元素已经是零,我们就不浪费时间再次将其置零。所以检查 val(i,Y) = 0

        Dim f As Double
        For i As Integer = k + 1 To M - 1
            If val(i, Y) <> 0 Then
                f = val(i, Y) / val(k, Y)
                For j As Integer = 0 To N - 1
                    NewMatrix.val(i, j) = val(i, j) - val(k, j) * f
                Next
            End If
        Next

这里,

是一个声明为 double 的因子。我们在上面的代码中写了 NewMatrix.val(i,j) = . . . ,而不是 val(i,j) = . . . 。这是一个小的内存节省步骤,解释如下。

在粗略的按行展开方法中,我们从特定行中取出一个元素,生成 SubMatrix,然后我们对新形成的矩阵执行相同的操作。在通过 SubMatrix 形成新矩阵后,我们没有丢弃旧矩阵,因为该旧矩阵中还有其他元素等待处理。然而,在此方法中,一旦形成 SubMatrix,我们就可以轻松丢弃旧矩阵,因为没有等待的元素。这是因为它们都为零。但是,重要的是我们不要丢弃原始矩阵,因为行列式方法不打算更改调用它的矩阵的内容。它可以随意处理它自己创建的矩阵或子矩阵,因此该方法的签名是

    Private Function GEDeterminant(ByVal DoClone As Boolean) As Double
        If M = 1 And N = 1 Then
            Return val(0, 0)
        End If

        Dim NewMatrix As Matrix
        If DoClone Then
            NewMatrix = Clone()
        Else
            NewMatrix = Me
        End If
        ...
    End Function

现在我们使用

        NewMatrix = NewMatrix.SubMatrix(k, Y) 'Save space
        temp += ((-1) ^ (k + Y)) * val(k, Y) * NewMatrix.GEDeterminant(False)
        Return temp

其中 temp 之前已通过以下方式声明为临时变量

        Dim temp As Double

Clone() 可以轻松编程为

    Public Overridable Function Clone() As Object Implements ICloneable.Clone
        Dim temp As Matrix = New Matrix(M, N)
        temp.val = val.Clone
        Return temp
    End Function

Public 函数的形式如下,因为我们需要 Clone() 原始矩阵

    Public Function GEDeterminant() As Double
        If IsSquare() Then
            Return GEDeterminant(True)
        Else
            Throw New Exception("Determinant exists only possible" 
                & _ "for a sqaure matrix.")
        End If
    End Function

最后

    Public Overrides Function ToString() As String
        Dim temp As String
        For Y As Integer = 0 To N - 1
            For X As Integer = 0 To M - 1
                temp &= val(X, Y) & ","
            Next X
            temp &= Chr(13)
        Next Y
        Return temp
    End Function

关注点

有趣的是,在编程 GEDeterminant() 方法时,我不知道它被称为高斯消元法。按行展开根本不实用。例如,一个 10x10 矩阵的 O(n!) 行列式会导致几十秒的延迟,而对于高斯消元法 O(n3) 的一千个 10x10 矩阵,延迟仅为 200 毫秒。

这个矩阵类帮助我解决了许多不同领域的问题。使用克莱姆法则求解联立线性方程组是矩阵的一个重要用途。请记住,我在文章前面使用了“可以相加或相乘的抽象实体”这个短语。对于本文,我们使用了 Double 作为抽象实体,但使用自定义的 Polynomial 可以帮助我们解决复杂的电气电路——就像 SPICE 所做的那样——使用修改后的节点分析。矩阵的另一个重要应用是我广泛使用的,即从任意数量的给定样本中逼近多项式函数。逼近通常可以是任何形式,包括指数、正弦等。正弦形式的逼近导致了傅里叶级数——包括连续和离散——的形成,从而开辟了一个全新的探索世界。

历史

  • 2007 年 5 月 14 日 - 发布原版本
© . All rights reserved.