学习算法的一些笔记。
枚举法(穷举法)思想
也称穷举法,是用于穷尽每一种可能的情况,效率不高,适用于没有明显规律的情况。
填数游戏
比如有下面这样一个数学题:
算法描述题
x 算
----------
题题题题题题
要求分别计算出各汉字的可能性。
分析:其中一共有5个不同的字“算法描述题”,需要分别定义5个整形变量,取值在[0, 9],要使算式有意义,结果就不能为0。然后求出被乘数(算法描述题)、乘数(算)、结果(题题题题题题),如果能使算式成立就输出结果。
/**
* 填数游戏:算法描述题,如下:
* <pre>
* 算法描述题
* x 算
* ----------
* 题题题题题题
* </pre>
* 要使算式有意义,“算”和“题”就不能为0。
*/
public static void 算法描述题() {
int i1, i2, i3, i4, i5;//“算法描述题”
long multi, result;//分别保存被乘数和结果
for (i1 = 1; i1 <= 9; i1++) {// 算
for (i2 = 0; i2 <= 9; i2++) {//法
for (i3 = 0; i3 <= 9; i3++) {//描
for (i4 = 0; i4 <= 9; i4++) {//述
for (i5 = 1; i5 <= 9; i5++) {//题
multi = i1 * 10000 + i2 * 1000 + i3 * 100 + i4 * 10 + i5;
result = i5 * 100000 + i5 * 10000 + i5 * 1000 + i5 * 100 + i5 * 10 + i5;
if (multi * i1 == result) {
System.out.println(String.format("%s x %s = %s", multi, i1, result));
}
}
}
}
}
}
}
最终结果:79365 x 7 = 555555。
填运算符
给定几个操作数和结果值,利用程序填写运算符,使等式成立。
这里先写了5个操作数的情况,直接上代码,然后慢慢分析。
/**
* 根据输入的 5 个运算数和 1 个结果值,程序自动添加运算符号使等式成立。<br/>
* 如下:a、b、c、d、e、f代表用户输入的几个数字,程序在a、b、c、d、e中加入 + - x / 符号使等式成立。
* <pre>
* a b c d e = f
* </pre>
* <pre>
* 分析:
* 1、要使算式有意义,除数就不能为0。
* 2、乘除法的优先级高于加减法,可使用变量保存乘除法左右两边的结果,最后再做加减法。
* </pre>
*
* @param numbers 运算数
* @param result 结果值
*/
public static boolean 填数游戏(int[] numbers, int result) {
boolean successFlag = false;
System.out.println(String.format("%s %s %s %s %s = %s", numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], result));
int count = 0;//统计有多少符合条件的方案
float left = 0, right = 0;//记录运算符左侧和右侧的运算结果
int sign;//累加运算时的符号
char[] operator = {'+', '-', 'x', '/'};// 运算符集合
int[] i = new int[4];//表示算式中的4个位置的运算符,0表示+,1表示-,2表示x,3表示/
int operatorSize = operator.length;// 算式中运算符的个数
for (i[0] = 0; i[0] < operatorSize; i[0]++) {//第 1 个位置的运算符
if (i[0] < 3 || numbers[1] != 0) {// 如果运算符是/,则后面的运算数不能是0
for (i[1] = 0; i[1] < operatorSize; i[1]++) {//第 2 个位置的运算符
if (i[1] < 3 || numbers[2] != 0) {
for (i[2] = 0; i[2] < operatorSize; i[2]++) {//第 3 个位置的运算符
if (i[2] < 3 || numbers[3] != 0) {
for (i[3] = 0; i[3] < operatorSize; i[3]++) {//第 4 个位置的运算符
if (i[3] < 3 || numbers[4] != 0) {
left = 0;
right = numbers[0];
sign = 1;
//依次判断每位符号
for (int j = 0; j < operatorSize; j++) {
switch (operator[i[j]]) {
case '+':
left = left + sign * right;
sign = 1;
right = numbers[j + 1];
break;
case '-':
left = left + sign * right;
sign = -1;
right = numbers[j + 1];
break;
case 'x':
right = right * numbers[j + 1];
break;
case '/':
right = right / numbers[j + 1];
break;
}
}
if (left + sign * right == result) {
count++;
String str = String.format("%s: %s %s %s %s %s %s %s %s %s = %s", count, numbers[0], operator[i[0]], numbers[1], operator[i[1]], numbers[2], operator[i[2]], numbers[3], operator[i[3]], numbers[4], result);
System.out.println(str);
successFlag = true;
}
}
}
}
}
}
}
}
}
return successFlag;
}
分析上面的代码,可以发现有这样几个步骤:
- 循环运算符
- 循环第一位运算符
- 判断第一位运算符不为除法或者第二个运算符不为0
- 循环第二位运算符
- 判断第二位运算符不为除法或者第三个运算符不为0
- 循环第三位运算符
- 判断第三位运算符不为除法或者第四个运算符不为0
- 循环第四位运算符
- 判断第四位运算符不为除法或者第五个运算符不为0
- 循环第一位运算符
- 依次取出每位符号
- 通过 switch 匹配运算符
- 计算临时结果
- 通过 switch 匹配运算符
- 计算最终结果
因为这里只计算5个数,所以只添加 4 个运算符,每个位置上的运算符又有 4 种情况,需要要循环每个位置的 4 种情况。这里约定使用一个数组 i 来表示每个位的运算符,数组有索引,也有结果值。索引用于表示第几位运算符,结果值用来表示是什么运算符。约定 0 表示加法,1 表示减法,2 表示乘法,3 表示除法。那么可以这样表示每位运算符的不同情况:
- i[0] 的值为0、1、2、3分别表示第一位的运算符的加、减、乘、除。
- i[1] 的值为0、1、2、3分别表示第二位的运算符的加、减、乘、除。
- i[2] 的值为0、1、2、3分别表示第三位的运算符的加、减、乘、除。
- i[3] 的值为0、1、2、3分别表示第四位的运算符的加、减、乘、除。
所以就有了这样几个 for 循环:每层循环表示第几位的运算符,每次循环得出来的值为运算符的值。
for (i[0] = 0; i[0] < operatorSize; i[0]++) {
for (i[1] = 0; i[1] < operatorSize; i[1]++) {
for (i[2] = 0; i[2] < operatorSize; i[2]++) {
for (i[3] = 0; i[3] < operatorSize; i[3]++) {
每个 for 循环的里面还有一个 if 判断是干什么的?
因为要使等式有意义,除数不能为0,这个 if 判断就是这个目的。
很直观的想法是这样的:如果运算符是除法 && 后面一个操作数是0,则结束当前循环,否则继续执行。
我们只关心继续执行的情况,所以取其反面:如果运算符不是除法,或者后面一个操作数不是0,继续执行,其他情况不做考虑。
再详细的说明判断逻辑,其中包含两种情况:
- 如果运算符不是除法,对于加减乘,运算数可以任何数字,包括0。
- 如果运算符后面的运算数不是 0,对于加减乘除来讲,算式都有意义。
所以在每个 for 循环后跟了一个这样的 if 判断
if (i[0] < 3 || numbers[1] != 0) {
if (i[1] < 3 || numbers[2] != 0) {
if (i[2] < 3 || numbers[3] != 0) {
if (i[3] < 3 || numbers[4] != 0) {
对等式进行有意义判断之后,就开始计算了。依次循环每位运算符,然后做比较。
此前定义了一个操作符集合char[] operator = {'+', '-', 'x', '/'};
。
//依次判断每位符号
for (int j = 0; j < operatorSize; j++) {
switch (operator[i[j]]) {
case '+':
left = left + sign * right;
sign = 1;
right = numbers[j + 1];
break;
case '-':
left = left + sign * right;
sign = -1;
right = numbers[j + 1];
break;
case 'x':
right = right * numbers[j + 1];
break;
case '/':
right = right / numbers[j + 1];
break;
}
}
switch (operator[i[j]])
正如之前所讲,利用 i[] 的值表示某个运算符,下标(这里是 j)表示第几位运算符。
计算过程:定义两个变量 left、right,用于保存临时结果。left 保存运算符左侧的结果,right 表示运算符右侧的结果。变量 sign 表示下次计算的符号为正还是负,即加减法的实现。下面以3 / 2 x 6 - 8 + 1 = 2
为例来进行分析。
- 初始化:left = 0,right = 3,sign = 1。
- 除法:left = 0,right = right / 2 = 1.5。
- 乘法:left = 0,right = right * 6 = 9。
- 减法:left = left + sign * right = 9,right = 8,sign = -1。
- 加法:left = left + sign * right = 9 + (-1 * 8) = 1,right = 1,sign = 1。
- 最终结果:left + sign * right = 1 + 1 = 2。
总结运算过程:
- 加减法
- left = left + sign * right。
- 加法时 sign 要变为1,减法时 sign 要变为-1。
- 加减法的运算是滞后的,是在下一步运算时才计算的,比如上面的减法,是在加法那一步才计算的,所以需要用 sign 来记录下一步运算的符号。用 right 来记录下一步运算的运算数。
- left 用于记录运算符左侧的数:因为加减法的时候是不立即运算的,所以先将加减法左侧的数记录下来。
- right 的值变为下一个运算数。
- 乘除法
- 乘法:right = right * numbers[j + 1]。
- 除法:right = right / numbers[j + 1]。
- 乘除法直接进行运算,left 的值不变。
我写了一个测试函数:
private static void 测试填数游戏() {
new Thread(new Runnable() {
@Override
public void run() {
boolean flag = false;
Random random = new Random();
int[] arr = new int[5];
while (!flag) {
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(10);
}
flag = 填数游戏(arr, random.nextInt(50));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
我想了一下,有这样几个问题,需要我们去一一验证或者进行修改。
运算符不重复的情况怎么写?
当操作数不是固定的 5 个数的情况下该如何写?
这里只操作的整形数,那浮点型数是否也可如此计算?
负数的情况又是如何?
如果有括号该如何编码?