大整數計算器


需求

編程語言中內置的int類型能表示的范圍是32位或者64位,有時我們需要參與運算的數,可能會遠遠不止32位或64位,一般我們稱這種基本數據類型無法表示的整數為大整數

我們需要實現一個程序,完成大整數的基本運算,如加法、減法、乘法和除法,其中,大整數用字符串來表示。例如:

  • “9999999999999999999999999999” + “-9999999999999999999999999999” = “0”
  • “10000000000000000000000000000” - “1” = “9999999999999999999999999”
  • “222222222222222” * "333333333" = "74074073999999925925926"

實例化

從分析需求開始,我們可以將大整數的計算分為加、減、乘、除,并在此過程中將需求分解為下列測試用例的列表。(除法暫沒包括)

測試用例的列表不僅是對需求進行實例化的產物,同時也是我們開發過程中的路標和燈塔,指引著前進的方向。

加法:

"1" + "2" == "3";
"11" + "222" == "233";
"1" + "99" == "100";
"11" + "99" == "110";

"5" + "-3" == "2";
"5" + "-6" == "-1";
"-123" + "23" == "-100";
"-111" + "-222" == "-333";

減法

"4" - "3" == "1";
"44" - "3" == "41";
"123" - "24" = "99"
"4" - "6" == "-2"
"23" - "100" == "-77"

"45" - "-123" == "168"
"-45" - "123" == "-168"
"-45" - "-123" == "78"

乘法

"4" * "3" == "12"
"11" * "22" == "242"

"11" * "-22" == "-242"
"-11" * "22" == "-242"
"-11" * "-22" == "242"
"0" * "-22" == "0"

實現

首先從加法開始,編寫測試用例。

void test_add()
{ 
    char * result = NULL;

   assert(strcmp(result = add("1","2"), "3") == 0);
   free(result);

   assert(strcmp(result = add("4","2"), "6") == 0);
   free(result);

   assert(strcmp(result = add("11","222"), "233") == 0);
   free(result);

   assert(strcmp(result = add("1","99"), "100") == 0);
   free(result);
}    

按照TDD的三部軍規,應該在每個失敗的用例之后,編寫快速的實現代碼,然后重構。這里限于篇幅,無法全部展現其中的過程,對其中的部分過程進行了快進

char digit_value(const char* str, int pos)
{
    return pos >= strlen(str) ?
            0 : *(str + strlen(str) - pos - 1) - '0';
}

digit_value函數取出一個整數中的某一位,方向從低位到高位。比如digit_value("12345", 0)取出個數的數值5.

unsigned int max_width(const char* left, const char* right)
{
    return strlen(left) > strlen(right) ? strlen(left) : strlen(right);
}

兩個正整數相加,結果最大的位數為較大整數的位數(無進位),或較大的位數加1(有進位).

char * add(const char * left, const char * right)
{
  char *result = NULL;
  int digit_pos = 0;
  unsigned int sum = 0, carry_bit = 0;
  unsigned int len = max_width(left,right);
  unsigned int max_width = len + 1;

  result = (char *)calloc(max_width + 1, sizeof(char));

  while(digit_pos < max_width)
  {
      sum = digit_value(left, digit_pos) 
               + digit_value(right, digit_pos) + carry_bit;
      result[max_width - 1 - digit_pos] = sum % 10 + '0';
      carry_bit = sum / 10;

      digit_pos++;
  }
  if(result[0] == '0')
      memmove(result, result + 1, max_width);
  result[max_width] = '\0';
  return result;

}

add函數為兩個正整數相加的過程。其計算過程與小學生進行多位數的加法類似。將兩數按最低位對齊,逐位相加;如果相加結果大于10,則需要進位。

下一個測試用例?

按照前面擬定的測試用例列表,現在應該實現add("5","-3")

assert(strcmp(result = add("5","-3"), "2") == 0);
free(result);

如何快速實現呢?發現有點難以下手。

經過對正整數和負整數相加的過程進行分析之后,我們發現這個過程其實是一個減法。

A + (-B) = A - B,其中A,B>0

認識到這一點,果斷放棄加法,轉而先實現減法。

實現減法

void test_sub()
{
    char * result = NULL;
    assert(strcmp(result = sub("4","3"), "1") == 0);
    free(result);

    assert(strcmp(result = sub("44","3"), "41") == 0);
    free(result);

    assert(strcmp(result = sub("123","24"), "99") == 0);
    free(result);

}

此處略去三個測試用例的實現過程。

