C语言实现大整数加减运算详解

前言

我们知道,在数学中,数值的大小是没有上限的,但是在计算机中,由于字长的限制,计算机所能表示的范围是有限的,当我们对比较小的数进行运算时,如:1234+5678,这样的数值并没有超出计算机的表示范围,所以可以运算。但是当我们在实际的应用中进行大量的数据处理时,会发现参与运算的数往往超过计算机的基本数据类型的表示范围,比如说,在天文学上,如果一个星球距离我们为100万光年,那么我们将其化简为公里,或者是米的时候,我们会发现这是一个很大的数。这样计算机将无法对其进行直接计算。

可能我们认为实际应用中的大数也不过就是几百位而已,实际上,在某些领域里,甚至可能出现几百万位的数据进行运算,这是我们很难想象的。如果没有计算机,那么计算效率可想而知。

由于编程语言提供的基本数值数据类型表示的数值范围有限,不能满足较大规模的高精度数值计算,因此需要利用其他方法实现高精度数值的计算,于是产生了大数运算。本项目实现了大数运算的加、减运算。

一. 问题提出

用C语言实现一个大整数计算器。初步要求支持大整数的加、减运算,例如8888888888888+1112=8888888890000或1000000000000-999999999999=1。

C语言中,整型变量所能存储的最宽数据为0xFFFF FFFF,对应的无符号数为4294967295,即无法保存超过10位的整数。注意,此处"10位"指数学中的10个数字,并非计算机科学中的10比特。浮点类型double虽然可以存储更多位数的整数,但一方面常数字面量宽度受编译器限制,另一方面通过浮点方式处理整数精度较低。例如:

  double a = 1377083362513770833626.0, b=1585054852315850548524.0;
  printf("res = %.0f\n", a+b);

输出为res = 2962138214829621510144,而正确值应为2962138214829621382150。

既然基本数据类型无法表示大整数,那么只能自己设计存储方式来实现大整数的表示和运算。通常,输入的大整数为字符串形式。因此,常见的思路是将大整数字符串转化为数组,再用数组模拟大整数的运算。具体而言,先将字符串中的数字字符顺序存入一个较大的整型数组,其元素代表整数的某一位或某几位(如万进制);然后根据运算规则操作数组元素,以模拟整数运算;最后,将数组元素顺序输出。

数组方式操作方便,实现简单,缺点是空间利用率和执行效率不高。也可直接操作大整数字符串,从字符串末尾逆向计算。本文实现就采用这种方式。

二. 代码实现

首先,给出几个宏定义和运算结构:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#define ADD_THRES   (sizeof("4294967295")-2) //两个9位整数相加不会溢出
#define MUL_THRES   (sizeof("65535")-2)    //两个4位整数相乘不会溢出
#define OTH_THRES   (sizeof("4294967295")-1) //两个10位整数相减或相除不会溢出

typedef struct{
  char *leftVal;
  char *rightVal;
  char operator;
}MATH_OPER;

基于上述定义,以下将依次给出运算代码的实现。

加法运算主要关注相加过程中的进位问题:

void Addition(char *leftVal, char *rightVal,
       char *resBuf, unsigned int resbufLen) {
  unsigned int leftLen = strlen(leftVal);
  unsigned int rightLen = strlen(rightVal);

  unsigned char isLeftLonger = (leftLen>=rightLen) ? 1 : 0;
  unsigned int longLen = isLeftLonger ? leftLen : rightLen;
  if(resbufLen < longLen) { //possible carry + string terminator
    fprintf(stderr, "Not enough space for result(cur:%u)!\n", resbufLen);
    return;
  }

  char *longAddend = isLeftLonger ? leftVal : rightVal;
  char *shortAddend = isLeftLonger ? rightVal : leftVal;
  unsigned int diffLen = isLeftLonger ? (leftLen-rightLen) : (rightLen-leftLen);
  //a carry might be generated from adding the most significant digit
  if((leftLen == rightLen) && (leftVal[0]-'0'+rightVal[0]-'0' >= 9))
    resBuf += 1;

  unsigned int carry = 0;
  int i = longLen-1;
  for(; i >= 0; i--) {
    unsigned int leftAddend = longAddend[i] - '0';
    unsigned int rightAddend = (i<diffLen) ? 0 : shortAddend[i-diffLen]-'0';
    unsigned int digitSum = leftAddend + rightAddend + carry;
    resBuf[i] = digitSum % 10 + '0';
    carry = (digitSum >= 10) ? 1 : 0;
  }
  if(carry == 1) {
    resBuf -= 1;
    resBuf[0] = '1';
  }
  else if(leftVal[0]-'0'+rightVal[0]-'0' == 9) {
    resBuf -= 1;
    resBuf[0] = ' '; //fail to generate a carry
  }
}

