背景
在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;
}
}
}