char * sub(const char *left, const char * right)
{
    char * result = NULL;
    int digit_pos = 0;
    unsigned int max_digit = strlen(left);
    int diff = 0, borrow_bit = 0;

    result = (char *)calloc(max_digit+1, sizeof(char));

    while(digit_pos < max_digit)
    {
        diff = digit_value(left, digit_pos) 
                - borrow_bit - digit_value(right, digit_pos);
        borrow_bit = (diff < 0 ? 1 : 0);
        result[max_digit - 1 - digit_pos] = diff + 10 * borrow_bit + '0';
        digit_pos ++;
    }
    for(digit_pos = 0; digit_pos<strlen(result); digit_pos++)
        if(result[digit_pos] != '0') break;
    memmove(result, result+digit_pos, max_digit);

    result[max_digit] = '\0';
    return result;
}

目前實現了正整數的減法,且被減數大于減數。其過程與加法的過程類似,不同之處在于,減法過程中可能需要借位

繼續實現減法,下一個測試用例是被減數小于減數的情況。

sub("5","6") == "-1";

對于小減大的情況,可以轉化為大減小,然后再求負值。即A -B = -(B - A),其中A,B>0

將之前的sub函數重命名為subInternal,新的sub函數邏輯如下所示:

char * sub(const char *left, const char * right)
{
    if (less_than_by_abs(left, right))
        return negate(subInternal(right,left));
    else
        return subInternal(left,right);
}

其中,negate函數對一個整數求負值。

char * negate(char * s)
{
    char *result = calloc(strlen(s)+1+1, sizeof(char));
    memcpy(result+1, s, strlen(s));
    result[0] = '-';
    result[strlen(s)+1] = '\0';
    free(s);
    return result;
}

less_than_by_abs函數判斷兩個正整數的大小。

int less_than_by_abs(const char* left, const char* right) {
    return strlen(left) < strlen(right) ||
            (strlen(left) == strlen(right) && strcmp(left, right) < 0);
}

到這里為止,可以再轉回加法,對上面沒有完成的測試用例進行實現。

assert(strcmp(result = add("5","-3"), "2") == 0);
free(result);

assert(strcmp(result = add("5","-6"), "-1") == 0);
free(result);

assert(strcmp(result = add("-123","23"), "-100") == 0);
free(result);

利用剛才實現的減法,完成加法。

首先將上面實現的add函數重命名為add_internal,然后引入對整數符號的判斷。新的add函數如下所示。

char * add(const char * left, const char * right)
{
    if(is_positive(left) && is_negative(right))  return sub(left, right+1);
    if(is_negative(left) && is_positive(right))  return sub(right,left+1);
    else                                         return add_internal(left, right);
}

其中的邏輯可以描述為:正+負可以轉換為正-正負-正可以轉換為正+正,再取負

int is_negative(const char * number)
{
    return *number == '-';
}
int is_positive(const char * number)
{
    return !is_negative(number);
}
unsigned int greater_than_by_abs(const char * l, const char* r)
{
    return strlen(l) > strlen(r) ||
            (strlen(l) == strlen(r) && strcmp(l, r) > 0);
}

到哪里了?

對于加法,已經實現A+B,A+(-B), (-A)+B,其中A,B>=0。還缺少兩個都是負數的情況。

assert(strcmp(result = add("-111","-222"), "-333") == 0);
free(result);

經過補充和重構之后的add如下:

char * add(const char * left, const char * right)
{
    if(is_positive(left) && is_negative(right))  return sub(left, right+1);
    if(is_negative(left) && is_positive(right))  return sub(right,left+1);
    if(is_negative(left) && is_negative(right))  return negative(add(left+1, right+1));
    else                                         return add_internal(left, right);
}

每一個分支對應著一種符號組合的情況。

到目前為止,加法已經完全實現。

繼續減法

assert(strcmp(result = sub("45","-123"), "168") == 0);
free(result);

對于A-(-B),其中A,B>=0,可以轉化為 A+B

char * sub(const char *left, const char * right)
{
    if(is_positive(left) && is_negative(right))
        return add(left, right+1);
    if (less_than_by_abs(left, right))
        return negative(subInternal(right,left));
    else
      return subInternal(left,right);
}

負減正

對于(-A)-B,其中A,B>=0,可以轉化為 -(A+B)

assert(strcmp(result = sub("-45","123"), "-168") == 0);
free(result);

對應下面sub函數中第二個分支。

char * sub(const char *left, const char * right)
{
    if(is_positive(left) && is_negative(right))
        return add(left, right+1);
    if(is_negative(left) && is_positive(right))
        return negate(add(left+1, right));
    if (less_than_by_abs(left, right))
        return negate(subInternal(right,left));
    else
        return subInternal(left,right);

}

