这是一篇本来不应该存在的程序,但是有了世界之大,就有这一段程序,如果你同情我的遭遇,那么进来帮我改进一下,大家一起交流讨论一下吧,我谢谢大家了。
本来我哪有这个业务啊,这个单变量求解可是excel的功能,谁能想到会有领导让人实现这个功能的,你直接调用excel,让他去算不好嘛,偏偏我对象的领导,就想出了这么一个搜肠刮肚,都想不出来的馊主意,还好这个功能使用的场景比较单一,只是用在了一个增函数上,如果连这个函数,是增函数还是减函数,都不清楚的情况下,以我这考不上985的大脑,估计是想不出来了。
废话说完,直接上货,底下是全部源码,不用着急看,可以先看源码底下的解释。
public class GoalSeek {
public static void main(String[] args) {
double target = 6.875531;
double right;
double left;
double x=Math.random();
if(f(x) < target){
left = x;
do{
x*=10;
} while(f(x) < target);
right = x;
} else{
right = x;
do{
x/=10;
} while(f(x) > target);
left = x;
}
double threshold = findThreshold(right-left);
double result = 0;
while(left<right){
double mid = (right + left)/2.0;
double temp = f(mid);
System.out.println("right="+right+" left="+left);
if(Math.abs(temp-target)<0.0000001){
result = mid;
break;
}
if(temp > target){
right -= threshold;
while(f(right)<target){
right += threshold;
threshold*=0.1;
right -= threshold;
}
} else if( temp < target ){
left += threshold;
while(f(left)>target){
left -= threshold;
threshold*=0.1;
left += threshold;
}
}
}
System.out.println(result);
}
private static double findThreshold(double max){
BigDecimal bigDecimal = new BigDecimal(max);
String[] nums = bigDecimal.setScale(7, RoundingMode.HALF_EVEN).toPlainString().split("\\.");
if(Integer.parseInt(nums[0])>0){
return Math.pow(10,nums[0].length()-1);
}else{
double result = Math.pow(0.1,(nums[1].length() - String.valueOf(Long.parseLong(nums[1])).length()));
return result == 1?0.1:result;
}
}
private static double f(double x){
return x*x+3;
}
因为是一个增函数,所以需要找到两个值,这两个值需要在目标值的两边,类似于下面这个图:
红点是目标点,绿色是right值,黄色是left值,用二分法一点一点逼近目标值。
1.寻找上下界
所以第一步就是要先找到左右这两个点,代码如下所示:
//1.先随机出一个数来
double x=Math.random();
//如果这个数经过函数计算小于目标值,说明这个数是left值,再继续找right值就可以
//寻找的方法就是将这个随机数扩大10倍(扩大几倍都行),一直到这个数经过函数计算之后大于target就可以
if(f(x) < target){
left = x;
do{
x*=10;
} while(f(x) < target);
right = x;
}
//如果这个数经过函数计算大于目标值,说明这个数是right值,再继续寻找left值
//寻找的方法和上面寻找right相反,将随机数缩小10倍,直到这个数经过函数计算之后小于target就可以
else{
right = x;
do{
x/=10;
} while(f(x) > target);
left = x;
}
2.计算阈值
当使用二分法对值进行逼近的时候,left和right值的加减变得尤为重要,是每一步+1,还是+0.1,还是0.01?这个不好确定,所以就有了如下的方法:
private static double findThreshold(double max){
BigDecimal bigDecimal = new BigDecimal(max);
String[] nums = bigDecimal.setScale(7, RoundingMode.HALF_EVEN).toPlainString().split("\\.");
if(Integer.parseInt(nums[0])>0){
return Math.pow(10,nums[0].length()-1);
}else{
double result = Math.pow(0.1,(nums[1].length() - String.valueOf(Long.parseLong(nums[1])).length()));
return result == 1?0.1:result;
}
}
方法入参是right-left所得,这两个数的差值格式,有三种情况:
-
1.1,这种情况就是小数点两边都有值,这个时候只需要小数点左边的整数值就可以,如果是1.0就得到1,如果是10.0就得到10,如果是100.0就得到100,以此类推。
-
1.0,这种情况和上述情况一样,只不过这种情况是恰巧得到一个整数,小数点右边没有数
-
0.1,这种情况就是小数点左边为0,需要找到右边最靠近小数点的非零数,比如0.123得到0.1,0.0123得到0.01,0.00000123得到0.000001.
上述方法是先将小数转换成String格式,通过小数点将字符串变为两个部分,nums[0]是整数部分,nums[1]是小数部分,这地方有个坑就是要防止double变成科学计数法,例如0.00009变成9E-5这种情况,PHP具体怎么变我不知道。
小数点左侧比较好处理,比如左侧是89,则返回10即可,10的1次方
小数点右侧不太好处理,比如0.0076,右边是0076,先将0076转换为Long型变成76,再转换成字符串,用0076的字符串长度减去76的字符串长度 ,得到目标值2,0.1的平方是0.01。这里有一个特殊情况就是0.1最后得到的结果是0,0.1的0次方等于1,所以当这种情况遇到结果是1的时候,返回0.1即可。
3.防止错过答案
有的时候会出现这样一种情况,例如y=x+3,这个函数,如果你的目标值是5.1,即y=5.1是你的目标值,当x的左右值在加减的时候,会让x错过正确的答案,比如正确答案是x=2.1,但是你的左右值随机出来的值是0.1和2.2,计算出来的阈值是1,第一次二分结束的时候会让右边的值编程1.2,直接就错过了2.1这个正确答案,之后再如何算都是徒劳的,甚至进入死循环!
这个时候就是源代码的第三步:
if(temp > target){
right -= threshold;
while(f(right)<target){
right += threshold;
threshold*=0.1;
right -= threshold;
}
} else if( temp < target ){
left += threshold;
while(f(left)>target){
left -= threshold;
threshold*=0.1;
left += threshold;
}
}
这里的思想就是right的函数值是一定要大于目标的,因为是增函数,所以如果right的函数值小于目标,说明阈值大了,那么就将right值恢复,然后将阈值变小,再进行计算,一直到得出结果,左侧同理。
总结
过程就是这么一个过程,作为最底层码农的我,不仅要服务于我的领导,还有我领导的领导,还有客户,还有对象的领导,还有对象领导的客户,你们看吧,我苦不苦,啥也不说了,我又双叒叕坏肚子了。最后我对象还和我说,她有新的任务,暂时先不看了,我... ...