什么是大数运算?
大数运算,顾名思义就是很大数值的数进行一系列的计算。我们知道,在数学中数值的大小是没有上限的,但是在计算机中由于字长的限制,在实际的应用中进行大量的数据处理时,会发现参与运算的数会超过计算机的基本数据类型的表示范围。由于编程语言提供的基本数据类型表示的数值范围有限,不能满足较大规模的高精度数值计算,因此需要利用其他方法实现高精度数值的计算,于是产生了大数运算。大数运算主要有加、减、乘三种方法。——(概念引自百度百科)
为何要支持浮点类型?
既然要实现的是高精度的大数计算,那就不能止步于整型大数,计算机中的浮点数用以近似的表示任意实数,如果脱离基本数据类型与相关的计算并模拟人脑对两个任意数的计算过程,是不是就可以不被字节长度限制并保证精度呢。
如何表示实数?
脱离了基本数据类型,最接近的表示方式就是用字符串。String的基本结构是一个char[],我们知道数组的长度是int类型的,所以理论上一个String可以存放一个Integer.MAX_VALUE(2147483647)位的数字,利用数组连续性,将大数每一位上的数字单独取出,然后再对每一位做单独的运算即可。
由于对浮点类型的支持,实现难度成倍增加,此次仅实现加法与减法,实现逻辑仅模拟大脑处理过程,具体算法未做过多优化。
如何处理字符串?
String str1 = "123.45";
String str2 = "234.567";
我们知道整数部分与小数部分的计算规则是不同的,所以对于每一个表示实数的字符串,应该将其整数部分与小数部分剥离开来,然后分别进行计算。
定义字符串处理类:
private static class SplitResult{
private int[]
intArray,
decimalArray;
public SplitResult(String num) {
//分离两个数字的整数小数部分 String.splt(".")无法如期切分,此处使用转义字符
String[] split = num.split("\\.");
//取出数字的整数和小数部分
String intString = split[0];
String decimalString = split[1];
//转化为int数组
this.intArray = stringToArray(intString);
this.decimalArray = stringToArray(decimalString);
}
}
小数部分计算结果接收类:
private static class DecimalResult {
private int[] result; //计算结果
private boolean flag;//加法时表示是否进位,减法时表示是否借位
public DecimalResult(int... result, boolean flag) {
this.result = result;
this.flag = flag;
}
}
加法
private static void add(String num1, String num2) {
//处理字符串
SplitResult splitResult1 = new SplitResult(num1);
SplitResult splitResult2 = new SplitResult(num2);
//计算整数部分
int[] intResult = addInt(splitResult1.intArray, splitResult2.intArray);
//计算小数部分
DecimalResult decimalResult = addDecimal(splitResult1.decimalArray, splitResult2.decimalArray);
//若小数部分有进位,则给整数再加一
if (decimalResult.flag) {
addInt(intResult, 1);
}
StringBuilder stringBuilder = new StringBuilder();
//结果拼接
append(stringBuilder, intResult, decimalResult.result);
}
整数部分相加
public static int[] addInt(int[] firstIntArray, int... secondIntArray) {
int numLength = 0; //短数组长度
int[] shortArray = null;
int totalLength = 0; //长数组长度
int[] longArray = null;
boolean carry = false; //是否进位
int multNum = 1; //进一
if (firstIntArray.length < secondIntArray.length) {
numLength = firstIntArray.length;
totalLength = secondIntArray.length;
shortArray = firstIntArray;
longArray = secondIntArray;
} else {
numLength = secondIntArray.length;
totalLength = firstIntArray.length;
shortArray = secondIntArray;
longArray = firstIntArray;
}
//从数组末位开始,即数组右对齐,短数组遍历结束则跳出循环
for (int i = numLength - 1, j = totalLength - 1; i >= 0; i--, j--) {
carry = isCarry(shortArray, longArray, carry, multNum, i, j);
//若最后一个仍需进位
if (i == 0 && carry) {
while (j >= 0){
//并且长数组也走到尽头,数组扩容并将首位置1后直接跳出循环
if (j == 0){
longArray = expansion(longArray);
longArray[0] = 1;
return longArray;
}
//否则对下一位数进行加一
int pre = ++longArray[j - 1];
//若大于9则表示循环仍需进行,对下一位继续加一
if (pre > 9) {
longArray[j - 1] = pre % 10;
} else {
//否则结束
longArray[j - 1] = pre;
break;
}
j--;
}
}
}
return longArray;
}
小数部分相加
public static DecimalResult addDecimal(int[] firstDecimalArray, int[] secondDecimalArray) {
int numLength = 0; //短数组长度
int[] shortArray = null;
int[] longArray = null;
boolean carry = false; //是否进位
int multNum = 1; //进一
if (firstDecimalArray.length < secondDecimalArray.length) {
numLength = firstDecimalArray.length;
shortArray = firstDecimalArray;
longArray = secondDecimalArray;
} else {
numLength = secondDecimalArray.length;
shortArray = secondDecimalArray;
longArray = firstDecimalArray;
}
//小数部分计算较为简单,两个数组左对齐,直接从相同的下标开始即可
for (int i = numLength - 1; i >= 0; i--) {
carry = isCarry(shortArray, longArray, carry, multNum, i, i);
}
return new DecimalResult(longArray, carry);
}
对相应位置的数值进行计算,并返回进位结果
private static boolean isCarry(int[] shortArray, int[] longArray, boolean carry, int multNum, int i, int j) {
int num = shortArray[i] + longArray[j]; //直接相加
//若上一次计算有进位,则再加一,并将标识位置为false
if (carry) {
num += multNum;
carry = false;
}
//判断本次计算结果是否需要再次进位
if (num > 9) {
carry = true;
num = num % 10;
}
longArray[j] = num; //使用长数组接收每次的计算结果
return carry;
}
将数组扩充一单位长度,并右对齐
public static int[] expansion(int[] src) {
int[] newArray = new int[src.length + 1];
System.arraycopy(src, 0, newArray, 1, src.length);
return newArray;
}
计算结果拼接
private static void append(StringBuilder stringBuilder, int[] intArray,int[] decimalArray) {
for (int i = 0; i < intArray.length; i++) {
stringBuilder.append(intArray[i]);
}
stringBuilder.append(".");
for (int i = 0; i < decimalArray.length; i++) {
stringBuilder.append(decimalArray[i]);
}
System.out.println(stringBuilder.toString());
}
减法
减法处理较为复杂,可分为四种情况
num1 | num2 | |
---|---|---|
整数部分 | 大于 | 小于 |
小数部分 | 大于 | 小于 |
整数部分可以通过交换参数的数值保证num1的整数部分始终大于num2,然后根据情况加上符号即可,
总流程
private static void minus(String num1, String num2) {
//处理字符串
SplitResult splitResult1 = new SplitResult(num1);
SplitResult splitResult2 = new SplitResult(num2);
//初始化结果集
StringBuilder stringBuilder = new StringBuilder();
int[] intArray = null;
DecimalResult decimalResult = null;
//首先对两个实数的整数部分进行大小对比
switch (compare(splitResult1.intArray,splitResult2.intArray)){
case 1://num1>num2
intArray = subtractionInt(splitResult1.intArray, splitResult2.intArray);
decimalResult = borrowSubtraction(splitResult1.decimalArray, splitResult2.decimalArray);
break;
case -1: //num1 < num2 给结果先拼接一个负号,注意整数部分交换,小数部分也要跟着交换。
intArray = subtractionInt(splitResult2.intArray, splitResult1.intArray);
decimalResult = borrowSubtraction(splitResult2.decimalArray, splitResult1.decimalArray);
stringBuilder.append("-");
break;
case 0://若两个实数的整数部分相等,则给整数数组直接初始化为0。
intArray = new int[]{0};
int[] newArray ; //由于小数部分左对齐,新建用于给短数组末位补零,然后使用整数减法函数做计算
if (splitResult1.decimalArray.length > splitResult2.decimalArray.length){
newArray = new int[splitResult1.decimalArray.length];
System.arraycopy(splitResult2.decimalArray,0,newArray,0,splitResult2.decimalArray.length);
splitResult2.decimalArray = newArray;
}else {
newArray = new int[splitResult2.decimalArray.length];
System.arraycopy(splitResult1.decimalArray,0,newArray,0,splitResult1.decimalArray.length);
splitResult1.decimalArray = newArray;
}
//再次判断两个小数部分数值的大小
switch(compare(splitResult1.decimalArray,splitResult2.decimalArray)){
//逻辑同上
case 1:
decimalResult = new DecimalResult(subtractionInt(splitResult1.decimalArray, splitResult2.decimalArray),false);
break;
case -1:
stringBuilder.append("-");
decimalResult = new DecimalResult(subtractionInt(splitResult2.decimalArray, splitResult1.decimalArray),false);
break;
case 0:
decimalResult = new DecimalResult(0,false);//如果小数部分也相等则直接置为0
break;
}
break;
}
if (decimalResult.flag) {
subtractionInt(intArray, new int[]{1});
}
append(stringBuilder, intArray, decimalResult.result);
}
判断大小
这是我之前用于比较大小的方法,10是进制,将两个数组的int值求出来,后来发现会有溢出,所以既然是大数运算就要避免基本类型的使用
private static int getInt(int[] array) {
int num = 0;
for (int i = 0; i < array.length; i++) {
num *= 10;
num -= array[i];
}
return -num;
}
目前采用的比较大小的方法,不用做任何计算并且时间复杂度为O(n):大于返回1,小于返回-1,相等返回0
private static int compare(int[] array1, int[] array2) {
int result = 0;
if (array1.length > array2.length){
result = 1;
}else if (array1.length == array2.length){
for (int i = 0; i < array1.length; i++) {
int a = array1[i];
int b = array2[i];
if (a == b) continue;
if (a > b){
result = 1;
break;
}else {
result = -1;
break;
}
}
}else {
result = -1;
}
return result;
}
整数部分相减
private static int[] subtractionInt(int[] bigNumArray, int[] littleNumArray) {//保证大数减小数
boolean borrow = false; //是否借位运算
//依旧是右对齐,下标左移
for (int i = bigNumArray.length - 1, j = littleNumArray.length - 1; j >= 0; i--, j--) {
borrow = subtraction(bigNumArray, borrow, i, bigNumArray[i], littleNumArray[j]);
//若最后一次相减仍需借位
if (j == 0 && borrow) {
//继续依次借位
while (i>0){
int pre = --bigNumArray[i - 1];
if (pre < 0){
bigNumArray[i - 1] = pre+10;
}else {
bigNumArray[i - 1] = pre;
break;
}
i--;
}
}
}
//由于借位会出现零开头的情况,此处去零
if (bigNumArray[0] == 0){
int[] newArray = new int[bigNumArray.length - 1];
System.arraycopy(bigNumArray, 1, newArray, 0, newArray.length);
bigNumArray = newArray;
}
return bigNumArray;
}
小数部分借位相减
private static DecimalResult borrowSubtraction(int[] firstNumArray, int[] secondNumArray) {
int firstNumArrayLength = firstNumArray.length;
int secondNumArrayLength = secondNumArray.length;
int counts = firstNumArrayLength > secondNumArrayLength ? firstNumArrayLength : secondNumArrayLength;
boolean borrow = false;
if (counts == firstNumArrayLength) {
//根据数组长度差异,给短数组不可达的下标位一个0值
for (int i = counts - 1; i >= 0; i--) {
int subtrahend = firstNumArray[i];
int subtractor = i > secondNumArrayLength - 1 ? 0 : secondNumArray[i];
borrow = subtraction(firstNumArray, borrow, i, subtrahend, subtractor);
}
return new DecimalResult(firstNumArray, borrow);
} else {
for (int i = counts - 1; i >= 0; i--) {
int subtrahend = i > firstNumArrayLength - 1 ? 0 : firstNumArray[i];
int subtractor = secondNumArray[i];
borrow = subtraction(secondNumArray, borrow, i, subtrahend, subtractor);
}
return new DecimalResult(secondNumArray, borrow);
}
}
减法逻辑,跟加法逻辑相似
private static boolean subtraction(int[] firstNumArray, boolean borrow, int i, int subtrahend, int subtractor) {
int num = subtrahend - subtractor;
//需要被借位则先减一
if (borrow) {
num--;
borrow = false;
}
//若被减数小于减数,借位减
if (num < 0) {
num += 10;
borrow = true;
}
firstNumArray[i] = num;
return borrow;
}
目前100000+人已关注加入我们