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

标签: 大整数加减 C


一. 问题提出

培训老师给出一个题目:用C语言实现一个大整数计算器。初步要求支持大整数的加、减运算,例如8888888888888+1112=88888888900001000000000000-999999999999=1

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

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

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

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

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

二. 代码实现

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<string.h>
  4. #define ADD_THRES (sizeof("4294967295")-2) //两个9位整数相加不会溢出
  5. #define MUL_THRES (sizeof("65535")-2) //两个4位整数相乘不会溢出
  6. #define OTH_THRES (sizeof("4294967295")-1) //两个10位整数相减或相除不会溢出
  7. typedef struct{
  8. char *leftVal;
  9. char *rightVal;
  10. char operator;
  11. }MATH_OPER;

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

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

  1. void Addition(char *leftVal, char *rightVal,
  2. char *resBuf, unsigned int resbufLen) {
  3. unsigned int leftLen = strlen(leftVal);
  4. unsigned int rightLen = strlen(rightVal);
  5. unsigned char isLeftLonger = (leftLen>=rightLen) ? 1 : 0;
  6. unsigned int longLen = isLeftLonger ? leftLen : rightLen;
  7. if(resbufLen < longLen) { //possible carry + string terminator
  8. fprintf(stderr, "Not enough space for result(cur:%u)!\n", resbufLen);
  9. return;
  10. }
  11. char *longAddend = isLeftLonger ? leftVal : rightVal;
  12. char *shortAddend = isLeftLonger ? rightVal : leftVal;
  13. unsigned int diffLen = isLeftLonger ? (leftLen-rightLen) : (rightLen-leftLen);
  14. //a carry might be generated from adding the most significant digit
  15. if((leftLen == rightLen) && (leftVal[0]-'0'+rightVal[0]-'0' >= 9))
  16. resBuf += 1;
  17. unsigned int carry = 0;
  18. int i = longLen-1;
  19. for(; i >= 0; i--) {
  20. unsigned int leftAddend = longAddend[i] - '0';
  21. unsigned int rightAddend = (i<diffLen) ? 0 : shortAddend[i-diffLen]-'0';
  22. unsigned int digitSum = leftAddend + rightAddend + carry;
  23. resBuf[i] = digitSum % 10 + '0';
  24. carry = (digitSum >= 10) ? 1 : 0;
  25. }
  26. if(carry == 1) {
  27. resBuf -= 1;
  28. resBuf[0] = '1';
  29. }
  30. else if(leftVal[0]-'0'+rightVal[0]-'0' == 9) {
  31. resBuf -= 1;
  32. resBuf[0] = ' '; //fail to generate a carry
  33. }
  34. }

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

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

  1. void Subtraction(char *leftVal, char *rightVal,
  2. char *resBuf, unsigned int resbufLen) {
  3. int cmpVal = strcmp(leftVal, rightVal);
  4. if(!cmpVal) {
  5. resBuf[0] = '0';
  6. return;
  7. }
  8. unsigned int leftLen = strlen(leftVal);
  9. unsigned int rightLen = strlen(rightVal);
  10. unsigned char isLeftLonger = 0;
  11. if((leftLen > rightLen) || //100-10
  12. (leftLen == rightLen && cmpVal > 0)) //100-101
  13. isLeftLonger = 1;
  14. unsigned int longLen = isLeftLonger ? leftLen : rightLen;
  15. if(resbufLen <= longLen) { //string terminator
  16. fprintf(stderr, "Not enough space for result(cur:%u)!\n", resbufLen);
  17. return;
  18. }
  19. char *minuend = isLeftLonger ? leftVal : rightVal;
  20. char *subtrahend = isLeftLonger ? rightVal : leftVal;
  21. unsigned int diffLen = isLeftLonger ? (leftLen-rightLen) : (rightLen-leftLen);
  22. //a borrow will be generated from subtracting the most significant digit
  23. if(!isLeftLonger) {
  24. resBuf[0] = '-';
  25. resBuf += 1;
  26. }
  27. unsigned int borrow = 0;
  28. int i = longLen-1;
  29. for(; i >= 0; i--)
  30. {
  31. unsigned int expanSubtrahend = (i<diffLen) ? '0' : subtrahend[i-diffLen];
  32. int digitDif = minuend[i] - expanSubtrahend - borrow;
  33. borrow = (digitDif < 0) ? 1 : 0;
  34. resBuf[i] = digitDif + borrow*10 + '0';
  35. //printf("[%d]Dif=%d=%c-%c-%d -> %c\n", i, digitDif, minuend[i], expanSubtrahend, borrow, resBuf[i]);
  36. }
  37. //strip leading '0' characters
  38. int iSrc = 0, iDst = 0, isStripped = 0;
  39. while(resBuf[iSrc] !='\0') {
  40. if(isStripped) {
  41. resBuf[iDst] = resBuf[iSrc];
  42. iSrc++; iDst++;
  43. }
  44. else if(resBuf[iSrc] != '0') {
  45. resBuf[iDst] = resBuf[iSrc];
  46. iSrc++; iDst++;
  47. isStripped = 1;
  48. }
  49. else
  50. iSrc++;
  51. }
  52. resBuf[iDst] = '\0';
  53. }

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

  1. #include<assert.h>
  2. #define ASSERT_ADD(_add1, _add2, _sum) do{\
  3. char resBuf[100] = {0}; \
  4. Addition(_add1, _add2, resBuf, sizeof(resBuf)); \
  5. assert(!strcmp(resBuf, _sum)); \
  6. }while(0)
  7. #define ASSERT_SUB(_minu, _subt, _dif) do{\
  8. char resBuf[100] = {0}; \
  9. Subtraction(_minu, _subt, resBuf, sizeof(resBuf)); \
  10. assert(!strcmp(resBuf, _dif)); \
  11. }while(0)
  12. void VerifyOperation(void) {
  13. ASSERT_ADD("22", "1686486458", "1686486480");
  14. ASSERT_ADD("8888888888888", "1112", "8888888890000");
  15. ASSERT_ADD("1234567890123", "1", "1234567890124");
  16. ASSERT_ADD("1234567890123", "3333333333333", "4567901223456");
  17. ASSERT_ADD("1234567890123", "9000000000000", "10234567890123");
  18. ASSERT_ADD("1234567890123", "8867901223000", "10102469113123");
  19. ASSERT_ADD("1234567890123", "8000000000000", " 9234567890123");
  20. ASSERT_ADD("1377083362513770833626", "1585054852315850548524", "2962138214829621382150");
  21. ASSERT_SUB("10012345678890", "1", "10012345678889");
  22. ASSERT_SUB("1", "10012345678890", "-10012345678889");
  23. ASSERT_SUB("10012345678890", "10012345678891", "-1");
  24. ASSERT_SUB("10012345678890", "10012345686945", "-8055");
  25. ASSERT_SUB("1000000000000", "999999999999", "1");
  26. }

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

  1. void CalcOperation(MATH_OPER *mathOper, char *resBuf, unsigned int resbufLen) {
  2. unsigned int leftLen = strlen(mathOper->leftVal);
  3. unsigned int rightLen = strlen(mathOper->rightVal);
  4. switch(mathOper->operator) {
  5. case '+':
  6. if(leftLen <= ADD_THRES && rightLen <= ADD_THRES)
  7. snprintf(resBuf, resbufLen, "%d",
  8. atoi(mathOper->leftVal) + atoi(mathOper->rightVal));
  9. else
  10. Addition(mathOper->leftVal, mathOper->rightVal, resBuf, resbufLen);
  11. break;
  12. case '-':
  13. if(leftLen <= OTH_THRES && rightLen <= OTH_THRES)
  14. snprintf(resBuf, resbufLen, "%d",
  15. atoi(mathOper->leftVal) - atoi(mathOper->rightVal));
  16. else
  17. Subtraction(mathOper->leftVal, mathOper->rightVal, resBuf, resbufLen);
  18. break;
  19. case '*':
  20. if(leftLen <= MUL_THRES && rightLen <= MUL_THRES)
  21. snprintf(resBuf, resbufLen, "%d",
  22. atoi(mathOper->leftVal) * atoi(mathOper->rightVal));
  23. else
  24. break; //Multiplication: product = multiplier * multiplicand
  25. break;
  26. case '/':
  27. if(leftLen <= OTH_THRES && rightLen <= OTH_THRES)
  28. snprintf(resBuf, resbufLen, "%d",
  29. atoi(mathOper->leftVal) / atoi(mathOper->rightVal));
  30. else
  31. break; //Division: quotient = dividend / divisor
  32. break;
  33. default:
  34. break;
  35. }
  36. return;
  37. }

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