負-負

assert(strcmp(result = sub("-45","-123"), "78") == 0);
free(result);   

對于|A| < |B|的情況,其結果可以轉化為 B - A,也等價于 |B| - |A|.

char * sub(const char *left, const char * right)
{
    if(is_positive(left) && is_negative(right))
        return add(left, right+1);
    if(is_negative(left) && is_positive(right))
        return negate(add(left+1, right));
    if(is_negative(left) && is_negative(right))
        return sub(right+1, left+1);
    if (less_than_by_abs(left, right))
        return negate(subInternal(right,left));
    else
        return subInternal(left,right);
}

經過完善和重構的sub函數如下所示。

char * sub(const char *left, const char * right)
{
    if(is_positive(left) && is_negative(right))    return add(left, right+1);
    if(is_negative(left) && is_positive(right))    return negate( add(left+1, right));
    if(is_negative(left) && is_negative(right))    return sub(right+1, left+1);
    if(is_positive(left) && is_positive(right))
    {
        if (less_than_by_abs(left, right))    return negate( subInternal(right,left));
    }
    return subInternal(left,right);
}

至此加法減法都已完全實現。從上面的實現可以看出,核心的計算邏輯包含在addInternalsubInternal兩個函數中,其余的功能全部在此基礎上通過組合完成。

乘法

void test_mul()
{
    char * result = NULL;

    assert(strcmp(result = mul("4","3"), "12") == 0);
    free(result);
}

一個簡單粗暴的實現。

char * mul(const char *left, const char * right)
{
    int max_width = strlen(left) * strlen(right) + 1;
    char * result = malloc(max_width + 1);

    int a = digit_value(left,0);
    int b = digit_value(right,0);
    int product = a * b;

    if(product > 10)
    {
        result[max_width - 1] = product % 10 + '0';
        result[max_width - 2] = product /10 + '0';
    }
    result[max_width] = '\0';
    return result;
}

重構之后:

char * mul(const char *left, const char * right)
{
    char * acc = add(left,"0");
    char * p;
    int i = 1;

    for(i = 1; i<right[0] - '0'; i++)
    {
        p = add(left, acc);
        free(acc);
        acc = p;
    }  
    return acc;
}

多位數的乘法:

assert(strcmp(result = mul("11","22"), "242") == 0);
free(result);

其計算邏輯與小學生進行乘法計算類似。第二個整數的每一位分別與被乘數相乘,然后將結果左移一位(乘10),再累加到中間結果上。最終的累加值即為最后的乘積。

char * mul(const char *left, const char * right)
{
    int pos = 0;
    char * acc = add("0","0");
    char * p = NULL;
    char * p1 = NULL;
    char * p2 = NULL;

    while(pos < strlen(right))
    {
        p2 = mulByX(acc, 10);
        p1 = mulByX(left, right[pos] - '0');
        p = add(p2, p1);
        free(p1);
        free(p2);
        acc = p;
        pos++;
    }
    return acc;
}

其中的mulByX函數將一個大整數與一個單位正整數進行相乘。

char * mulByX(const char *n, int x)
{
    char * acc = add("0","0");
    char * p;
    int i = 1;

    for(i = 0; i < x; i++)
    {
        p = add(n, acc);
        free(acc);
        acc = p;
    }
    return acc;
}

正×負

其結果為兩個整數相乘,再取負值。

assert(strcmp(result = mul("11","-22"), "-242") == 0);
free(result);

char * mul(const char *left, const char * right)
{
    int pos = 0;
    char * acc = add("0","0");
    char * p = NULL;
    char * p1 = NULL;
    char * p2 = NULL;

    if(is_positive(left) && is_negative(right))
    {
        return negate( mul(left, right+1));
    }
    while(pos < strlen(right))
    {
        p2 = mulByX(acc, 10);
        p1 = mulByX(left, right[pos] - '0');
        p = add(p2, p1);
        free(p1);
        free(p2);
        acc = p;
        pos++;
    }
    return acc;
}

同樣地,將之前的mul重命名為mulInternal,新的mul重構之后如下所示:

char * mul(const char *left, const char * right)
{ 
    if(is_positive(left) && is_negative(right))
    {
        return negative( mul(left, right+1));
    }
    return mulInternal(left, right);
}

負×正

assert(strcmp(result = mul("-11","22"), "-242") == 0);
free(result);

與前面正×負類似。

