软件创新作业--四则运算题目

0. 序言

这次软件创新的作业要求通过结对编程来实现一个四则运算题目生成的程序, 目的是为了体现软件开发过程中不可避免的团队合作, 只是这次结对编程的要求是一位同学coding, 另一位同学在旁边检查. 之后身份互换. 不是同时进行, 并且没有提前计划.

ps: 本次合作的两人学号分别为2060118和2152118.


1. 题目简介

  • 小学老师要每周给同学出300道四则运算练习题。
  • 一或两个运算符(a+b或a+b+c),100 以内的数字,不需要写答案。
  • 需要检查答案是否正确,并且保证答案在 0..100 之间
  • 尽可能地多设置一些条件

ps: 特别说明的是, 我们使用C++进行编程, 实现的程序会生成2两个以上运算符, 并且会有答案


2. 解法思路

经过事先讨论, 确定了如下算法:

2.1 步骤分解

  • 一张试卷由多到题目组成, 那么先确定如何生成一道题目;

  • 一道题目由一个初始操作数和多个符号 + 操作数的组合.

2.2 算法过程

1.确定题目数量. 如300道.
2.生成一道题目逻辑.
   2.1 生成一个范围内的初始操作数. 如88, 该数入操作数队列.
   2.2 首次必定添加一个操作符和操作数. 如+号和12(结合2.1, 此时结果为100), 符号和操作数分别入符号队列和操作数队列.
      2.2.1 随机生成一个索引, 确定使用加法, 减法, 乘法还是除法.
      2.2.2 如果能得到合法的操作数. 不合法案例如随机到加法, 但是此时被加数是100(最大值),那么满足的加数只能是0. 0是无意义的, 所以得换用其它算法, 比如减法.
      2.2.3 直到找到一个合理的被操作数和符号,对上面的100来说, 比如减法100 - 10
   2.3 以某个概率, 比如0.5. 生成后续的操作数和符号. 如 88 + 10 * 2
3.构造题目
   通过符号队列和操作数队列生成题目内容, 和之前的答案一起保存起来.


3. 实现过程

为了更好的进行编程, 避免无计划时分别编码带来的歧义. 我们事先讨论并设计了不同类

3.1 类设计

Random类: 负责随机数相关, 比如以某一概率是否发生; 生成指定范围的随机数等.

Operator类: 拥有虚函数的基类, 规定了产生操作符和操作数的函数规范.

AddenGenerate类: Operator类的派生类, 实现了如何生成"+"号, 以及合法"加数"的逻辑.

DivisorGenerate类: Operator类的派生类, 实现了如何生成"/"号, 以及合法"除数"的逻辑.

MultiplierGenerate类: Operator类的派生类, 实现了如何生成"*"号, 以及合法"乘数"的逻辑.

SubtractorGenerate类: Operator类的派生类, 实现了如何生成"-"号, 以及合法"减数"的逻辑.

Paper类: 所有题目, 一道题目生成函数, 一个操作符和操作数生成函数均在这里, 并且提供产看等其它功能.

LinkedQueue类: 基于链表的队列, 这个类是借已经有的. 它用于保存操作数和操作符

TopicData结构体: 保存一道题目的内容和答案.

3.2 头文件

Random.h

#pragma once
#include <random>
#include <assert.h>

class Random
{
public:
	static int Range(int left, int right);  // value scope: left close, right open
	static float Range(float left, float right);
	static bool Happen(float possible);
private:
	static float __Random0To1();
	static std::random_device dev;
	static std::mt19937 rng;
	static std::uniform_real_distribution<float> dist; // distribution in range [1, 6]
};


Operator.h

#pragma once
#include "Random.h"

class Operator
{
public:
	virtual bool GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result)=0;
};


AddenGenerate.h

#pragma once
#include "Operator.h"

class AddenGenerate : public Operator {
public:
	bool GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result);
};

DivisorGenerate.h

#pragma once
#include "Operator.h"

class DivisorGenerate : public Operator {
public:
	bool GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result);
};

MultiplierGenerate.h

#pragma once
#include "Operator.h"

class MultiplierGenerate : public Operator {
public:
	bool GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result);
};

SubtractorGenerate.h