最后,完成入口函数:

  1. int main(void) {
  2. VerifyOperation();
  3. char leftVal[100] = {0}, rightVal[100] = {0}, operator='+';
  4. char resBuf[1000] = {0};
  5. //As you see, basically any key can quit:)
  6. printf("Enter math expression(press q to quit): ");
  7. while(scanf(" %[0-9] %[+-*/] %[0-9]", leftVal, &operator, rightVal) == 3) {
  8. MATH_OPER mathOper = {leftVal, rightVal, operator};
  9. memset(resBuf, 0, sizeof(resBuf));
  10. CalcOperation(&mathOper, resBuf, sizeof(resBuf));
  11. printf("%s %c %s = %s\n", leftVal, operator, rightVal, resBuf);
  12. printf("Enter math expression(press q to quit): ");
  13. }
  14. return 0;
  15. }

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

三. 效果验证

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

  1. [wangxiaoyuan_@localhost ~]$ gcc -Wall -o BigIntOper BigIntOper.c
  2. [wangxiaoyuan_@localhost ~]$ ./BigIntOper
  3. Enter math expression(press q to quit): 100+901
  4. 100 + 901 = 1001
  5. Enter math expression(press q to quit): 100-9
  6. 100 - 9 = 91
  7. Enter math expression(press q to quit): 1234567890123 + 8867901223000
  8. 1234567890123 + 8867901223000 = 10102469113123
  9. Enter math expression(press q to quit): 1377083362513770833626 - 1585054852315850548524
  10. 1377083362513770833626 - 1585054852315850548524 = -207971489802079714898
  11. Enter math expression(press q to quit): q
  12. [wangxiaoyuan_@localhost ~]$

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

