新程序包下载(密码:4kp6)
>>>>>直接上代码,问题出在随机分数的生成上,确实出现了一些非常鱼唇的错误,不过已经提交了就没办法了,在这里发出来仅供参考吧:
修改前:
public static Fraction nextFrac(int width, Random seed)
{
Random rd = seed;
int down1 = (int)(rd.NextDouble() * (width - ) + );//随机生成低于width的分母
int quo1 = (int)(rd.NextDouble() * (width - ) + );//随机生成低于width-1的带分数整数部分
long up1 = (long)(rd.NextDouble() * (width - ) + );//随机生成低于width的分子 up1 += quo1*down1;
return new Fraction(up1, down1);
}
修改后:
public static Fraction nextFrac(int width, Random seed)
{
Random rd = seed;
int down1 = (int)(rd.NextDouble() * (width - ) + );//随机生成低于width的分母
long up1 = (long)(rd.NextDouble() * (width *down1-) + );//随机生成低于分母*width的分子 return new Fraction(up1, down1);
}
对于随机数元素的生成范围,第一次要求作业中有如下规定:
使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10
将生成10以内(不包括10)的四则运算题目
对于我之前的处理方法,即 分数 = 整数[1,width-1)+ 分子[1,width)/分母[1,width)
其值域实际上为[1,2*width-2),而且导致生成的分数至少大于1,实际上少了很多种情况...............................................
特别的,当width取2时,由于width-2=0,随机数失去意义,这样取值范围就固定变成了[1,2],这就与老师在课程要求上的说明冲突了;
同理可得:
int down1 = (int)(rd.NextDouble() * (width - ) + );
long up1 = (long)(rd.NextDouble() * (width *width-) + );
这样的处理方式也是存在问题的,值域实际上变成了[1,width^2)
值得说明的一点是,仍然保留了random*(width-1)+1这一设定,以规避分母或分子为0的情况,主要是为了减少无意义操作数(0)的出现频率和规避一些不合法情况
程序设计过程中出现的其他Bug汇总:
1. 当范围较大,读取表达式计算结果时,容易出现分子超int范围的情况,笔者在之前的博客中已经提到过,为了支持足够的范围,需要采用Long整型存储分子分母,因为计算过程中可能出现超int范围的分子分母结果,因此相应的,读取答案的时候,我们也要采用Int64.Parse来录入结果,并用相应的Long型变量存储,而非常用的Int32.Parse(感谢乾麻提醒)
2. 求最大公约数,也即约分的方法一定要考虑传入参数含0的情况,以及,需要考虑,对于公约数为0的情况,约分时应当怎样处理,是否需要报异常等
3. 对于计算模块,自栈顶向下逐步运算时(从中缀表达式的角度来看就是从右往左算),一定要考虑前一个操作符是否为“-”号,对于没有添加括号的情况,盲目按序运算会导致错误 如 (3-2-1),当计算完一个括号内的值后,需要取前一个运算符判断是否为×或÷,因为在这两个符号后面跟括号的情况下,其运算会被延后,一旦其后所跟的括号内式子得出结果,该运算应当被首先考虑。
需要注意的是,如果不对每个生成的子表达式加上括号,其实际运算优先顺序可能与表达式建立过程不符。 如e=>e1÷e2=>e3÷e4÷e2=>3÷2÷1 (减法同)
非常建议生成一些不加括号,运算符也多于3的中缀表达式,对于检查计算模块的正确性很有帮助
例如:6×5+8÷(3-2) ;
213'3/5 - 65×(3+5-(2)-4)
为了批量检查计算模块的正确性,可以使用excel,这里分享一下具体的用法:
S1:
我们需要对程序做一点点扩展,使其通过命令行的控制,能够输出同时被excel和计算模块解析的式子
也即:将生成带分数全部转为假分数形式,并括起来(避免除法过程的二义性)
笔者做法如下:
class user {
...
public static bool mixed = false;
...
class program {
...
if (args[i] == "-m")
{
user.mixed = true;//命令行参数控制是否以假分数形式输出
}
...
ExerciseWriter.Write((j + ).ToString() + ". \0\t" + expStr.getExpStr() + "\r\n");
AnswerWriter.Write((j + ).ToString() + ". \0\t" + answer1 + "\r\n");//输出制表符,这样直接粘到Excel里会变成两列,表达式和序号就直接分离了 class Fraction { public String express()
...
long quo = up / down;
long res = up % down;
String s = "";
if (user.mixed) return (s = "(" + up + "/" + down + ")");
else ...//正常输出
...
需要注意的是,因为有制表符的存在,所以读取式子用replace去除空白符应当用“\\s+”匹配而不是空格“ ”
S2
将式子粘到excel里,会发现自动分成两列了(比如A、B两列吧),然后我们用excel自带的查找替换工具,将B列表达式出现的×÷替换为*/;
在C列输入 =”=“&B1,并拖动格式手柄应用到其余行(如果数字太大,就拖滚动条到表格底部,用shift选中底部到头部的所有单元格,按ctrl+d即可)
再按ctrl+c复制,粘贴选择左上角粘贴选项里的 ”粘贴数值——值“,使用查找替换工具,将”=“替换为”=“,所有表达式的值就悉数计算出来了;
S3
采用同样的办法把答案复制到D、E列,并用同样的办法求其值(答案实际上多为分数,可以看成一个简单表达式),用S2的办法求其值;
再在F列中计算C列,E列两列值之差并用sum求和,如sum为0,则基本可以确保程序的计算模块已经完全没有问题了
4. 在用Dictionary<String Answer ,List<String> usedCase>这一数据结构时发现索键求值的过程存在很多数据错误,经过仔细的检查,发现问题在于将List对象Add到Dictionary里的时候,直接传引用作为值放进去了,因为笔者的程序设计的时候,只有一个usedCase随表达式生成函数传入,clear,add,传出,那么无疑所有键的键值都是这一个List,它被修改的话相应的Dictionary里的所有键键值就会被修改,所以实际上作为value传入的时候是需要通过拷贝构造的,至于中间出现的数据混乱情况,可能是在修改这一模块时只修改了一部分所致吧。
5. 计算表达式时,为了避免栈空取值的情况,需要时刻关注栈的容量,但有一个简洁的办法可以改善性能,那就是预置栈底元素(比如#之类的作为标记),这样只需访问栈中的一个值而不是全部
6. 对于负分数的表示形式,应规范成仅分子为负或仅整数部分为负(读入78/-4,转为-20'1/2或-39/2),假分数转换成带分数的时候,因为分子由取余操作产生,其值可能为负,这种情况下,需要手动让分子加上分母的值,然后整数部分-1来简约形式,避免-5’-39/78这种情况的产生,恰恰因为我们的项目要求里对减法做了约定来规避负数的产生,这一点才不容易被发觉,试着生成一些负分数的String表示形式,或者让分子,分母,整数部分取0,看看会有怎样的结果
7. 养成好习惯,随手关门,随手close
Ps:如果输入文件路径无效试着转绝对地址吧,方法为System.IO.Path.GetFullPath(YourAddress),另外输出文件时需统一编码(再次感谢乾麻提醒),如:
StreamWriter GradeWrite = new StreamWriter(Grade, System.Text.Encoding.Unicode);
8. 通过Opnum和OpLim控制表达式的推导生成过程,显然,当Opnum = 0的时候应规避 e=>n的文法规则,当Opnum = OpLim时应规避二元式的生成,并在当前表达式已经被括号括起来的情况下,规避e=>(e)的文法规则来避免括号冗余,以避免增加计算模块负担
9. 一个比较常见的情况是生成表达式的时候,明明用了random却出现了大规模的重复情况,这是因为random生成随机数是基于当前系统时间的,由此生成一个伪随机序列,如果程序执行的效率高,同时又有多个random对象被新建用于生成随机数的话就会出现重复,对于这样的情况,最好的办法是全程只用一个random对象,让其作为随机种子传入即可
10. 暂时只注意到这么多,有新的会补充