注意第33~36行的处理,当最高位未按期望产生进位时,原来为0的resBuf[0]被置为空格字符,否则将无法输出运算结果。当然,也可将resBuf整体前移一个元素。

减法运算相对复杂,需要根据被减数和减数的大小调整运算顺序。若被减数小于减数("11-111"或"110-111"),则交换被减数和减数后再做正常的减法运算,并且结果需添加负号前缀。此外,还需关注借位问题。

void Subtraction(char *leftVal, char *rightVal,
         char *resBuf, unsigned int resbufLen) {
  int cmpVal = strcmp(leftVal, rightVal);
  if(!cmpVal) {
    resBuf[0] = '0';
    return;
  }

  unsigned int leftLen = strlen(leftVal);
  unsigned int rightLen = strlen(rightVal);
  unsigned char isLeftLonger = 0;
  if((leftLen > rightLen) ||       //100-10
    (leftLen == rightLen && cmpVal > 0)) //100-101
    isLeftLonger = 1;
  unsigned int longLen = isLeftLonger ? leftLen : rightLen;
  if(resbufLen <= longLen) { //string terminator
    fprintf(stderr, "Not enough space for result(cur:%u)!\n", resbufLen);
    return;
  }

  char *minuend = isLeftLonger ? leftVal : rightVal;
  char *subtrahend = isLeftLonger ? rightVal : leftVal;
  unsigned int diffLen = isLeftLonger ? (leftLen-rightLen) : (rightLen-leftLen);
  //a borrow will be generated from subtracting the most significant digit
  if(!isLeftLonger) {
    resBuf[0] = '-';
    resBuf += 1;
  }

  unsigned int borrow = 0;
  int i = longLen-1;
  for(; i >= 0; i--)
  {
    unsigned int expanSubtrahend = (i<diffLen) ? '0' : subtrahend[i-diffLen];
    int digitDif = minuend[i] - expanSubtrahend - borrow;
    borrow = (digitDif < 0) ? 1 : 0;
    resBuf[i] = digitDif + borrow*10 + '0';
    //printf("[%d]Dif=%d=%c-%c-%d -> %c\n", i, digitDif, minuend[i], expanSubtrahend, borrow, resBuf[i]);
  }

  //strip leading '0' characters
  int iSrc = 0, iDst = 0, isStripped = 0;
  while(resBuf[iSrc] !='\0') {
    if(isStripped) {
      resBuf[iDst] = resBuf[iSrc];
      iSrc++; iDst++;
    }
    else if(resBuf[iSrc] != '0') {
      resBuf[iDst] = resBuf[iSrc];
      iSrc++; iDst++;
      isStripped = 1;
    }
    else
      iSrc++;
   }
   resBuf[iDst] = '\0';
}

对于Addition()Subtraction()函数,设计测试用例如下:

#include<assert.h>
#define ASSERT_ADD(_add1, _add2, _sum) do{\
  char resBuf[100] = {0}; \
  Addition(_add1, _add2, resBuf, sizeof(resBuf)); \
  assert(!strcmp(resBuf, _sum)); \
}while(0)
#define ASSERT_SUB(_minu, _subt, _dif) do{\
  char resBuf[100] = {0}; \
  Subtraction(_minu, _subt, resBuf, sizeof(resBuf)); \
  assert(!strcmp(resBuf, _dif)); \
}while(0)
void VerifyOperation(void) {
  ASSERT_ADD("22", "1686486458", "1686486480");
  ASSERT_ADD("8888888888888", "1112", "8888888890000");
  ASSERT_ADD("1234567890123", "1", "1234567890124");
  ASSERT_ADD("1234567890123", "3333333333333", "4567901223456");
  ASSERT_ADD("1234567890123", "9000000000000", "10234567890123");
  ASSERT_ADD("1234567890123", "8867901223000", "10102469113123");
  ASSERT_ADD("1234567890123", "8000000000000", " 9234567890123");
  ASSERT_ADD("1377083362513770833626", "1585054852315850548524", "2962138214829621382150");

  ASSERT_SUB("10012345678890", "1", "10012345678889");
  ASSERT_SUB("1", "10012345678890", "-10012345678889");
  ASSERT_SUB("10012345678890", "10012345678891", "-1");
  ASSERT_SUB("10012345678890", "10012345686945", "-8055");
  ASSERT_SUB("1000000000000", "999999999999", "1");
}

考虑到语言内置的运算效率应该更高,因此在不可能产生溢出时尽量选用内置运算。CalcOperation()函数便采用这一思路:

