这是前面的课: 第一节-游戏介绍与基本算法 。
尽管写程序时,由上而下分解逻辑是常见的,但到了讲课,通常要先扫清一些边边角角的函数或定义。这一节课的作用就是这样的。
不管用的什么IDE,先建立一个控制台工程(Console project)。
然后加入一个 main.cpp,用于写C++的入口函数(main函数)——通常,你的IDE向导,会帮你生成这个文件及main函数。Code::Blocks就是这样,不过我们先不管它们。
再新建 “common_def.h” 和"common_def.cpp" 文件,把它加入工程。
一、 common_def.h 文件:
- #ifndef COMMON_DEF_H_INCLUDED
- #define COMMON_DEF_H_INCLUDED
- /*
- 为了方便运算,在内部并不直接使用'+','-'
- 等字符表示加减乘除的操作符.
- 而是使用 0,1,2,3分别表示加减乘除
- */
- typedef unsigned char operator_index;
- extern char operator_char_def[]; //'+', '-', '*' , '/'
- #endif // COMMON_DEF_H_INCLUDED
把 "unsigned char" 类型取个别名,存是为了直观。我们用(0,1,2,3)四个数来表达“加、减、乘、除” 。定义一个enum或许也很常见,但在本例中enum会带来不方便(以后再说,差不多类似于当年java抛弃enum的某些原因,当然,当然,我知道java后面又捡回来了)。
既然用整数表达操作符,那 typedef int operator_index 似乎更直观?不过我还是故意避开int了,原因也以后再说。
可以肯定,后面我们会需要显示这些操作符,这时还是直接显示“+”, "-", "*", "/" 直观可读。所以,我们又定义了一个char数组。具体内容见 cpp 文件:
二、common_def.cpp 文件:
- #include "common_def.h"
- char operator_char_def[] = { '+', '-', '*', '/' };
公共定义的源文件就这么简单,列出四个操作符而已。
小测一下,从一个operator_index,要得到它的操作符字符,方法太简单了:
operator_index oi = 2; //乘法
cout << operator_char_def[oi]; //输出 '*' 字符
==================================
接下来,再新建一个 calc_24.h 和calc_24.cpp文件,将它们都加入到工程。
三、calc_24.h 文件:
- #ifndef CALC_24_H_INCLUDED
- #define CALC_24_H_INCLUDED
- #include "common_def.h"
- bool calc_24(int card[4]);
- #endif // CALC_24_H_INCLUDED
再简单不过了,声明了一个 calc_24 的函数。这个函数:
入参是一个整数数组……当然,由于在c/c++语言中, 数组作为函数入参时,将退化为指针,所以该函数也相当于:
bool calc_24(int* card);
所以, 那个[4]其实一点用处没有,写1,写2,甚至什么都一个样,但这里我们还是一个4,用来提示该函数需要4张牌的数字。本例实在简单,对函数入参的检查略掉。
假设我们在玩扑克时,拿到 红桃K,黑桃5, 黑桃2 ,方块8 , 那些花色对算24点没有什么用,将值组成int数组,传给calc_24数组即可。
返回值: 为真表示用这4张牌算出24点了,为假表示算不出来,估计是无解。
四、 calc_24.cpp 文件:
这个文件显然最复杂。为了实现 calc_24函数,我们还需要一些边边角角的函数。下面代码是一段一段讲的,除非文中有特别提到插入在某处,否则代码就是相当于一点点往下写去。
先是简单的头文件包含及名字空间引入等。
- #include <iostream>
- #include "calc_24.h"
- using namespace std;
然后是一个只需要计算两个数操作的函数,比如,给出 n1=1 和 n2= 2, 再给出 operator_index 为 0 ,则进行 1 + 2 的计算,结果为3,很简单不是? (如果o为2,则计算 1 * 2 = 2)。
4.1 实现两数一步计算的函数:calc
- int calc(int n1, int n2, operator_index o)
- {
- if (o < 0 || o >= 4)
- return -1;
- switch(operator_char_def[o])
- {
- case '+' :
- {
- return (n1 + n2);
- }
- case '-' :
- {
- if (n1 < n2) //不允许出现负数,所以被减数不能小于减数
- return -1;
- return (n1 - n2);
- }
- case '*' :
- {
- return (n1 * n2);
- }
- case '/' :
- {
- if ((n2 == 0) || (n1 % n2) !=0) //1)除数不为0; 2)要确保整除
- return -1;
- return (n1 / n2);
- }
- }
- return -1;
- }
第一节说过,这是小学生玩的游戏,不允许出现负数计算,所以当o为减法时,n1 < n2 ,则返回 -1 表示无效计算 ……对于通用的计算,因为计算结果也可能是-1……所以此时出错通常是抛出异常(c++),但我们的计算过程已经约定不能是负数了,所以用-1表示出错就可以了,调用者可以判断得出。
除法需要判断的事情多了点:除0错肯定是要防的,然后就是不允许有余数,因为我们已经规定不允许在计算过程出现小数,同时也确实没有用double来表达一个张牌的值。
4.2 连续计算
第一节说过,排好一个算式时,我们有两种计算方法。第一种称为“连贯式”计算方法,比如: 1 + 2 * 3 - 4 , 用连贯计算,结果是5,而不是3!,因为我们只是从左到右连续拿数计算(先左后右),而不用考虑什么“先乘除后加减”优先级。
- int calc_consecutive(int n[4], operator_index o[3])
- {
- int r1 = calc(n[0], n[1], o[0]);
- if (r1 < 0)
- return 0;
- int r2 = calc(r1, n[2], o[1]);
- if (r2 < 0)
- return 0;
- int r = calc(r2, n[3], o[2]);
- return r;
- }
代码太好读了:) 先拿前面两个数: n[0] 和n[1],用第一个操作符:o[0] 进行运算,得到临时结果 r。再拿 r 和第三个数n[2] 及第二个操作符o[1] 进行运算……
calc 返回-1表示当前计算出错,此时可以放弃尝试,比如出现: 1 - 2 + 3 * 4 ,一开始计算 1-2 就可放弃(不用继续计算)了,因为我们不允许负数(原因,及它为什么并无影响结果正确性见第一节)。
4.3 分隔式计算
分隔式计算先算前两个数,再算后两个数,然后再用中间的操作符最后计算。比如: 同样是 1 + 2 * 3 - 4 ,则相当于是:(1+2) * (3-4) 结果是……噢,真不凑巧,出现负数,无效,换个例子: 5 + 3/ 3 - 1 ,结果不是 5, 而是 (5+3) / (3-1)得到4。
- int calc_separate(int n[4], operator_index o[3])
- {
- int r1 = calc(n[0], n[1], o[0]);
- if (r1 < 0)
- return 0;
- int r2 = calc(n[2], n[3], o[2]);
- if (r2 < 0)
- return 0;
- int r = calc(r1, r2, o[1]);
- return r;
- }
4.4 calc_24 函数!
calc_24的逻辑,在第一节已经讲过一些,总之它的特点就是**,现在来看一个完整代码:
- bool calc_24(int card[4])
- {
- int n[4]; //四个操作数
- operator_index o[3]; //三个操作符
- for (int i1=0; i1<4; ++i1)
- {
- n[0] = card[i1];
- for (int i2=0; i2<4; ++i2)
- {
- if (i2==i1)
- continue;
- n[1] = card[i2];
- for (int i3=0; i3<4; ++i3)
- {
- if (i3 == i1 || i3 == i2)
- continue;
- n[2] = card[i3];
- int i4 = 6-i1-i2-i3; //6 <- 0+1+2+3;
- n[3] = card[i4];
- for (operator_index j1=0; j1<4; ++j1)
- {
- o[0] = j1;
- for (operator_index j2=0; j2<4; ++j2)
- {
- o[1] = j2;
- for (operator_index j3=0; j3<4; ++j3)
- {
- o[2] = j3;
- if (calc_consecutive(n, o) == 24)
- {
- tmp_output(1, n, o);
- return true;
- }
- if (calc_separate(n, o) == 24)
- {
- tmp_output(2, n, o);
- return true;
- }
- }
- }
- }
- }
- }
- }
- return false;
- }
循环循环套循环……当然不好看。如果用来练习C++,倒是可以写成一个具备iterator接口的类来处理,,不过对于本例,那样做只会让事情更不直观(还只会慢不会更快,代码也短不了)。再看那些著名的C数学计算库,这样的循环算是干净的了……(这里,最可能的改进是使用标准库的next_permutation)
这里重点是理解前面三层for,是用来不断以各种排列,将四个数放到n数组里去(为什么四个数只需要3层循环?见第一节)。相比前一节课,这里又嵌入了三层循环,用来将操作符(+,-,*/)塞入到o数组里去。和牌出了就不能再出不一样,同一个操作符可以出多次,所以这三层循环里没有看到if ... continue。
排完好四个数,又排好三个操作符,我们就先调用 “ calc_consecutive(连贯式) ”函数计算一下,看是否结果为24。如果结果是0表示无效,当然这里也不需如何特殊处理无效计算; 如果 calc_consecutive 算出24,我们就当是找到了(我们暂时只需要找出一个答案就可以)。如果不24,对于的算式,我们再调“ calc_separate (分割式)” 函数计算一下。
不管哪种算法算出了24,我们都调用一个用于临时的函数(tmp_output),将结果输出到屏幕。之所以是临时的,是因它的输出内容很不很不直观,我们肯定要重写它的。
tmp_output函数定义如下,请将它插入到 bool calc_24(int card[4]) 函数之前。
- void tmp_output(int method, int n[4], operator_index o[3])
- {
- cout << ((method == 1)? "consecutive" : "separate") << endl;
- cout << n[0]; //先输出第一个数
- //输出后面的操作符和操作数
- for (int i=1; i<4; ++i)
- cout << operator_char_def[o[i-1]] << n[i];
- cout << endl;
- }
tmp_output 函数需要3个入参。第一个入参表示当前用的是哪一种方法计算出24点的,1是连贯法,2是分割法。 然后就是输出那个算式。比如对于 4, 7, 8,8 这四张牌,它会输出:
consecutive
4+7-8*8
你能看懂吧? 4+7-8*8 按照我们说的连贯法计算之后,确实得到24。。
五、 第一版 main 函数
编译一下,如果没错,就可以开始写本版的main函数,其实主要用来测试。
- #include <iostream>
- #include "calc_24.h"
- using namespace std;
- int main()
- {
- int num[4];
- cout << "(按Ctrl + C结束!)" << endl;
- do
- {
- cout << "请输入4个数字 : ";
- for (int i=0; i<4; ++i)
- cin >> num[i];
- if (!calc_24(num))
- {
- cout << "查无答案" << endl;
- }
- }
- while(1);
- return 0;
- }
为了简单,我们甚至让这个程序只能通过按 Ctrl + C 键来强行中断运行。
实际运行示例如下:
------------------------------------------------------------
请输入4个数字 : 4 5 6 10
consecutive
4*5-6+10
------------------------------------------------------------
请输入4个数字 : 5 6 10 2
consecutive
5+10*2-6
------------------------------------------------------------
再特意搞个分隔式的:
请输入4个数字 : 5 3 6 6
separate
5-3*6+6
------------------------------------------------------------
再三地,输出内容实在有背人类的良知 啊, 下一节改进。
另外 其这连贯式和分隔式计算会存在重复计算时候,最简单的,比如: 1 + 2 + 3 + 4 。用连贯式计算,和用分隔式计算 (1+2) + (3+4) ,显然是一样的,我们可以对此做一点点优化。这个我们也放在以后讲。
布置个作业: 自己动手改一下tmp_output函数,让它可以分步输出计算过程,比如:
5, 10, 2 6 ==>
5+10 = 15
15 * 2 = 30
30 - 6 = 24;
-------------------------------------------------------------------------------
如果您想与我交流,请点击如下链接成为我的好友:
http://student.youkuaiyun.com/invite.php?u=112600&c=f635b3cf130f350c