大整数加减运算的C语言实现的更多相关文章

  1. C语言中指针变量的加减运算

    1.指针变量中存放的是地址值,也就是一个数字地址,例如某指针变量中的值是0x20000000,表示表示此指针变量存放的是内存中位于0x20000000地方的内存地址.指针变量可以加减,但是只能与整型数 ...

  2. C语言中指针的加减运算

    参考文章,值得一看 char arr[3]; printf("arr:\n%d\n%d\n%d\n", arr, arr + 1, arr + 2); char *parr[3]; ...

  3. [Swift]LeetCode592. 分数加减运算 | Fraction Addition and Subtraction

    Given a string representing an expression of fraction addition and subtraction, you need to return t ...

  4. Leetcode 592.分数加减运算

    分数加减运算 给定一个表示分数加减运算表达式的字符串,你需要返回一个字符串形式的计算结果. 这个结果应该是不可约分的分数,即最简分数. 如果最终结果是一个整数,例如 2,你需要将它转换成分数形式,其分 ...

  5. Java实现 LeetCode 592 分数加减运算(纯体力活)

    592. 分数加减运算 给定一个表示分数加减运算表达式的字符串,你需要返回一个字符串形式的计算结果. 这个结果应该是不可约分的分数,即最简分数. 如果最终结果是一个整数,例如 2,你需要将它转换成分数 ...

  6. velocity加减运算注意格式 ,加减号的左右都要有空格

    velocity加减运算注意格式 ,加减号的左右都要有空格 #set( $left= $!biz.value - $vMUtils.getReturnMoney($!biz.billBuy) )

  7. Problem B: 大整数的加法运算

    Problem B: 大整数的加法运算 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 112  Solved: 57[Submit][Status][W ...

  8. Linux中日期的加减运算

    Linux中日期的加减运算 目录 在显示方面 在设定时间方面 时间的加减 正文 date命令本身提供了日期的加减运算. date 可以用来显示或设定系统的日期与时间. 回到顶部 在显示方面 使用者可以 ...

  9. void *指针的加减运算

    1.手工写了一个程序验证void *指针加减运算移动几个字节: //本程序验证空类型指针减1移动几个字节 #include <stdio.h> int main(int argc, cha ...

随机推荐

  1. hdu6026 Deleting Edges(Dijkstra+思路)

    https://vjudge.net/problem/HDU-6026 我一直想不明白的是,它的乘法是如何保证n-1条边的.后来画了一张图大概能明白了. 结合最后的乘法二层循环的代码来看,当i=4的时 ...

  2. APB协议

    https://wenku.baidu.com/view/2663f629ef06eff9aef8941ea76e58fafab04592.html https://www.cnblogs.com/l ...

  3. jsp执行过程图解

    转自:https://blog.csdn.net/y277an/article/details/76561451 一.jsp执行过程图解 用户访问jsp页面时,jsp的处理过程如下图所示: 二.预处理 ...

  4. 目前流行前端几大UI框架

    title: "Windows照片查看器-召回大法" categories: windows tags: windows author: LIUREN --- Windows照片查 ...

  5. [Java] Windows/Linux路径不同时,统一war的最简办法

    作者: zyl910 一.缘由 在项目开发时,因为运行环境的不同,导致有时得分别为不同的环境,切换配置参数打不同war包.但手工切换配置文件的话,不仅费时费力,而且容易出错. 有些打包工具支持配置切换 ...

  6. [STF手机设备管理平台]连接其它操作系统上的安卓设备实操介绍

    一.背景 看到之前曾有人发贴,贴名[stf 连接各操作系统上安卓设备的操作方法分享],介绍了一下,虽然说方法和理论都有,但下述评论中还是有很多人不知如何操作,特别是不知道stf provider命令如 ...

  7. keil软件错误总结.doc

    KEIL编译错误信息表   错误代码及错误信息 错误释义 error 1: Out of memory 内存溢出 error 2: Identifier expected 缺标识符 error 3: ...

  8. Mac NVM 配置

    1.NVM 简介 NVM(node version manager)是一个可以让你在同一台机器上安装和切换不同版本 node 的工具. GitHub 地址 2.NVM 环境配置 2.1 安装 NVM ...

  9. 设计模式? GoF

    GoF  >>> Gang of Four.四人帮 是Design Patterns: Elements of Reusable Object-Oriented Software ( ...

  10. CentOS 7 yum nginx MySQL PHP7 简易环境搭建(精)

    用centos自带的yum源来安装nginx,mysql和php,超级方便,省去编译的麻烦,省去自己配置的麻烦,还能节省非常多的时间. 我们先把yum源换成国内的阿里云镜像源(当然不换也可以),先备份 ...