void CalcOperation(MATH_OPER *mathOper, char *resBuf, unsigned int resbufLen) {
  unsigned int leftLen = strlen(mathOper->leftVal);
  unsigned int rightLen = strlen(mathOper->rightVal);

  switch(mathOper->operator) {
    case '+':
      if(leftLen <= ADD_THRES && rightLen <= ADD_THRES)
        snprintf(resBuf, resbufLen, "%d",
             atoi(mathOper->leftVal) + atoi(mathOper->rightVal));
      else
        Addition(mathOper->leftVal, mathOper->rightVal, resBuf, resbufLen);
      break;
    case '-':
      if(leftLen <= OTH_THRES && rightLen <= OTH_THRES)
        snprintf(resBuf, resbufLen, "%d",
             atoi(mathOper->leftVal) - atoi(mathOper->rightVal));
      else
        Subtraction(mathOper->leftVal, mathOper->rightVal, resBuf, resbufLen);
      break;
    case '*':
      if(leftLen <= MUL_THRES && rightLen <= MUL_THRES)
        snprintf(resBuf, resbufLen, "%d",
             atoi(mathOper->leftVal) * atoi(mathOper->rightVal));
      else
        break; //Multiplication: product = multiplier * multiplicand
      break;
    case '/':
      if(leftLen <= OTH_THRES && rightLen <= OTH_THRES)
        snprintf(resBuf, resbufLen, "%d",
             atoi(mathOper->leftVal) / atoi(mathOper->rightVal));
      else
        break; //Division: quotient = dividend / divisor
      break;
    default:
      break;
  }

  return;
}

注意,大整数的乘法和除法运算尚未实现,因此相应代码分支直接返回。

最后,完成入口函数:

int main(void) {
  VerifyOperation();

  char leftVal[100] = {0}, rightVal[100] = {0}, operator='+';
  char resBuf[1000] = {0};

  //As you see, basically any key can quit:)
  printf("Enter math expression(press q to quit): ");
  while(scanf(" %[0-9] %[+-*/] %[0-9]", leftVal, &operator, rightVal) == 3) {
    MATH_OPER mathOper = {leftVal, rightVal, operator};
    memset(resBuf, 0, sizeof(resBuf));
    CalcOperation(&mathOper, resBuf, sizeof(resBuf));
    printf("%s %c %s = %s\n", leftVal, operator, rightVal, resBuf);
    printf("Enter math expression(press q to quit): ");
  }
  return 0;
}

上述代码中,scanf()函数的格式化字符串风格类似正则表达式。其详细介绍参见《sscanf的字符串格式化用法》一文。

三. 效果验证

将上节代码存为BigIntOper.c文件。测试结果如下:

[wangxiaoyuan_@localhost ~]$ gcc -Wall -o BigIntOper BigIntOper.c
[wangxiaoyuan_@localhost ~]$ ./BigIntOper
Enter math expression(press q to quit): 100+901
100 + 901 = 1001
Enter math expression(press q to quit): 100-9
100 - 9 = 91
Enter math expression(press q to quit): 1234567890123 + 8867901223000
1234567890123 + 8867901223000 = 10102469113123
Enter math expression(press q to quit): 1377083362513770833626 - 1585054852315850548524
1377083362513770833626 - 1585054852315850548524 = -207971489802079714898
Enter math expression(press q to quit): q
[wangxiaoyuan_@localhost ~]$ 

通过内部测试用例和外部人工校验,可知运算结果正确无误。

总结

以上就是C语言实现大整数加减运算的全部内容,大家都学会了吗?希望本文的内容对大家学习C语言能有所帮助。

(0)

