蔡诺文负责部分:
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 540 | 800 |
· Analysis | · 需求分析 (包括学习新技术) | 10 | 10 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 30 | 50 |
· Coding | · 具体编码 | 480 | 600 |
· Code Review | · 代码复审 | 20 | 10 |
· Test | · 测试(自我测试,修改代码,提交修改) | 40 | 30 |
Reporting | 报告 | 60 | 50 |
· Test Report | · 测试报告 | 10 | 20 |
· Size Measurement | · 计算工作量 | 30 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 720 | 850 |
实验环境:
QT Creator 4.12.2
C++语言
Windows 10家庭版
实验目标:三阶段
第1阶段
写一个能自动生成小学四则运算题目的命令行 “软件”, 分别满足下面的各种需求。
下面这些需求都可以用命令行参数的形式来指定:
a) 一次可以出一千道道题目,并且没有重复的,把题目写入一个文件中。
任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
b) 当有多于一个运算符的时候,如何对一个表达式求值?逐步扩展功能和可以支持的表达式类型,最后希望能支持下面类型的题目 (最多 10 个运算符,括号的数量不限制):
25 - 3 * 4 - 2 / 2 + 89 = ?
1/2 + 1/3 - 1/4 = ?
(5 - 4 ) * (3 +28) =?
c) 除了整数以外,还要支持真分数的四则运算。 (例如: 1/6 + 1/8 = 7/24 )
d) 让程序能接受用户输入答案,并判定对错。 最后给出总共 对/错 的数量。
第2阶段
增加一个运算符,要支持乘方(power)运算。乘方运算的优先级高于乘除法。如何表示乘方,有两种表示方法:
4 ^ 2 = 16, 4 的二次方等于 16。 这里, ^ 表示乘方
4 ** 2 = 16, 4 的二次方等于16。 这里, ** 表示乘方 (** 之间不能有空格,否则是错误的算式)
两种表示方法都要支持,可以通过设置来选择。
第3阶段
结对的同学商量一下,从以下几个方向中选择一个,对程序进行扩展。
- 把程序变成一个 Windows/Mac/Linux 电脑图形界面的程序 (取决于你目前使用的电脑),同时增加 “倒计时” 功能, 每个题目必须在 20 秒钟完成,如果完不成,则得0 分并进入下一题。增加“历史纪录” 功能, 把用户做题的成绩记录下来并可以展现历史记录。
- 把程序变成一个智能手机程序 (你正在用什么手机, 就写那个手机的程序), 增加倒计时,和历史纪录功能(见上)。
- 把程序变成一个网页程序, 用户通过设定参数,就可以得到各种题目。
- 选一个你从来没有学过的编程语言,试一试实现基本功能。 估计做好这个软件需要的时间,并且写出大概的设计步骤和实现算法。
- 把这个程序的思路变成一个可以一步一步演示的动画。写一个带有图形界面的程序:
输入:一个正常的四则运算句子
输出:程序用动画表示计算的过程,后序转换的过程,处理不同运算符优先级的过程, 根据调度场工作的原理,逐步算出得出结果的过程。
实验过程:
第一阶段:
-
整个UI界面设计、实现
使用QT Creator设计Windows桌面应用。界面如下:
-
有关组件的信息传递、调用过程
主要是界面中两个按钮的点击事件,实现了生成和输出任意数目的随机算式和判断正误功能。
代码如下:
void MainWindow::on_buttonGenerate_clicked() { QString ba = ui->num->text(); num = ba.toInt(); if(cnt == 0) { if(num == 0 || num >1000)//判断输入合法性 { QMessageBox::warning(NULL, "提示", "请填写正确的数字", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); } else { cnt++; ui->scrollAreaWidgetContents->setFixedHeight(num * 50);//动态调整widget大小实现缩放 QGridLayout *q = new QGridLayout(ui->scrollAreaWidgetContents); ui->scrollAreaWidgetContents->setLayout(q); gentree(num);//生成树并计算结果、判重 for(int i = 0; i < num; i++) { printtree(cop[i], i); QLabel *l = new QLabel(); QString name = "lbl" + QString::number(i); l->setObjectName(name); l->setFixedHeight(50); l->setMargin(5); l->setText(save[i]); q->addWidget(l,i,0); QLineEdit *p = new QLineEdit(); name = "edt" + QString::number(i); p->setObjectName(name); //p->setText(answer[i]); p->setFixedWidth(200); p->setFixedHeight(30); q->addWidget(p,i,1); } writeIntxt(num); } } } void MainWindow::on_confirm_clicked() { int correct = 0;//正确数目 char tmp[100]; //QGridLayout *q = new QGridLayout(ui->scrollAreaWidgetContents); for(int i = 0; i < num; i++) { QString name = "edt" + QString::number(i); QLineEdit *edit = ui->scrollAreaWidgetContents->findChild<QLineEdit*>(name); //printf("%s %s",answer[i], edit->text()); if(edit->text()==QString::fromLocal8Bit(answer[i])) { correct++; } } double rate = (double)correct / (double)num; gcvt(rate, 3, tmp); ui->labelCheck->setText(tmp); }
-
分数计算
实数计算由队友完成,自己完成了分数的计算和对0的处理问题
if(first->type==1)//分数 { int fenMu = 0; int fenZi = 0; switch (symbol->op) { case 0: //加法 fenMu = first->fenmu * second->fenmu; fenZi = first->fenzi * second->fenmu + second->fenzi * first->fenmu; symbol->type = 1; break; case 1://减法 fenMu = first->fenmu * second->fenmu; fenZi = first->fenzi * second->fenmu - second->fenzi * first->fenmu; symbol->type = 1; break; case 2://乘法 fenMu = first->fenmu * second->fenmu; fenZi = first->fenzi * second->fenzi; symbol->type = 1; break; case 3://除法 if(second->fenzi == 0) symbol->type = -1; else { fenMu = first->fenmu * second->fenzi; fenZi = first->fenzi * second->fenmu; symbol->type = 1; } break; } int m0 = fenMu, n0 = fenZi; //m0和n0保存m和n的原始值 if(fenZi == 0) { symbol->fenzi = 0; symbol->leftchild = nullptr; symbol->rightchild = nullptr; symbol->fenmu = fenMu; } else { while (fenMu % fenZi != 0) { int temp = fenMu ; fenMu = fenZi; fenZi = temp % fenZi; } symbol->fenmu = m0 / fenZi; symbol->fenzi = n0 / fenZi; symbol->leftchild = nullptr; symbol->rightchild = nullptr; } }
-
将生成的算式储存在txt中
利用qfile将生成的算式存储在txt中(二叉树的形成和中序输出由队友完成)
void writeIntxt(int total)//将式子写入txt文件,答案存储在数组里 { QFile file("D:/git/Calculator/save.txt"); if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; } else { for(int i = 0; i < total; i++) { file.write(save[i]); if(roots[i]->type == 0)//整数 { itoa(roots[i]->fenzi, tmp2, 10); strcat(answer[i], tmp2); } else { itoa(roots[i]->fenzi, tmp2, 10); strcat(answer[i], tmp2); strcat(answer[i], "/"); itoa(roots[i]->fenmu, tmp2, 10); strcat(answer[i], tmp2); } file.write("\n"); //file.close(); } } }
-
判断重复
利用二叉树的性质较为方便的完成了查重操作,也正是老师对查重操作的细致描述让我们想到了用二叉树的思想来解决这个问题。
bool search(NODE* already, NODE* present)//递归检查 { if(already == nullptr && present == nullptr) return true; else if(already == nullptr || present == nullptr) return false; else { if(already->type == present->type && already->fenzi == present ->fenzi && already->fenmu == present->fenmu && already->op == present->op) { if(search(already->leftchild,present->leftchild) && search(already->rightchild,present->rightchild)) { return true; } else if(already->op==1 || already->op == 3)//判断是否可以交换 { return false; } else if(search(already->leftchild,present->rightchild) && search(already->rightchild,present->leftchild)) { return true; } else { return false; } } else return false; } } bool checkIfDuplicated(NODE* check, int prev)//检查是否重复 待检测根节点、节点前已有算式数 { int temp = 0; for (int i = 0; i < prev; i++) { temp+= search(roots[i],check); } if(temp != 0) return true; else return false; }
-
判断对错、计算正确率
在“交卷”按钮响应事件中完成了判断对错和计算正确率操作。
代码如下:
void MainWindow::on_confirm_clicked() { int correct = 0;//正确数目 char tmp[100]; //QGridLayout *q = new QGridLayout(ui->scrollAreaWidgetContents); for(int i = 0; i < num; i++) { QString name = "edt" + QString::number(i); QLineEdit *edit = ui->scrollAreaWidgetContents->findChild<QLineEdit*>(name); //printf("%s %s",answer[i], edit->text()); if(edit->text()==QString::fromLocal8Bit(answer[i])) { correct++; } } double rate = (double)correct / (double)num; gcvt(rate, 3, tmp); ui->labelCheck->setText(tmp); }
7.小结
第一阶段实现了软件最主要的功能,构建好了主要框架。功能的实现依靠上述的几个功能函数,Qt中界面的按钮、文本等也通过独立的函数来实现。将功能封装进函数里,函数之间只有互相调用的关系,相互之间没有其他影响,独立性高,是松耦合结构。模块间的高独立性使得我们在分工、实现、合并、修改等都非常方便。如上所示,我们分工完成了上述的功能模块,一个模块的编写过程中不受其他模块影响,只有接口调用,各自编写负责的模块时不受对方干扰,写完后直接合并,非常方便。修改时,也只需要改动目标模块,而其他模块不受影响。
函数之间的接口调用关系如下:
画出UML图——顺序图如下:
对软件进行性能分析。CPU性能分析:采用直接查看内存的方法,当运行软件后,内存占用率不变。Memory性能分析:整个程序大小为120KB,save.txt文档大小仅为1KB,对存储器几乎不造成影响。
单元测试
软件开发过程中,采用边开发边进行单元测试的方式。每编写好一个模块,直接将该模块复制到dev-c++中,输入输出数据进行测试。编写测试用例进行测试,需要考虑到各种情况。以下测试中,对模块单独检测的方法均采用将模块复制到dev-c++中,手动输入数据,并使用printf查看输出数据的方式。
1.输入生成数据1,生成数据1000,生成数据50,都能够正确生成
2.输入生成数据1500,生成数据0,系统提示“请填写正确的数字”
3.对checkIfDuplicated()
模块单独检测(内含search()
子函数),输入两个完全相同的二叉树,结果返回true,证明判断结果重复,结果正确
4.对checkIfDuplicated()
模块单独检测(内含search()
子函数),输入两个高度相同但数值不同的二叉树,结果返回false,判断未重复,结果正确。
5.对real_calcu()
模块单独检测,输入两个整数类型叶子节点和一个运算符类型节点,返回含正确结果的节点,且结果节点类型为整数类型(即type=0)
6.对real_calcu()
模块单独检测,输入两个分数类型叶子节点和一个运算符类型节点,返回含正确结果的节点,且结果节点类型为分数类型(即type=1)
7.对calcu()
模块单独检测(内含real_calcu()
子函数),输入一颗算式二叉树,内含除零操作,返回节点type=-1,表示二叉树不合法。结果正确
8.对calcu()
模块单独检测(内含real_calcu()
子函数),输入一颗算式二叉树,内含乘方操作,返回正确的结果
9.对calcu()
模块单独检测(内含real_calcu()
子函数),输入一颗算式二叉树,不含乘方操作,但二叉树结构较复杂。返回正确的结果
10.对printtree()
模块单独检测,复制到dev-c++后将写入文件操作改为直接printf输出操作。输入乘法节点含两个加法子节点的二叉树,检测括号的输出。结果正确。
11.对printtree()
模块单独检测,复制到dev-c++后将写入文件操作改为直接printf输出操作。输入某符号节点中square!=0的二叉树,检测乘方的输出。结果正确。
第二阶段:
第二阶段的任务比较简单,队友负责写好乘方的运算,自己负责完成两种乘方表示方法的选择。
最终实现界面如下:
默认会选中第一种乘方生成方式,且运行正常。主要方法是在struct结构中增加一个square属性,默认为0,值为1时选择第一种,值为2时选第二种。
typedef struct node{
int type=0;//type==0,整数 type==1,分数 type==2,运算符
long long int fenzi=0;
//type==0,整数的值=分子的值
//type==1,分数的值=分子的值/分母的值
long long int fenmu=0;
//只有type==1时该值有意义
int op=0;
//type==2,op==0:+ op==1:- op==2:* op==3:/
//只有type=2时该值有意义
int square=0;//square==1:^;square==2: **
int e=1;//指数
struct node* leftchild;
struct node* rightchild;
}NODE;
对radioButton的操作如下:
QButtonGroup *bg=new QButtonGroup(this);
bg->addButton(ui->radioButton1, 1);
bg->addButton(ui->radioButton2, 2);
int sel=bg->checkedId();//取到你所选的radioButton的值
int cftype = 0; // 乘方表示
int stop = 1;
switch(sel)
{
case 1:
cftype = 1;
stop = 1;
break;
case 2:
cftype = 2;
stop = 1;
break;
default:
stop = 0;
QMessageBox::warning(NULL,
"提示", "请选择使用的乘方表示方法",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
break;
}
if(stop == 1)
{
cnt++;
gentree(num, cftype);//生成树并计算结果、判重
........
其中cftype即选择的方式,我将其添加到了两个树生成函数中以便使用。
NODE* gen(int depth,int type, int cftype)
void gentree(int total, int cftype)
具体实现方面由队友完成。
第三阶段:
我们团队选择了方案1:
把程序变成一个 Windows/Mac/Linux 电脑图形界面的程序 (取决于你目前使用的电脑),同时增加 “倒计时” 功能, 每个题目必须在 20 秒钟完成,如果完不成,则得0 分并进入下一题。增加“历史纪录” 功能, 把用户做题的成绩记录下来并可以展现历史记录。
主要有四部分功能:
1.电脑图形界面(已完成)
2.倒计时功能(由队友完成)
3.将原来的全部显示并最后完成交卷改为单独题目显示逐题判断
4.历史记录功能
自己主要完成了后两部分操作。
将原来的全部显示并最后完成交卷改为单独题目显示逐题判断:
更改原先的“交卷”按钮,改为:
① “下一题”,若未到达最后一题
②“交卷”,若到达最后一题
将原来的全部同时显示改为逐题显示,具体操作可以参考最后完成的录屏视频。
修改思路:
为了实现后续的“历史记录”功能,已作答的题目不能删除,而是使用“隐藏”方法,在点击下一题后,自动判断正误,修改正确率为随时更新(正确的题目数/目前做完的题目数)。
每一次点击进行判断,如果当前已经做到num - 1题,则修改按钮文字为“交卷”,下一次点击则直接判分,不再显示新的题目。
修改代码如下:
int correct = 0;//正确数目
void MainWindow::on_confirm_clicked()//下一题、交卷
{
char tmp[100];
QString nm = "edt" + QString::number(i);
QLineEdit *edit = ui->scrollAreaWidgetContents->findChild<QLineEdit*>(nm);
QString nm2 = "lbl" + QString::number(i);
QLabel *l = ui->scrollAreaWidgetContents->findChild<QLabel*>(nm2);
ui->answerShow->setText(answer[i]);
if(edit->text()==ui->answerShow->text())
{
correct++;
}
double rate = (double)correct / (double)(i + 1);
gcvt(rate, 3, tmp);
ui->labelCheck->setText(tmp);
edit->hide();
l->hide();//隐藏
i++;//准备呈现下一题
if(i >= num - 1)//输出完毕
{
ui->confirm->setText("交卷");
}
if(i < num)
{
QGridLayout *q =ui->scrollAreaWidgetContents->findChild<QGridLayout*>("layoutCreate");
QLabel *l = new QLabel();
QString name = "lbl" + QString::number(i);
l->setObjectName(name);
l->setFixedHeight(50);
l->setMargin(5);
l->setText(save[i]);
q->addWidget(l,i,0);
QLineEdit *p = new QLineEdit();
name = "edt" + QString::number(i);
p->setObjectName(name);
//p->setText(answer[i]);
p->setFixedWidth(200);
p->setFixedHeight(30);
q->addWidget(p,i,1);
}
}
添加历史记录功能
添加按钮“历史答题”。定义全局变量vis,初始化为0,点击按钮,将之前隐藏的控件全部显示,并将vis取反。下一次点击时,再将其隐藏。
具体思路:
通过每个控件的objectName找到每个控件,用show()和hide()来控制显示和隐藏即可。
代码如下:
bool vis = 0;
void MainWindow::on_history_clicked()
{
if(vis == 0)
{
vis = !vis;
for(int j = 0; j < i; j++)
{
QString nm = "edt" + QString::number(j);
QLineEdit *edit = ui->scrollAreaWidgetContents->findChild<QLineEdit*>(nm);
QString nm2 = "lbl" + QString::number(j);
QLabel *l = ui->scrollAreaWidgetContents->findChild<QLabel*>(nm2);
edit->show();
l->show();
}
}
else
{
vis = !vis;
for(int j = 0; j < i; j++)
{
QString nm = "edt" + QString::number(j);
QLineEdit *edit = ui->scrollAreaWidgetContents->findChild<QLineEdit*>(nm);
QString nm2 = "lbl" + QString::number(j);
QLabel *l = ui->scrollAreaWidgetContents->findChild<QLabel*>(nm2);
edit->hide();
l->hide();
}
}
}
测试与修改
整个项目完成后,需要对软件性能、正确性等进行测试,以发现问题并改正。测试中发现,“历史记录”中,已经做过的题目的答案类型为QLineEdit,且类型为可读写,即可修改答案,非常不合理。于是将历史记录的答案改为只读型QLineEdit。一行代码实现:
edit->setFocusPolicy(Qt::NoFocus);//下一题之后做过的题设置为只读
生成的题目及答案都保留在save.txt文档中,便于测试正确性。测试中发现,当式子中含有乘方运算时,结果有很大概率会出错。阅读代码没有发现问题,为此我使用按行qDebug的方式,发现在calcu()
函数和real_calcu()
函数中都有乘方运算,calcu()
函数中对本节点进行乘方运算,real_calcu()
函数对子节点进行乘方运算。由于一个节点运算后,还会作为子节点再进行运算,会被重复进行乘方运算,导致结果的错误。由于叶子节点只能作为子节点被乘方,根节点只能作为本节点被乘方,两个函数中都不能删掉乘方运算,而控制条件又比较复杂,因此我的改正方法是,一个节点进行完乘方运算后,将其square改为0,即不再进行乘方运算。该部分改动由我完成。
还有一个问题是运算结果出现了不正确的大负数。经检查后,是运算结果超出了int的表示范围。我将所有节点中的分子、分母的类型都由int改为long long int类型后,不再发现有该问题。
修改错误后,我们又进行了多次测试,界面没有任何问题,计算结果的正确性也得到保证。
使用说明
软件运行后,主界面如图所示:
在“生成个数”处填写要生成的算式的个数,按下“确定”按钮后,将会生成相应个数的算式。
如图所示,按下“确定”按钮立即显示第一题,同时开始倒计时20秒。若没有在20秒内点击“下一题”,系统将自动进入下一题,文本框内的答案会被自动提交。
点击“历史答题”会出现历史记录
历史记录的文本框为只读类型,已经作答的题目不可再更改答案。查看历史记录将会占用本题的时间,时间用尽后将会自动进入下一题。再次点击“历史答题”,将会隐藏历史记录。
“正确答案”部分显示的是上一题的答案,“正确率”显示已经作答的题中正确题目的比例。
当作答最后一题时,界面如图:
“下一题”按钮变为“交卷”按钮,点击交卷后三秒关闭窗口。
需要注意的是,分数运算结果以分数表示,需要月份。整数运算结果以整数表示,除法运算结果向下取整,如:1/2=0,3/2=1
总结
结对过程中,我和队友配合比较默契,分工明确。具体分工内容在“实验过程”部分已经有详细说明。我和队友的讨论主要在网上进行,部分截图如下:
结对编程是一种敏捷软件开发的方法。我和队友的结对更偏向于远程结对编程,各有分工。结对编程的优点是团队只有两个人,方便沟通交流,方便任务分配。缺点是人数较少,每个人的工作量较大,也无法集思广益。由于我和队友比较熟悉,之前也合作过项目,沟通比较顺利,编程习惯也大致相似,项目对接也很方便,整个项目的完成都相对顺利。我的优点是擅长写算法,编程不怕麻烦,代码出错率低,缺点是思维不够活跃。队友蔡诺文的优点是擅长写界面,思维活跃,编程效率高,缺点是怕麻烦。因此,我们分工的方法是各自负责擅长的领域,互相督促,互相学习,改正缺点。
在项目代码中,所有算法核心的代码都由独立的函数模块来实现,界面通过接口调用函数,将内容展示出来。如果更换界面,只需要将代码中与界面有关的内容更改,继续调用函数接口即可,核心代码无需变动。业务模型与用户界面分离度较高,符合MVC设计模式。
code contract(代码契约)和design by contract(契约式设计)都强调契约,优点是代码规范,不易出错,便于对接,缺点是比较麻烦。考虑到该项目比较简单,并且组内只有两个人,沟通也很顺利,并且多design by contract和code contract不是很熟悉,较为冷僻,因此本项目中没有采用。
程序设计中,我和队友达成共识,在负责自己的部分时,代码的模块独立性要强,以便于合并、交接工作的顺利进行,提高并行性,提高效率。程序有时会碰到异常,我们处理异常的方式是,找到问题出在哪一模块,再由该模块的负责人来修改该模块。
敏捷软件开发的方法。我和队友的结对更偏向于远程结对编程,各有分工。结对编程的优点是团队只有两个人,方便沟通交流,方便任务分配。缺点是人数较少,每个人的工作量较大,也无法集思广益。由于我和队友比较熟悉,之前也合作过项目,沟通比较顺利,编程习惯也大致相似,项目对接也很方便,整个项目的完成都相对顺利。我的优点是擅长写算法,编程不怕麻烦,代码出错率低,缺点是思维不够活跃。队友蔡诺文的优点是擅长写界面,思维活跃,编程效率高,缺点是怕麻烦。因此,我们分工的方法是各自负责擅长的领域,互相督促,互相学习,改正缺点。
在项目代码中,所有算法核心的代码都由独立的函数模块来实现,界面通过接口调用函数,将内容展示出来。如果更换界面,只需要将代码中与界面有关的内容更改,继续调用函数接口即可,核心代码无需变动。业务模型与用户界面分离度较高,符合MVC设计模式。
code contract(代码契约)和design by contract(契约式设计)都强调契约,优点是代码规范,不易出错,便于对接,缺点是比较麻烦。考虑到该项目比较简单,并且组内只有两个人,沟通也很顺利,并且多design by contract和code contract不是很熟悉,较为冷僻,因此本项目中没有采用。
程序设计中,我和队友达成共识,在负责自己的部分时,代码的模块独立性要强,以便于合并、交接工作的顺利进行,提高并行性,提高效率。程序有时会碰到异常,我们处理异常的方式是,找到问题出在哪一模块,再由该模块的负责人来修改该模块。
本次项目使用c++语言,我和队友都比较熟悉,在语法、用法、结构等几乎没有遇到困难。但在Qt界面、多线程等方面,我们有很多东西不了解,需要通过网络搜索。在算法方面,所有的思路都是我和队友两个人想出来的,所有的代码都是我们手写,遇到算法上的困难,网络上找不到答案,因此全部都由我和队友经过商量、讨论得到答案。不管是上网求解的过程,还是商量讨论的过程,对我们的知识、能力都是很大的提升。