一、前言
一次面试时被问到一个问题:
实现一个求阶乘的方法,要求是能正常上线。
首先想到的就是阶乘的定义: n! = 1 * 2 * 3 * ... * n,1! = 1,0! = 1
所以很容易就能推导出阶乘的递归方程:f(n) = f(n - 1) * n,f(1) = 1,f(0) = 1
咋一看似乎挺简单的,直接上最简单的代码:
public static void main(String[] args) {
try {
System.out.println(factorial(10));
}
catch(Exception e) {
e.printStackTrace();
}
}
private static int factorial(int n) throws Exception {
if(n < 0) {
throw new Exception("Integer out of range.");
}
if(n == 0 || n == 1) {
return 1;
}
// 递归
return factorial(n - 1) * n;
}
但是这段代码有个致命的问题,我们知道java中int的长度为4个字节,最大能表示的整数为 2^31 - 1,可以看到在阶乘这种增长极为迅速的运算下很快便会溢出。即使将int替换成long也只能保证多算几位。
所以这段代码并不实用,需要做一定的改进。
二、大数相乘
大数相乘的解决方案能很好的解决上述问题。
使用 String 或者 int[] 代替原本的 int,这样就能保证运算结果不溢出了。
大数相乘的难点在于:需要手动实现 String 或者 int[] 之间乘法的运算规则(123 x 45 = 123 x 5 + 123 x 4 x 10,应该不需要复习吧)
深化一下运算规则可得到如下公式:
若 n 的位数为 t,且 , 则有:
根据上述公式,可以想到大数相乘一个思路:将长整数拆分成多段短整数相乘,得到的结果进行加权累加
这里我使用将长整数分解为最大长度为4的短整数进行分段乘积求和
(因为最大长度为4不会出现 int 溢出的情况,而长度为5时就可能溢出:99,999 x 99,999 = 9,999,800,001,溢出了)
下面是代码,分别列出上述需求的数组实现和字符串实现,其中数组实现方式执行效率更高
数组实现方式如下:
/**
* 大数相乘与阶乘 - 数组实现
*/
public class Main {
/**
* 数组乘法,将两个数组的乘法分解为一个数组和一个整数的乘法,1000以内的阶乘不需要用到这个方法
* @param num1 乘数,数组中每一位的最大值不超过4位
* @param num2 乘数,数组中每一位的最大值不超过4位
* @return 一个存有乘积的新数组
*/
private static int[] mult(int[] num1, int[] num2) throws Exception {
int[] lowArr = null;
int[] highArr = null;
for(int i = num2.length - 1; i >= 0; i --) {
if(num2[i] >= MAX_LIMIT || num2[i] < 0) {
throw new Exception("Params out of range. num2[" + i + "]: " + num2[i]);
}
highArr = eachMult(num1, num2[i]);
if(lowArr == null) {
lowArr = highArr;
continue;
}
lowArr = addArr(lowArr, highArr);
}
return lowArr;
}
/**
* 数组与整数的乘法,将数组与整数的乘法分解为整数乘法
* @param num1 乘数,数组中每一位的最大值不超过4位
* @param each 乘数,一个不超过4位的正整数
* @return 一个存有乘积的新数组
* @throws Exception
*/
private static int[] eachMult(int[] num1, int each) throws Exception {
if(each >= MAX_LIMIT || each < 0) {
throw new Exception("Params out of range. each: " + each);
}
// 进位数,不为0时需要加到高位中去
int tribute = 0;
for(int i = num1.length - 1; i >= 0; i --) {
if(num1[i] >= MAX_LIMIT || num1[i] < 0) {
throw new Exception("Params out of range. num1[" + i + "]: " + num1[i]);
}
num1[i] = num1[i] * each;
if(tribute > 0) {
num1[i] += tribute;
tribute = 0;
}
// 判断是否进位
if(num1[i] >= MAX_LIMIT) {
tribute = num1[i] / MAX_LIMIT;
num1[i] %= MAX_LIMIT;
}
}
if(tribute > 0) {
int[] newNum1 = new int[num1.length + 1];
System.arraycopy(num1, 0, newNum1, 1, num1.length);
newNum1[0] = tribute;
num1 = newNum1;
}
return num1;
}
/**
* 数组加法,将两个数组进行累加
* @param lowArr 低位加数
* @param highArr 高位加数,相对于低位加数末位自动增加一位
* @return 一个存有和的新数组
*/
private static int[] addArr(int[] lowArr, int[] highArr) {
int lowLen = lowArr.length;
int highPos = highArr.length - 1;
// 进位符
boolean flag = false;
// 遍历低位加数与高位加数求和
for(int i = lowLen - 1; i >= 0; i --) {
// 等价于高位加数自动增加一位
if(i == lowLen - 1) {
continue;
}
// 进位
if(flag) {
lowArr[i] += 1;
flag = false;
}
// 与高位相加
if(highPos >= 0) {
lowArr[i] += highArr[highPos];
highPos --;
}
if(lowArr[i] >= MAX_LIMIT) {
flag = true;
lowArr[i] %= MAX_LIMIT;
}
}
// 当高位比低位位数多时,高位剩余部分需要处理
for(int i = highPos; i >= 0; i --) {
if(flag) {
highArr[i] += 1;
flag = false;
}
if(highArr[i] >= MAX_LIMIT) {
flag = true;
highArr[i] %= MAX_LIMIT;
}
else {
break;
}
}
int startPos= flag ? 1 : 0;
int[] newArr = new int[lowArr.length + highPos + 1 + startPos];
if(flag) {
// 还需要进位时在前面补1
// 此时highArr多余的部分一定都是0,由于int默认是0所以可以省略拷贝
newArr[0] = 1;
}
else {
System.arraycopy(highArr, 0, newArr, startPos, highPos + 1);
}
System.arraycopy(lowArr, 0, newArr, startPos + highPos + 1, lowArr.length);
return newArr;
}
/**
* 将数组按照规则转换成字符串: [1, 0, 2222, 33, 0] -> 10000222200330000
*/
private static String printBigNum(int[] nums) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < nums.length; i++) {
if(i == 0) {
sb.append(nums[i]);
continue;
}
String numStr = String.valueOf(nums[i]);
if(numStr.length() < EACH_MAX_LEN) {
int pNum = EACH_MAX_LEN - numStr.length();
while(pNum -- > 0) {
sb.append(ZERO);
}
}
sb.append(numStr);
}
return sb.toString();
}
public static int[] factorial(int n) throws Exception {
if(n < 0) {
throw new Exception("Integer out of range.");
}
if(n == 0 || n == 1) {
return new int[] { 1 };
}
// 1000以内的阶乘直接调用eachMult即可,
return eachMult(factorial(n - 1), n);
}
public static void main(String[] args) {
try {
System.out.println(printBigNum(factorial(9999)));
}
catch(Exception e) {
e.printStackTrace();
}
}
// 分段长度,数组之间按照乘法规则计算。
// 如:[1, 2345, 6789] * [98, 7654] 分解为 [1, 2345, 6789] * 7654 + [1, 2345, 6789] * 98 * 10000
// 而 [1, 2345, 6789] * 7654 可以继续分解 6789 * 7654 + 2345 * 7654 * 10000 + 1 * 7654 * 100000000
private static final int EACH_MAX_LEN = 4;
private static final int MAX_LIMIT = 10000;
private static final String ZERO = "0";
}
字符串实现方式如下:
import org.apache.commons.lang.StringUtils;
/**
* 大数相乘与阶乘 - 字符串实现
*/
public class Main {
/**
* 两个字符串按照乘法规则相乘,最终会分解为一个字符串和一个4位整数相乘
* @param num1
* @param num2
* @return 两个大数的乘积的字符串表示
* @throws Exception
*/
private static String mult(String num1, String num2) throws Exception {
if(StringUtils.isEmpty(num1) || StringUtils.isEmpty(num2)) {
throw new Exception("Error: String is empty.");
}
// 记录分解为子串后每段计算的中间结果权重相加后的值,所有子串计算完成后该值即为最终结果
StringBuilder tempValue = new StringBuilder();
// 用于记录当前循环中的子串的权重,权重每加1,权值增加10^4倍
int offset = -1;
while(num2.length() != 0) {
String endStr = null;
if(num2.length() < EACH_MAX_LEN) {
endStr = num2;
num2 = "";
}
else {
endStr = num2.substring(num2.length() - EACH_MAX_LEN);
num2 = num2.substring(0, num2.length() - EACH_MAX_LEN);
}
offset += 1;
// 每段子串与另一个乘数相乘的值
StringBuilder multAns = eachMult(num1, Integer.valueOf(endStr));
// 为所得值加权
for(int i = 0; i < offset; i ++) {
multAns.append(OFFSET_ZEROS);
}
tempValue = addStr(multAns, tempValue);
}
return tempValue.toString();
}
/**
* 将长字符串与一个四位整数按照乘法规则相乘
* (最终会分解为两个4位整数多次相乘,两个5位整数的乘积可能会溢出)
* @param num1
* @param num2
* @throws Exception
*/
private static StringBuilder eachMult(String num1, int num2) throws Exception {
if(num2 > 9999) {
throw new Exception("Params out of range.");
}
// 记录分解为子串后每段计算的中间结果权重相加后的值,所有子串计算完成后该值即为最终结果
StringBuilder tempValue = new StringBuilder();
// 用于记录当前循环中的子串的权重,权重每加1,权值增加10^4倍
int offset = -1;
while(num1.length() != 0) {
String endStr = null;
if(num1.length() < EACH_MAX_LEN) {
endStr = num1;
num1 = "";
}
else {
endStr = num1.substring(num1.length() - EACH_MAX_LEN);
num1 = num1.substring(0, num1.length() - EACH_MAX_LEN);
}
offset += 1;
// 每段子串与另一个乘数相乘的值
StringBuilder multAns = new StringBuilder(String.valueOf(Integer.valueOf(endStr) * num2));
// 为所得值加权
for(int i = 0; i < offset; i ++) {
multAns.append(OFFSET_ZEROS);
}
tempValue = addStr(multAns, tempValue);
}
return tempValue;
}
/**
* 将两个长字符串按照整数规则相加
* @param str1
* @param str2
*/
private static StringBuilder addStr(StringBuilder str1, StringBuilder str2) {
// 字符串倒序,末位对齐更方便
String s1 = str1.reverse().toString();
String s2 = str2.reverse().toString();
// 确保字符串 s1 的长度大于 s2
if(s1.length() < s2.length()) {
String temp = s1;
s1 = s2;
s2 = temp;
}
int max_len = s1.length();
// 进位标志位
boolean flag = false;
StringBuilder sb = new StringBuilder();
// 循环进行同位相加操作
for(int i = 0; i < max_len; i ++) {
// 同位相加的临时结果
int temp = s1.charAt(i) - ZERO;
// 若有进位,则加上进位,加法的进位值为 1
if(flag) {
temp += 1;
flag = false;
}
// 若 s2 存在第 i + 1 位,则相加
if(i < s2.length()) {
temp += s2.charAt(i) - ZERO;
}
// 若同位相加值大于9,则需要进位
if(temp >9) {
flag = true;
temp -= 10;
}
sb.append(temp);
}
// 循环完成后需要判断是否还要进一位
if(flag) {
sb.append(1);
}
// 字符串顺序改为正序并返回
return sb.reverse();
}
/**
* 阶乘的递归函数
*/
private static String factorial(int n) throws Exception {
if(n < 0) {
throw new Exception("Integer out of range.");
}
if(n == 0 || n == 1) {
return "1";
}
return mult(factorial(n - 1), String.valueOf(n));
}
public static void main(String[] args) {
try {
for(int i = 0; i < 100; i ++) {
System.out.print(i + "的阶乘:");
System.out.println(factorial(i));
}
}
catch(Exception e) {
e.printStackTrace();
}
}
// 分段长度,字符串之间按照乘法规则计算。
// 如:123456789 * 987654 分解为 123456789 * 7654 + 123456789 * 98 * 10000
// 而 123456789 * 7654 可以继续分解 6789 * 7654 + 2345 * 7654 * 10000 + 1 * 7654 * 100000000
private static final int EACH_MAX_LEN = 4;
// 与 EACH_MAX_LEN 关联,EACH_MAX_LEN 即为 "0" 的个数
private static final String OFFSET_ZEROS = "0000";
private static final char ZERO = '0';
}