#pragma once
#include "Operator.h"

class SubtractorGenerate : public Operator {
public:
	bool GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result);
};

Paper.h

#pragma once
#include <vector>
#include "Operator.h"
#include "TopicData.h"
#include "LinkedQueue.h"
#include "Random.h"

#define NEW_NUMBER_AND_SYMBOL_POSSIBLE 0.6

inline LinkedQueue<int> operateNumQueue;  // 操作数队列
inline LinkedQueue<char> operateSymbolQueue;  // 操作符队列
inline std::vector<TopicData> topics;

class Paper
{
public:
	Paper(int topicAmount, int resMinValue, int resMaxValue, std::vector<Operator *> operators);
	void ShowPaper() const;
private:
	TopicData GenerateOneTopic(int resMinValue, int resMaxValue);  // 生成一道题
	bool GenerateNumAndSymbol(int initOperateNum, int resMinValue, int resMaxValue, int& result);  // 生成一个操作数和操作符
	string GenerateContent();  // 根据操作数队列和操作符队列, 组织成题目内容
	std::vector<Operator*> operators;  // 加减乘除操作数和操作符生成方法
};


LinkedQueue.h

#ifndef LINKEDQUEUE_H
#define LINKEDQUEUE_H
#include <iostream>
#include <cassert>
using namespace std;

// 这个代码是借用的
template <typename T>struct LinkNode{
	T data;
	LinkNode<T> *link;
	LinkNode(LinkNode<T> *ptr = NULL){
		link = ptr;
	}
	LinkNode(const T &item, LinkNode<T> *ptr = NULL){
		data = item;
		link = ptr;
	}
};

template <typename T>class LinkedQueue{//无头结点
public:
	LinkedQueue(){
		rear = NULL;
		front = NULL;
	}
	~LinkedQueue(){
		makeEmpty();
	}
	bool EnQueue(const T &x);
	bool DeQueue(T &x);
	bool getFront(T &x)const;
	void makeEmpty();
	bool IsEmpty()const{
		return front == NULL;
	}
	int getSize()const;
	friend ostream& operator << (ostream &os, LinkedQueue<T> &Q){
		LinkNode<T> *p = Q.front;
		int i = 0;
		while (p){
			os << "#" << ++i << ": " << p->data << endl;
			p = p->link;
		}
		os << "Queue Size: " << Q.getSize() << endl;
		return os;
	}
	void output();
protected:
	LinkNode<T> *front, *rear;
};

template <typename T>void LinkedQueue<T>::makeEmpty(){
	LinkNode<T> *p;
	while (front){
		p = front;
		front = front->link;
		delete p;
	}
}

template <typename T>bool LinkedQueue<T>::EnQueue(const T &x){
	//为下面的代码添加注释
	// 如果此时队列为空
	if (!front){
		// 将头和尾都指向这个新节点
		front = rear = new LinkNode<T>(x);
		if (!front)	return false;
	}
	// 如果队列不为空
	else{
		// 尾的下一个节点指向新节点
		rear->link = new LinkNode<T>(x);
		if (!(rear->link))	return false;
		// 更新尾
		rear = rear->link;
	}
	return true;
}

template <typename T>bool LinkedQueue<T>::DeQueue(T &x){
	if (IsEmpty())	return false;
	LinkNode<T> *p = front;
	x = front->data;
	front = front->link;
	delete p;
	return true;
}

template <typename T>bool LinkedQueue<T>::getFront(T &x)const{
	if (IsEmpty())	return false;
	x = front->data;
	return true;
}

template <typename T>int LinkedQueue<T>::getSize()const{
	LinkNode<T> *p = front;
	int k = 0;
	//补全代码
	while (p) {
		k++;
		p = p->link;
	}
	return k;
}

template <typename T>void LinkedQueue<T>::output(){
	LinkNode<T> *p = front; int i = 1;
	while (i <= getSize())	{
		cout << p->data << "  "<<endl;
		p = p->link;
        i++;
	}
}
#endif

TopicData.h

#pragma once
#include <string>

struct TopicData
{
	std::string content;
	int answer;
};

