CSCI 1200 - 作业 3 - 动态矩阵
作业要求
Details
在本次作业中,你需要构建一个名为 Matrix 的自定义类,该类将模仿传统的矩阵(matrix 的复数形式)。你不需要深入了解矩阵的知识,但如果你感兴趣的话可以在网上阅读更多关于它的内容:矩阵 (数学)。
矩阵在许多不同应用中被使用,并且多年来已经开发了许多优化、技巧和数值方法来快速处理矩阵运算并解决更复杂的问题。
构建这个数据结构将让你练习指针的使用,动态数组分配与释放以及二维指针。实现该数据结构需要编写一个新类。你不能使用任何 STL 容器类或除了 Matrix 之外的其他附加类或结构体。你需要使用 new
和 delete
关键字。你可以使用数组索引 []
。请在开始你的实现之前阅读整个作业说明。
数据结构
矩阵是一个二维数字排列。在这个作业中,我们假设每个矩阵包含双精度数(doubles)。我们用 m 行和 n 列的矩阵大小表示为 m×n 矩阵。例如,下面显示的是一个 4×3 的矩阵:
$$ \begin{bmatrix} -6 & 10 & 1 \\ 3 & -8 & 22 \\ -17 & 4 & 7 \\ 2 & 5 & 0 \end{bmatrix} $$
我们将使用二维数组表示 Matrix 类中的数据。因为矩阵可能具有任意大小,你需要使用动态内存来完成这个任务。上面显示的相同矩阵可以这样表示:

