需求
編程語言中內置的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);
}
至此加法和減法都已完全實現。從上面的實現可以看出,核心的計算邏輯包含在addInternal
和subInternal
兩個函數中,其余的功能全部在此基礎上通過組合完成。
乘法
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
,均對應著算術運算領域中的相應概念。使得程序具有良好的可理解性。