经过实现的计划, 能够比较好的规避歧义. 即使两个人交换着编写代码, 也会被束缚在已有的声明架构下. 编码者只关心函数的参数和返回值具体意义即可, 不需要直到函数具体实现细节


4. 遇到的问题

即使是已经有了计划, 结对得编程仍然出现了很多问题

  • 对代码理解的歧义: 虽然经过了提前计划, 但提前讨论不可能纠正两人的所有的理解偏差. 比如在实际中, 我(2060118)负责书写的Range函数用来生成一个随机整数, 它的范围是左闭右开的, 比如[1, 100)是不包括100的, 但同学(2152118)使用这个函数时以为包括100的. 这个情况是没有提前讨论到的.

  • 有时需要修改或者添加新的函数: 对接口的修改就不是一个人写代码时的随心所欲了, 接口任何的修改都要和对方沟通, 确定如何修改.

  • 排除Bug也变得麻烦: 由于每个人只熟悉自己书写的代码部分, 所以当bug产生的原因在两个人的代码里, 有时不得不询问另一个人具体的实现逻辑. 毕竟看的人不一定能在写的时候发现问题, 测试时, 看的人对不是自己写的代码也不熟悉.

  • 另外一个错误是最初的算法考虑不周到, 忽略了符号的优先级问题, 比如乘除法优先级比加减法高. 由于操作符和操作数的产生基于局部, 默认是从左到右的运算顺序. 比如算式 10 + 2 * 2. 真实的运算顺序是先乘法, 再加法, 得出结果14. 但当前程序产生的结果先算左边10 + 2, 再算12*2, 得出结果24. 解决是通过加括号, 保证运算顺序从左往右. 例子见5.运行结果第295道题.


5. 运行结果


6. 具体代码

6.1cpp文件

Random.cpp

#include "Random.h"

// 静态成员定义总是记不住
std::random_device Random::dev;
std::mt19937 Random::rng(dev());
std::uniform_real_distribution<float> Random::dist(0.f, 1.f);

int Random::Range(int left, int right)
{
	float f = Range((float)left, (float)right);
	int i = std::floor(f);

	i == right ? i -= 1 : i = i;
	return i;
}

float Random::Range(float left, float right)
{
	return left + __Random0To1() * (right - left);
}

bool Random::Happen(float possible)
{
	return __Random0To1() < possible;
}

float Random::__Random0To1()
{
	float f = dist(rng);
	assert(0. <= f && f <= 1.);
	return f;
}

AddenGenerate.cpp

#include "AddenGenerate.h"

bool AddenGenerate::GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result)
{
	int addenScope = resMaxValue - left;

	if (addenScope < 1) {
		return false;
	}

	int adden = Random::Range(1, addenScope+1);

	operateSymbol = '+';
	operateNum = adden;
	result = left + adden;

	return true;
}

DivisorGenerate.cpp

#pragma once
#include "Operator.h"

class DivisorGenerate : public Operator {
public:
	bool GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result);
};

MultiplierGenerate.cpp

#pragma once
#include "Operator.h"

class MultiplierGenerate : public Operator {
public:
	bool GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result);
};

SubtractorGenerate.cpp

#include "SubtractorGenerate.h"

bool SubtractorGenerate::GenerateOperateNum(int left, int resMinValue, int resMaxValue, int& operateNum, char& operateSymbol, int& result)
{
	int subScope = left - resMinValue;

	if (subScope < 1) {
		return false;
	}

	int sub = Random::Range(1, subScope+1);

	operateSymbol = '-';
	operateNum = sub;
	result = left - sub;

	return true;
}

Paper.cpp

#include "Paper.h"
#include "Random.h"
#include <sstream>

Paper::Paper(int topicAmount, int resMinValue, int resMaxValue, std::vector<Operator*> operators)
{
	this->operators = operators;

	for (int i = 0; i < topicAmount; i++) {
		topics.emplace_back(GenerateOneTopic(resMinValue, resMaxValue));
	}
}

void Paper::ShowPaper() const
{
	for (int i = 0; i < topics.size(); i++) {
		cout << "第" << i + 1 << "道题: " << endl;
		cout << topics[i].content << endl;
		cout << "答案: " << topics[i].answer << endl << endl;
	}
}