我们将用 $a_{i,j}$ 表示矩阵 A 中第 i 行和第 j 列的值。因此,一个通用矩阵可以描述为:
$$ A = \begin{bmatrix} a_{0,0} & a_{0,1} & \cdots & a_{0,n-2} & a_{0,n-1} \\ a_{1,0} & a_{1,1} & \cdots & a_{1,n-2} & a_{1,n-1} \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ a_{m-2,0} & a_{m-2,1} & \cdots & a_{m-2,n-2} & a_{m-2,n-1} \\ a_{m-1,0} & a_{m-1,1} & \cdots & a_{m-1,n-2} & a_{m-1,n-1} \end{bmatrix} $$
基本功能
类的私有部分将相对较小,主要挑战在于在实现特性以使类具有功能性时处理动态内存。你可以按任何顺序实现这些方法;我们首先提到一些会使得调试更容易的方法。
建议你先编写一个构造函数,它接受两个 unsigned int
类型:行数和列数以及一个双精度填充值。该构造函数应创建一个表示为 m×n 的数据结构,并将每个值初始化为 fill。如果任一维度为 0,则生成的矩阵大小应为 0×0。
你的类必须支持相等运算符 ==
和不等运算符 !=
。两个矩阵被认为是相等的,当且仅当它们在每一个位置上的值都相同。换句话说,矩阵 A 和 B 相等如果并且仅如果
$$(\forall_{i, j}|i \in {0,1,\ldots,m-2,m-1}, j \in {0,1,\ldots,n-2,n-1})a_{i,j} = b_{i,j}$$
$\forall$ 是一个常见的缩写,表示“对于所有”,因此 $\forall_{i, j}$ 表示“对每一个 i 和 j 的值”。$\in$ 也是一个常见的缩写,表示“属于”。
由于矩阵具有两个维度,你需要实现 num_rows()
和 num_cols()
方法,分别返回矩阵中的行数和列数。我们可能希望将一个已填充的矩阵变为一个空矩阵,因此你必须编写一个 clear()
方法。此方法应重置行数和列数为 0,并释放 Matrix 当前持有的任何内存。
当然我们需要能够访问存储在 Matrix 类中的数据。为此我们将实现一个“安全”的访问器叫做 get()
,它接受一行、一列以及一个双精度值。如果行和列在矩阵范围内,则应将 arow,col
的值存入 double
中,并返回 true
;否则函数应该返回 false
。
与访问数据类似的任务是能够在矩阵的特定位置设置一个值。这是通过安全修改器 set()
来完成的。此函数也接受一行、一列以及一个双精度值。如果位置有效,set()
应该返回 true
并将 arow,col
设置为传入 double
的值;否则返回 false
。
重载输出运算符
在某个时候,编写用于输出的方法可能是个好主意。与之前类中我们写方法来执行打印不同,我们将依赖于非成员重载的 operator<<
。我们以前练习过重载其他操作符以调用 std::sort()
,这与此类似。在 Matrix 类定义之外但在你的 .cpp 和 .h 文件中,你应该编写以下 operator:
|
|
这将允许我们按顺序打印一个或多个输出。如果您的 operator<<
实现正确,则下面的代码应该都能正常工作:
|
|
假设在上述示例中:
$$ m1 = \begin{bmatrix} \quad \end{bmatrix}, \quad m2 = \begin{bmatrix} 3 & 5.21 \\ -2 & 4 \\ -18 & 1 \end{bmatrix} $$
则输出应该类似于:
|
|
我们忽略空白,但我们期望你的操作符输出矩阵元素的顺序正确,并且大小输出在矩阵之前并遵循下面所示格式 - 每行一个,元素之间有空格。请注意,即使这些示例中对齐也不理想。我们更希望你专注于实现 Matrix 类的正确性以及处理内存问题而不是关注使输出漂亮。
简单矩阵运算
首先介绍一些基本的矩阵运算。第一个是方法 multiply_by_coefficient()
,它接受一个名为系数的 double 类型参数。该方法应将矩阵中的每个元素乘以系数。例如:
$$ m1 = \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}, \quad m1\text{.multiply\_by\_coefficient(5)} \Longrightarrow \begin{bmatrix} 5 & 10 \\ 15 & 20 \end{bmatrix} $$
另一个常见的操作是交换矩阵的两行。这将通过方法 swap_row()
来完成,该方法接受两个类型为 unsigned int
的参数:源行号和目标行号。如果这两行都在矩阵范围内,则函数应该交换这两个行中的值并返回 true;否则应返回 false。例如:
$$ m1 = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}, \quad m1\text{.swap\_row(1,2)} \Longrightarrow \begin{bmatrix} 1 & 2 & 3 \\ 7 & 8 & 9 \\ 4 & 5 & 6 \end{bmatrix} $$
注意
完成基本函数和 swap_row()
后,可以调用提供的 rref()
函数的测试。我们在这里不详细解释该函数,并且你不需要了解它的工作原理,但计算行阶梯形矩阵(RREF)可以用于找到逆矩阵,在许多领域中非常重要。我们使用一种简单的实现方法称为高斯-约旦消元法,你可以在此处阅读更多:高斯消去法。有其他更好的技术来寻找 RREF,但我们选择这种方法是因为它的简单性。
“翻转”矩阵是一个常见的需求,这个过程叫做转置。你需要编写 transpose()
方法,其返回类型为 void。形式上,$m \times n$ 矩阵 $A$ 转置成 $n \times m$ 矩阵 $A^T$ 定义如下:
$$(\forall_{i, j}|i \in {0,1,\ldots,m-2,m-1}, j \in {0,1,\ldots,n-2,n-1})a_{i,j}^{T} = a_{j,i}$$
$$ m1 = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}, \quad m1\text{.transpose\_row(1,2)} \Longrightarrow \begin{bmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{bmatrix} $$
双矩阵运算
双矩阵运算是涉及两个矩阵的操作。为了简化,我们将其写为类定义中的方法(不是操作符),因此当前的 Matrix 对象始终是“左部”矩阵 $A$。你需要实现 add()
和 subtract()
方法。这两个函数只接受一个参数,第二个 Matrix 类型的对象,我们将称之为 $B$,如果 $A$ 和 $B$ 的维度匹配,则修改 $A$。如果维度匹配,函数应该返回 true;否则它们应返回 false。两个矩阵的加法和减法形式定义如下:
$$ (\forall_{i, j}|i \in {0,1,\ldots,m-2,m-1}, j \in {0,1,\ldots,n-2,n-1})C_{i,j} = a_{i,j} + b_{i,j} \\ (\forall_{i, j}|i \in {0,1,\ldots,m-2,m-1}, j \in {0,1,\ldots,n-2,n-1})D_{i,j} = a_{i,j} - b_{i,j} $$
考虑这两个矩阵:
$$ m1 = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}, \quad m2 = \begin{bmatrix} 4 & 16 & 25 \\ 14 & 3.4 & 3.64159 \end{bmatrix} $$
$$ m1 + m2 = \begin{bmatrix} 1 + 4 & 2 + 16 & 3 + 25 \\ 4 + 14 & 5 + 3.4 & 6 + 3.64159 \end{bmatrix} = \begin{bmatrix} 5 & 18 & 28 \\ 18 & 8.4 & 9.64159 \end{bmatrix} $$
$$ m1 - m2 = \begin{bmatrix} 1 - 4 & 2 - 16 & 3 - 25 \\ 4 - 14 & 5 - 3.4 & 6 - 3.64159 \end{bmatrix} = \begin{bmatrix} -3 & -14 & -22 \\ -10 & 1.6 & 2.35841 \end{bmatrix} $$
更复杂的矩阵运算
如果要获取整个行或列的内容,使用 get()
逐个提取值会很烦人,尤其是因为我们的实现是一个“安全”的访问器,所以我们不能使用一些通常使用的编码技巧。为了解决这个问题,你需要再实现两个访问器方法:get_row()
和 get_col()
。这两个函数都接受一个无符号整数并返回一个 double*
类型的值。对于 get_row()
,参数是需要获取的行号;而对于 get_col()
参数则是要获取的列号。如果请求的行/列超出矩阵范围,则方法应返回一个指向 NULL 的指针。
我们期望你实现的最后一个方法 quarter()
不是一个传统的矩阵操作。该方法不接受任何参数并返回一个包含四个新 Matrix 元素的 Matrix*
,顺序为:左上(UL)象限、右上(UR)象限、左下(LL��象限和最后是右下(LR)象限。所有四个象限应该具有相同的大小。记住当函数结束时所有的局部变量都会超出作用域并被销毁,所以你需要特别小心如何构建和返回这些象限。在下一页有两个关于 quarter 操作的例子:
$$ m1 = \begin{bmatrix} 1 & 2 & 3 & 4 \\ 5 & 6 & 7 & 8 \end{bmatrix}, \quad m2 = \begin{bmatrix} 1 & 2 & 3 & 4 \\ 5 & 6 & 7 & 8 \\ 9 & 10 & 11 & 12 \end{bmatrix} $$
$$ m1^{(\mathrm{UL})} = \begin{bmatrix} 1 & 2 \end{bmatrix}, \quad m1^{(\mathrm{UR})} = \begin{bmatrix} 3 & 4 \end{bmatrix}, \quad m1^{(\mathrm{LL})} = \begin{bmatrix} 5 & 6 \end{bmatrix}, \quad m1^{(\mathrm{LR})} = \begin{bmatrix} 7 & 8 \end{bmatrix} $$
$$ m2^{(\mathrm{UL})} = \begin{bmatrix} 1 & 2 \\ 5 & 6 \end{bmatrix}, \quad m2^{(\mathrm{UR})} = \begin{bmatrix} 3 & 4 \\ 7 & 8 \end{bmatrix}, \quad m2^{(\mathrm{LL})} = \begin{bmatrix} 5 & 6 \\ 9 & 10 \end{bmatrix}, \quad m2^{(\mathrm{LR})} = \begin{bmatrix} 7 & 8 \\ 11 & 12 \end{bmatrix} $$
测试和调试
我们提供了一个 matrix_main.cpp 文件,其中包含对 Matrix 类的各种测试。一些这些测试最初被注释掉了。我们建议你在基本测试上让类运行正常,并在实现和调试剩余功能时取消注释额外的测试。研究提供的测试用例以了解参数和返回值。
注意:不要编辑提供的 matrix_main.cpp 文件,除非是为了取消注释提供的测试用例并添加指定位置的新测试用例。
assert()
函数在整个测试代码中使用。这是 C 和 C++ 中都可用的一个函数,如果条件为真则不做任何事情,如果条件为假则立即崩溃。如果条件为假,则你的命令行应该显示在崩溃前的断言失败信息。
我们建议使用内存调试工具来查找内存错误和内存泄漏。关于安装和使用“Dr. Memory”(适用于 Linux/MacOSX/Windows)和“Valgrind”(适用Linux/MacOS)的信息可以在课程网页上找到:https://www.cs.rpi.edu/academics/courses/fall23/csci1200/memory_debugging.php
作业提交服务器也将使用 Dr. Memory 运行你的代码以查找内存问题。为了获得满分,你的程序必须是无内存错误和无内存泄漏的。
任务及提供的代码
你必须根据本说明实现 Matrix 类。你的类应该分为一个 .cpp 文件和一个 .h 文件。你还应在 matrix_main.cpp 的 StudentTest() 函数中添加一些额外测试用例。在实现类时,请特别注意正确地实现拷贝构造函数、赋值运算符和析构函数。
务必编写自己的新测试用例,并不要忘记注释代码!使用提供的模板 README.txt 文件来写你需要评分者阅读的笔记。在 README.txt 文件中填写顺序表示部分。你必须独自完成此作业,如“合作政策与学术诚信”说明所述。如果你讨论了该作业、问题解决技巧或错误消息等,请在你的 README.txt 文件中列出他们的名字。
截止日期: 2025年2月6日(星期四),晚上10点。
评分标准
20分
- README.txt 完成情况 (3 分)
- 缺少姓名、合作者或时间中的一个 (-1)
- 缺少姓名、合作者或时间中的两个以上 (-2)
- 没有反思 (-1)
- 类声明和实现及编码风格(良好的类设计,分为 .h 和 .cpp 文件。超过一行的函数放在 .cpp 文件中。组织合理的类实现,并在适当的地方添加注释。正确使用 const/const& 和类方法 const) (5 分)
- 没有信用 (显著不完整的实现) (-5)
- Matrix 自身没有提供文档(函数文档和部分标题不算)。(-1)
- 函数体中包含多于一条语句的放在 .h 文件中。(模板类可以例外)(-2)
- 函数在 .h 或 .cpp 文件中的注释不充分或注释质量差 (-1)
- 错误使用或遗漏了 const 和引用。(-1)
- 缩进过于紧凑,空白过多,缩进不良。(-1)
- 变量名选择不当:描述性不足的名称(例如 ‘vec’, ‘str’, ‘var’),单字母变量名(除了循环计数器)等 (-2)
- 数据表示 (4 分)
- 没有信用 (显著不完整的实现) (-4)
- 使用 STL 数据结构(列表���向量等)。(-4)
- 成员变量是公开的。(-2)
- 顺序符号 (README 包含正确的顺序符号分析,包括适当的符号和提供的变量使用) (5 分)
- 额外测试用例 (广泛的额外学生编写测试用例) (4 分)
- 没有测试
transpose()
(-1) - 没有测试
multiply_by_coefficient()
(-1) - 没有测试
get_col()
(-1) - 没有测试某种情况的边界条件(行或列 = 0,奇数维度分块等)(-1)
- 没有测试用例,或者只是极小的测试用例,只测试了 SimpleTests 中涵盖的内容 (-4)
- 没有测试
支持文件
matrix_class.7z程序设计
首先,我们需要确定 Matrix 类的设计。根据提供的信息,我制作了一个图。
classDiagram class Matrix { %% Data members - rows : unsigned int - cols : unsigned int - data : double** %% Private Methods - allocateMemory() - deallocateMemory() %% Constructors + Matrix() %% Destructor + ~Matrix() %% Public Methods + operator=() + get_rows() // 更改为 get_rows 和 get_cols,符合中文习惯 + get_cols() + clear() + get() + set() + multiply_by_coefficient() + swap_row() + transpose() + add() + subtract() + get_row() + get_col() + quarter() + operator==() + operator!=() %% Friend Operator + operator<<() }classDiagram class Matrix { %% Data members - rows : unsigned int - cols : unsigned int - data : double** %% Private Methods - allocateMemory() - deallocateMemory() %% Constructors + Matrix() %% Destructor + ~Matrix() %% Public Methods + operator=() + get_rows() // 更改为 get_rows 和 get_cols,符合中文习惯 + get_cols() + clear() + get() + set() + multiply_by_coefficient() + swap_row() + transpose() + add() + subtract() + get_row() + get_col() + quarter() + operator==() + operator!=() %% Friend Operator + operator<<() }
踩坑内容
- 矩阵的大小不一定总是奇数。因此,当处理接近边缘的情况时需要额外努力。
- 我们需要编写一些测试用例,并且这些用例将由隐藏自动评分器进行测试。
解决方案
Matrix.h
|
|
Matrix.cpp
|
|
matrix_main.cpp(包含更多测试用例)
|
|
相关内容
- CSCI 1200 - 作业 2 - 设计一个简单的 Uber
- CSCI 1200 - 作业 1 - Spotify播放列表
- CSCI 1100 - 作业 8 - 熊、浆果与游客重聚:类
- CSCI 1100 - 作业 7 - 字典
- CSCI 1100 - 作业 6 - 文件、集合和文档分析