char * mul(const char *left, const char * right)
{ 
    if(is_positive(left) && is_negative(right))
    {
        return negate( mul(left, right+1));
    }
    if(is_negative(left) && is_positive(right))
    {
        return negate( mul(left+1, right));
    }
    return mulInternal(left, right);
}

負×負

負負相乘等于正。

assert(strcmp(result = mul("-11","-22"), "242") == 0);
free(result);

新的mul函數如下。

char * mul(const char *left, const char * right)
{
    if(is_positive(left) && is_negative(right))
        return negative( mul(left, right+1));
    if(is_negative(left) && is_positive(right))
        return negative( mul(left+1, right));
    if(is_negative(left) && is_negative(right))
        return mul(left+1, right+1);
    return mulInternal(left, right);

}

乘0

與0相乘等于0。

assert(strcmp(result = mul("0","-22"), "0") == 0);
free(result);

int is_zero(const char * n)
{
    return strlen(n) == 1 && n[0] == '0';
}

只要有一個乘數為0,結果就為0,避免無謂的計算。

 char * mul(const char *left, const char * right)
 {
    if(is_zero(left) || is_zero(right)) return add("0", "0");
    if(is_positive(left) && is_negative(right))
        return negative( mul(left, right+1));
    if(is_negative(left) && is_positive(right))
        return negative( mul(left+1, right));
    if(is_negative(left) && is_negative(right))
        return mul(left+1, right+1);
    return mulInternal(left, right);
  }

大功告成

最終的加、減、乘的計算過程如下所示。

char * add(const char * left, const char * right)
{
    if(is_positive(left) && is_negative(right))  return sub(left, right+1);
    if(is_negative(left) && is_positive(right))  return sub(right,left+1);
    if(is_negative(left) && is_negative(right))  return negative(add(left+1, right+1));
    else                                         return add_internal(left, right);
}

char * sub(const char *left, const char * right)
{
    if(is_positive(left) && is_negative(right))    return add(left, right+1);
    if(is_negative(left) && is_positive(right))    return negative( add(left+1, right));
    if(is_negative(left) && is_negative(right))    return sub(right+1, left+1);
    if(is_positive(left) && is_positive(right))
    {
        if (less_than_by_abs(left, right))    return negative( subInternal(right,left));
    }
    return subInternal(left,right);
}

char * mul(const char *left, const char * right)
{
    if(is_zero(left) || is_zero(right))          return add("0", "0");
    if(is_positive(left) && is_negative(right))  return negative( mul(left, right+1));
    if(is_negative(left) && is_positive(right))  return negative( mul(left+1, right));
    if(is_negative(left) && is_negative(right))  return mul(left+1, right+1);
    return mulInternal(left, right);
}

總結

  • 原子與組合。
    在最終的add sub mul函數中,關注于根據操作數不同的符號組合,執行不同的邏輯。而這里的邏輯是業務領域的高層邏輯,不關心算術運算的實現細節。

    需求中加法、減法和乘法的實現細節,只體現在addInternal subInternal mulInternal三個函數中,它們是實現過程中的原子;其它高層的業務邏輯均在其基礎通過組合的方式完成。使得設計具有良好的層次感和靈活性。

  • 測試驅動設計。
    測試用例在實現過程中具有很好的指引作用,通過測試用例的逐步深入,得以發現不同符合組合之間的轉換規則。

  • 領域概念。
    實現過程中提取出來的函數:is_positive,is_negative,is_zero,
    ,negate,less_than_by_abs,greater_than_by_abs,mulByX,digit_value,max_width,均對應著算術運算領域中的相應概念。使得程序具有良好的可理解性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容

  • 《裕語言》速成開發手冊3.0 官方用戶交流:iApp開發交流(1) 239547050iApp開發交流(2) 10...
    葉染柒丶閱讀 27,532評論 5 19
  • 定點小數運算 來自:http://www.eepw.com.cn/article/17893.htm 在DSP世界...
    郝宇峰閱讀 9,239評論 0 2
  • 物流部的蘇曉彤約我今晚去釣魚 本來不太想但還是被硬拉去 蘇曉彤是我老是跑物流那打印郵件認識 全三樓只這臺打印機旁邊...
    蔡不帥閱讀 375評論 0 0
  • 文/Screalling 在這個闔家團圓的中秋佳節,我盡然寫了這么一個略帶涼意的主題,似乎與節日的和諧氣氛有些格格...
    蒼木幽林閱讀 561評論 0 6
  • 主要比較參數: 庫體積,打包項目體積 開發體驗 性能對比 在對比參數前首先分析一下redux和mobx的設計模式,...
    azothaw閱讀 17,502評論 13 18