TopicData Paper::GenerateOneTopic(int resMinValue, int resMaxValue)
{
	int result;
	// 得通过::名称空间访问符访问
	// 添加首个操作数, 比如"20"
	int initOperateNum = Random::Range(resMinValue, resMaxValue);
	operateNumQueue.EnQueue(initOperateNum);
	
	// 首次执行, 保证至少有两个操作数和一个操作符. 比如"20+10"
	GenerateNumAndSymbol(initOperateNum, resMinValue, resMaxValue, result);

	// 后续操作数和操作符以某个概率添加, 这里是0.5. 添加后可能为"(20+10)*3"
	while (Random::Happen(NEW_NUMBER_AND_SYMBOL_POSSIBLE)) {
		GenerateNumAndSymbol(result, resMinValue, resMaxValue, result);
	}

	TopicData newTopic;
	newTopic.content = GenerateContent();
	newTopic.answer = result;
	return newTopic;
}

bool Paper::GenerateNumAndSymbol(int initOperateNum, int resMinValue, int resMaxValue, int& result)
{
	int operateNum;
	char operateSymbol;
	int answer;

	bool vaild;
	// 记录一个"用于生成操作符和操作数"方法是否被选过
	std::vector<bool> hasBeenChoiceOperator(operators.size());
	for (int i = 0; i < hasBeenChoiceOperator.size(); i++) {
		hasBeenChoiceOperator[i] = false;
	}

	do {
		int choiceIx = Random::Range(0, operators.size());
		// 如果某个方法已经被选过, 则重新选一个
		while (hasBeenChoiceOperator[choiceIx]) {
			choiceIx = Random::Range(0, operators.size());
		}
		hasBeenChoiceOperator[choiceIx] = true;

		// 生成的操作数是否合法. 比如10 + 12."+"和"12"是合法的; 但对于100 + 0. "+"和"0"是无意义的. 
		// 意味着此时加法无论如何也找不到一个合理的算数. 就换用其它算法, 比如减法.
		vaild = operators[choiceIx]->GenerateOperateNum(initOperateNum, resMinValue, resMaxValue, operateNum, operateSymbol, answer);
	} while (!vaild);

	// 合法操作数和操作符入列
	operateSymbolQueue.EnQueue(operateSymbol);
	operateNumQueue.EnQueue(operateNum);
	result = answer;
	return true;
}

// 对算法考虑不周, 忘记了乘除法优先级比加减法高.
// 在考虑问题时, 想当然的认为符号优先级是从左往右了
string Paper::GenerateContent()
{
	int num;
	char symbol;
	char previouSymbol = '\b';

	ostringstream content;
	ostringstream leftBracket;
	operateNumQueue.DeQueue(num);
	content << num;

	// 补丁是这样的, 如果出现-,+号在*,/前边, 那么添加()号, 保证优先级从左往右
	while (operateNumQueue.DeQueue(num) && operateSymbolQueue.DeQueue(symbol)) {
		if ((symbol == '*' || symbol == '/')
			&& (previouSymbol == '+' || previouSymbol == '-')) {
			content << ')';
			leftBracket << '(';
		}
		content << " " << symbol << " " << num;

		previouSymbol = symbol;
	}

	return leftBracket.str() + content.str();
}

main.cpp

#include "AddenGenerate.h"
#include "MultiplierGenerate.h"
#include "SubtractorGenerate.h"
#include "DivisorGenerate.h"
#include "Paper.h"

int main(void) {
	AddenGenerate add;
	SubtractorGenerate sub;
	MultiplierGenerate multi;
	DivisorGenerate divi;

	std::vector<Operator*> operators{
		&add,
		&sub,
		&multi,
		&divi,
	};

	Paper paper(300, 1, 100 ,operators);

	paper.ShowPaper();

	return 0;
}

7. 心得体会

多人合作是未来工作中不可避免的, 毕竟一个人能力精力都是有限的. 但是目前的结对编程, 如果建立在一个人写, 一个人检查的前提下. 感觉速度更慢. 如果能同时进行, 或许速度更快些. 不过就少了一个人监督. 发现潜在的问题了.

posted @   口乞厂几  阅读(43)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示