相关推荐

  • c++加法高精度算法的简单实现

    c++高精度算法,对于新手来说还是一大挑战,只要克服它,你就开启了编程的新篇章,算法. 我发的这个代码并不是很好,占用内存很多而且运行时间很长(不超过1秒),但是很好理解,很适合新手 高精算法的本质就是把数组编程字符串,然后将字符串像竖式一样加起来: a+b高精度算法 #include <iostream> #include <cmath> #include <cstring> using namespace std; int main() { char a[10001

  • C语言实现大整数加减运算详解

    前言 我们知道,在数学中,数值的大小是没有上限的,但是在计算机中,由于字长的限制,计算机所能表示的范围是有限的,当我们对比较小的数进行运算时,如:1234+5678,这样的数值并没有超出计算机的表示范围,所以可以运算.但是当我们在实际的应用中进行大量的数据处理时,会发现参与运算的数往往超过计算机的基本数据类型的表示范围,比如说,在天文学上,如果一个星球距离我们为100万光年,那么我们将其化简为公里,或者是米的时候,我们会发现这是一个很大的数.这样计算机将无法对其进行直接计算. 可能我们认为实际应

  • Java利用位运算实现加减运算详解

    目录 前言 思路分析 示例 位运算进位 初步结果 去除加号 整体思路 加法代码实现 减法实现 减法分析 减法代码实现 总结 前言 本文主要介绍如何使用位运算来实现加减功能,也就是在整个运算过程中不能出现加减符号. 加减乘除运算在计算机中,实际上都是用位运算实现的,今天就用位运算来模拟下加法和减法的运算功能. 思路分析 先分析如何用位运算实现加法运算. 示例 假设a=23,b=36,使用位运算实现加法得到结果59. 首先来看下23.36.59的二进制信息. 从上面的图中可以看到,两个数相加的结果与

  • C语言中指针的加减运算方法示例

    参考文章,值得一看 char arr[3]; printf("arr:\n%d\n%d\n%d\n", arr, arr + 1, arr + 2); char *parr[3]; printf("parr:\n%d\n%d\n%d\n", parr, parr + 1, parr + 2); 从结果可以看到,字符数组每个元素占1字节,字符指针数组每个占4字节. 再看一个例子: char a = 'a', b = 'b', c = 'c', d = 'd'; cha

  • ThinkPHP自定义函数解决模板标签加减运算的方法

    本文实例讲述了ThinkPHP自定义函数解决模板标签加减运算的方法.分享给大家供大家参考.具体如下: 实际项目中,我们经常需要标签变量加减运算的操作.但是,在ThinkPHP中,并不支持模板变量直接运算的操作. 幸运的是,它提供了自定义函数的方法,我们可以利用自定义函数解决: ThinkPHP模板自定义函数语法如下: 格式:{:function(-)} (参考官方帮助文档:http://thinkphp.cn/Manual/196) 利用这个,我们来试做加法和减法. 一.在ThinkPHP中定义

  • PHP实现针对日期,月数,天数,周数,小时,分,秒等的加减运算示例【基于strtotime】

    本文实例讲述了PHP实现针对日期,月数,天数,周数,小时,分,秒等的加减运算方法.分享给大家供大家参考,具体如下: 其实就是strtotime这个内置函数 //PHP 日期 加减 周 date("Y-m-d",strtotime("2013-11-12 +1 week")) //PHP 日期 加减 天数 date("Y-m-d",strtotime("2013-11-12 12:12:12 +1 day")) //PHP 日期

  • js实现文本框支持加减运算的方法

    本文实例讲述了js实现文本框支持加减运算的方法.分享给大家供大家参考.具体如下: 这是一个网页表单效果,让表单内的文本框支持加减运算,不过你要按正确的运算式输入,要不然它没有那么智能哦,比如输入1+5,文本框旁边会显示计算结果,这要归功于JavaScript的功能. 运行效果截图如下: 在线演示地址如下: http://demo.jb51.net/js/2015/js-math-input-method-codes/ 具体代码如下: <!DOCTYPE html PUBLIC "-//W3

  • Vue入门之数量加减运算操作示例

    本文实例讲述了Vue入门之数量加减运算操作.分享给大家供大家参考,具体如下: 效果图: HTML: <div class="count3"> <ul> <li v-for="(key,idx) in liList" :key="key.id"> {{key.id}},{{idx}} <template> <button class="cut" @click="cu

  • 使用MYSQL TIMESTAMP字段进行时间加减运算问题

    目录 MYSQL TIMESTAMP字段进行时间加减运算 计算公式如下 DATETIME 与 TIMESTAMP的区别 结论 参考文档 MYSQL TIMESTAMP字段进行时间加减运算 在数据分析过程中,想当然地对TIMESTAMP字段进行运算,导致结果谬之千里 计算公式如下 -- create_time与week_time的声明都是TIMESTAMP(), 要求精确到分钟 -- SELECT (sa.create_time - sa.week_time)/(1000 * 60) from a

  • C 语言指针变量的运算详解

    指针变量保存的是地址,本质上是一个整数,可以进行部分运算,例如加法.减法.比较等,请看下面的代码: #include <stdio.h> int main(){ int a = 10, *pa = &a, *paa = &a; double b = 99.9, *pb = &b; char c = '@', *pc = &c; //最初的值 printf("&a=%#X, pa=%#X, pb=%#X, pc=%#X\n", &

  • go语言题解LeetCode66加一示例详解

    目录 题目描述 思路分析 AC 代码 小结 JavaScript 66题 代码 python3 循环判断 分析: JAVA解决进位问题 解题思路 代码 题目描述 原题链接 : 66. 加一 给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一. 最高位数字存放在数组的首位, 数组中每个元素只存储单个数字. 你可以假设除了整数 0 之外,这个整数不会以零开头. 示例 1: 输入:digits = [1,2,3] 输出:[1,2,4] 解释:输入数组表示数字 123. 示例 2:

随机推荐