JAVA大数相乘与阶乘递归

一、前言

一次面试时被问到一个问题:

实现一个求阶乘的方法,要求是能正常上线。

首先想到的就是阶乘的定义: n! = 1 * 2 * 3 * ... * n1! = 10! = 1

所以很容易就能推导出阶乘的递归方程:f(n) = f(n - 1) * nf(1) = 1f(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,且 0 \leqslant k\leqslant tk\in N 则有:

{\color{Red} m\ast n=m\ast (n\; M\! od\; 10^{k})+m\ast (\left \lfloor n\div 10^{k} \right \rfloor)\ast 10^{k}}

根据上述公式,可以想到大数相乘一个思路:将长整数拆分成多段短整数相乘,得到的结果进行加权累加

这里我使用将长整数分解为最大长度为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';
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值