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;
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章