C++实现10000以内的正整数的阶乘

背景

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

前面其实已经提到过了,这里可以采用递归法,只是递归在这里并不是最适合的原因在于,它在时间、空间上的消耗过大。这也是副标题为“循环计算”而非“递归计算”的原因所在。这只是一个小小的插曲。言归正传!如何做到“循环计算”并转储结果值?两种可选的方案:

  1. 循环双向链表
  2. 循环单向链表

这里采用的是第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;
        }
    }
}
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值