背景
在Python中,用戶可以直接將一個比較大的數賦值給一個變量,而不會有溢出的風險。舉個例子,
var = 123321456564789000012398778947361548739098473
以上代碼能夠正常解釋執行。但是在C++中就會溢出,這樣一來就給計算一個給定正整數的階乘帶來了困難。衆所周知的是6以內的正整數的階乘,口算就可以算出來了,不幸的是,15的階乘就已經超出的INT_MAX(該宏定義在C標準庫的limits.h和C++標準庫的climits中)了。那麼計算一個給定正整數的階乘就有麻煩了。
思路
根據數學定義n! = n * (n-1) * (n-2) *...* 3 * 2 * 1
,即n! = n * (n-1)!
,且規定0! = 1
。設函數f(n) = n!
,即
f(n) = n * f(n-1)對n > 0成立
。
根據以上函數定義可知這裏存在兩個正整數的乘法運算,且f(n-1)
應該是一個位數較長且很可能會溢出基本整數數據類型的值。因此,字符串與字符串相乘,以得到一個新的結果字符串,應該不失爲一種較容易被接受的方法。
兩個字符串相乘
首先規定:兩個待運算的字符串均爲有效正整數,如"123454356786"、“346”。如何計算兩個字符串的乘積?這裏採用較容易被接受和理解的小學列豎式。
9999
* 999
----------------
89991
89991
89991
----------------
9989001
爲了方便計算機執行以上步驟,不妨把操作數先前後逆置,如"1234"用"4321"來參與運算,即計算"1234"X"2345" ,先計算"4321"X"5432",最後將結果再逆置即可。操作如下所示:
9999
999 *
----------------
19998000<----爲了方便計算,採用前導補0的方式,此處補了0個0
01999800<----此處補了1個0
00199980<----此處補了2個0
----------------
10098990 ----> 1009899 ----> 9989001
根據以上所示,一個最大四位數與一個最大三位數的乘積是一個七位數,因此可以考慮採用七個或八個整型來保存中間值,再進行最後的並列加法運算,計算完成後再刪除後導0並逆置即可。這裏不妨可以考慮採用C++ STL中的vector來保存中間值。但是,在此之前也一定要考慮到vector在擴充容量時,會進行元素拷貝所帶來的時間花費。
函數聲明:
void multiply(const std::string &, const std::string &, std::string *);
函數定義:
void hsc::multiply(const std::string &arg1, const std::string &arg2, std::string *result) {
std::vector<std::vector<int>> vvi;
unsigned long size = arg1.size() + arg2.size() + 1;
for (auto i = 0; i < arg2.size(); ++i) {
int x = 0, y = 0;
std::vector<int> t(size);
for (auto k = 0; k < i; ++k) t[y++] = 0;
for (auto j : arg1) {
int a = (arg2[i] - '0') * (j - '0') + x;
div_t b = div(a, hsc::base);
t[y++] = b.rem;
x = b.quot;
}
if (x != 0) t[y++] = x;
vvi.emplace_back(t);
}
std::ostringstream os;
int x = 0;
for (unsigned long i = 0; i < size; ++i) {
int k = x;
for (auto &j : vvi) k += j[i];
div_t m = div(k, hsc::base);
os << m.rem;
x = m.quot;
}
if (x != 0) os << x;
*result = os.str();
while (true) {
auto e = result->end();
--e;
if (*e == '0') result->erase(e);
else break;
}
}
此函數仍然有可待提升性能的地方。此處定義了一個std::vector<std::vector<int>> vvi;
,使用此變量來保存中間運算值就存在很明顯的問題:當中間值位數比較多的時候,就會產生比較多的資源消耗。因此可以考慮:在計算每一位乘法運算時,把上一次在該位的值補加上去,以此來產生一個新的商…餘數,並將餘數寫入當前位置中,那麼就可以省去大量的時間、空間。其次可以考慮:當每一位乘數遇到0時應該如何處理?已知0 * 任何數 = 0
,因此可以嘗試:遇0略過不計算。改良版如下代碼所示:
函數定義:
void hsc::multiply(const std::string &arg1, const std::string &arg2, std::string *result) {
unsigned long size = arg1.size() + arg2.size() + 1;
std::vector<int> vi(size);
for (auto i = 0; i < arg2.size(); ++i) {
if (arg2[i] == 0) continue;
int x = 0, y = 0;
for (auto k = 0; k < i; ++k) y++;
for (auto j : arg1) {
int a = (arg2[i] - '0') * (j - '0') + x + vi[y];
div_t b = div(a, hsc::base);
vi[y++] = b.rem;
x = b.quot;
}
if (x != 0) vi[y++] = x;
}
std::ostringstream os;
for (auto i : vi) os << i;
*result = os.str();
while (true) {
auto e = result->end();
--e;
if (*e == '0') result->erase(e);
else break;
}
}
正整數轉字符串並逆置
說得簡單一點兒就是,把123456轉成"654321"。看上去蠻簡單的樣子,實際上也確實如此。利用一下小學數學除法——商與餘數。
複習一下:
123456 / 10 = 12345......6
12345 / 10 = 1234......5
1234 / 10 = 123......4
123 / 10 = 12......3
12 / 10 = 1......2
1 / 10 = 0......1
複習完之後就知道,只要除以10取餘數就可以了,直到商爲0 。那麼這裏當然可以使用%法,但是此處,採用的是C標準庫中的div函數。
函數聲明:
std::string int_2_str(int);
函數定義:
std::string hsc::int_2_str(int index) {
std::ostringstream os;
for (auto x = div(index, hsc::base); true; x = div(x.quot, hsc::base)) {
os << x.rem;
if (x.quot == 0) break;
}
return os.str();
}
循環計算
另外一個需要考慮的問題是循環計算。簡單點說就是,當你已經算出10的階乘的時候,就不需要再計算比10小的正整數的階乘了。原因呢?根據前面的數學公式可知,計算階乘可以看成一個遞歸的運算。這裏面有一個限制,在於想要計算f(n)
時,必須先計算f(n-1)
,因爲公式就是這樣子定義的。由此說來,當要計算f(n)
時,f(n-1)
的值已經存在了。
這裏採用C++ STL中的list來保存所有已經計算出結果的階乘值。其中list中的元素類型如下所示:
struct hsc::factorial::list_element {
int index;
std::string value;
list_element(int n, const std::string &fn) : index{n} {
value = fn;
}
};
前面其實已經提到過了,這裏可以採用遞歸法,只是遞歸在這裏並不是最適合的原因在於,它在時間、空間上的消耗過大。這也是副標題爲“循環計算”而非“遞歸計算”的原因所在。這只是一個小小的插曲。言歸正傳!如何做到“循環計算”並轉儲結果值?兩種可選的方案:
- 循環雙向鏈表
- 循環單向鏈表
這裏採用的是第2種方案,因此對此方案做一個小解釋:4個結點,分別設爲p1,p2,p3,p4,其中,此結點的結構如下所示:
struct hsc::factorial::ring_element {
int index;
std::string *p_value;
ring_element *next;
ring_element() : index{0}, p_value{new std::string}, next{nullptr} {}
explicit ring_element(const std::string &value) : index{0}, p_value{new std::string{value}}, next{nullptr} {}
~ring_element() { delete p_value; }
};
由上結點結構可允許:
p1->next = p2;
p2->next = p3;
p3->next = p4;
p4->next = p1;
在最開始的時候,也就是當n = 0
時,f(0) = 1
,也就是說,允許p1 = new ring_element("1");
。指定2個結構指針iter_1、iter_2分別指向p1、p2,當計算出**f(n)**的時候,讓iter_1、iter_2同時指向其當前指向結構結點的next
結點。以此來達到“循環”的目的。
函數聲明:
void calculate(int);
函數定義:
void hsc::factorial::calculate(int n) {
if (n < elements.size()) return;
while (n >= elements.size()) {
iter_2->index = 1 + iter_1->index;
multiply(*iter_1->p_value, iter_2->index);
elements.emplace_back(list_element(iter_2->index, *iter_2->p_value));
iter_1 = iter_1->next;
iter_2 = iter_2->next;
}
}
頭文件
#include <iostream>
#include <list>
#include <sstream>
#include <vector>
名字空間
namespace hsc {
constexpr int base = 10;
}
執行結果
0
1
2
2
4
24
6
720
8
40320
10
3628800
12
479001600
14
87178291200
16
20922789888000
18
6402373705728000
20
2432902008176640000
50
30414093201713378043612608166064768844377641568960512000000000000
52
80658175170943878571660636856403766975289505440883277824000000000000
54
230843697339241380472092742683027581083278564571807941132288000000000000
56
710998587804863451854045647463724949736497978881168458687447040000000000000
58
2350561331282878571829474910515074683828862318181142924420699914240000000000000
60
8320987112741390144276341183223364380754172606361245952449277696409600000000000000
1000
40238726007709377354370243392300398571937486421071463254379991042993851239862902059204420848696940480047998861019719605(此處省略很多位~)
10000
2846259680917054518906413212119868890148051401702799230794179994274411340003764443772990786757784775815884062142317528830(此處省略很多位~)
有待提升
時間、空間上的消耗還是很大的,如何提升速度、減少內存使用量,永遠是一個難題;算法肯定有待提高的,畢竟目前筆者所知、所會用的算法量還是很有限的。
附上源碼
namespace hsc {
constexpr int base = 10;
std::string int_2_str(int);
void multiply(const std::string &, const std::string &, std::string *);
class factorial {
public:
struct list_element;
struct ring_element;
factorial();
~factorial();
void calculate(int);
void obtain(int);
private:
std::list<list_element> elements;
ring_element *p1, *p2, *p3, *p4, *iter_1, *iter_2;
void multiply(const std::string &, int);
};
}
int main() {
hsc::factorial f;
int n;
while (std::cin >> n) {
f.calculate(n);
f.obtain(n);
}
return 0;
}
hsc::factorial::factorial() {
p1 = new ring_element("1");
p2 = new ring_element;
p3 = new ring_element;
p4 = new ring_element;
p1->next = p2;
p2->next = p3;
p3->next = p4;
p4->next = p1;
iter_1 = p1;
iter_2 = p2;
elements.emplace_back(list_element(0, "1"));
}
hsc::factorial::~factorial() {
delete p1;
delete p2;
delete p3;
delete p4;
}
void hsc::factorial::multiply(const std::string &v, int n) {
const std::string n_str{int_2_str(n)};
hsc::multiply(v, n_str, iter_2->p_value);
}
void hsc::factorial::obtain(int n) {
int j = 0;
for (auto &i : elements) {
if (n == j++) {
for (auto k = i.value.crbegin(); k != i.value.crend(); ++k) {
std::cout << *k;
}
std::cout << std::endl;
break;
}
}
}