【项目】基于链表的大整数实现
基于链表的大整数实现
重要组件
- 链表结点类:内置next指针
- 链表类:需要能够进行插入、删除操作
- 大整数类:需要能够进行四则运算
实现过程
链表结点类
#ifndef NODE_H
#define NODE_H
class Node {
private:
int num;
Node* next;
public:
Node (int VALUE = 0, Node* NEXT = nullptr);
int getNum() const;
Node* getNext() const;
void setNum(int target);
void setNext(Node* targetNode);
Node (const Node&) = delete;
Node& operator = (const Node&) = delete;
};
#endif
以上是Node.h文件的内容,主要过程中注意了以下细节:
- 访问和修改分离:
为了使得接口可控,让访问的时候仅能访问,修改有另外的接口
- 阻止值拷贝:
对于链表结点,假如存在=拷贝,可能会有浅拷贝的问题,到内存管理的时候会更加复杂
因此删除,在后面的实现中不考虑用=对Node赋值
实现就简单了:
#include "Node.h"
Node::Node(int VALUE, Node* NEXT) : num(VALUE), next(NEXT) {}
int Node::getNum() const { return num; }
Node* Node::getNext() const { return next; }
void Node::setNum(int target) {
num = target;
}
void Node::setNext(Node* target) {
next = target;
}
链表类
List.h头文件如下:
#ifndef LIST_H
#define LIST_H
#include <iostream>
#include "Node.h"
class List {
private:
Node* head;
Node* tail;
public:
List ();
List (const List& oth);
List& operator = (const List& oth);
~List();
void clear();
int getSize() const;
void insertBack(int x);
void removeBack();
Node* getHead() const;
Node* getTail() const;
Node* operator [](int index) const;
bool isEmpty() const;
friend std::ostream& operator << (std::ostream& out, const List& list);
};
#endif
首先对于初始化,后续实现过程中需要保证涉及赋值都是深拷贝,这样才能避免对于内存的危险操作
其次是一些基本操作,为了给大整数类做铺垫,只需要在链表末尾添加/删除数据即可
对于一些值的获取,都给了直接的接口,为后面的实现提供方便(但受限于链表本身的性质,[]的重载只能做到O(n)查询,所以性能上真的不如数组直接访问)
最后是给了一个<<运算符的重载辅助调试
接下来可以讨论实现了:
List::List() {
head = new Node(-1);
tail = head;
}
List::List(const List& oth) {
head = new Node(-1);
tail = head;
Node* cur = oth.head->getNext();
while (cur) {
tail->setNext(new Node(cur->getNum()));
tail = tail->getNext();
cur = cur->getNext();
}
}
List& List::operator = (const List& oth) {
if (this != &oth) {
clear();
Node* cur = oth.head->getNext();
Node* currentTail = head;
while (cur) {
currentTail->setNext(new Node(cur->getNum()));
currentTail = currentTail->getNext();
cur = cur->getNext();
}
tail = currentTail;
}
return *this;
}
List::~List() {
Node* cur = head;
while (cur) {
Node* nxt = cur->getNext();
delete cur;
cur = nxt;
}
}
对于赋值的部分,我一开始简单地使用了swap函数,一方面没有深拷贝,在内存管理方面会有危险,另一方面共享指针会导致一些错误的操作。
所以一个个结点去创造赋值或者删除才是更稳定的写法。
void List::clear() {
Node* cur = head->getNext();
while (cur) {
Node* nxt = cur->getNext();
delete cur;
cur = nxt;
}
tail = head;
}
int List::getSize() const {
int siz = 0;
Node* cur = head->getNext();
while (cur) {
siz++;
cur = cur->getNext();
}
return siz;
}
void List::insertBack(int x) {
tail->setNext(new Node(x));
tail = tail->getNext();
}
void List::removeBack() {
if (head == tail) return;
Node *cur = head;
while (cur->getNext() != tail)
cur = cur->getNext();
delete tail;
tail = cur;
tail->setNext(nullptr);
}
实现过程中,我采用了一个哨兵结点(后面甚至还有妙用)。
我保证哨兵结点只会在析构的时候被删除,并且在clear时会保留,这样操作起来更加方便。
tail结点其实是为了insertBack函数而维护的,这样就不用\(O(n)\)操作了。
其实也可以维护tail结点的前驱结点来减少removeBack的复杂度,但是这样增加了维护成本,所以就不采用了。
剩下的都是正常操作:
Node* List::getHead() const {
return head;
}
Node* List::getTail() const {
return tail;
}
Node* List::operator [] (int index) const {
if (index < 0) return nullptr;
Node *cur = head->getNext();
int cnt = 0;
while (cur && cnt < index) {
cur = cur->getNext();
cnt++;
}
return cur;
}
bool List::isEmpty() const {
return head->getNext() == nullptr;
}
std::ostream& operator << (std::ostream& out, const List& list) {
Node* cur = list.head->getNext();
while (cur) {
out << cur->getNum() << " ";
cur = cur->getNext();
}
return out;
}
大整数类
为了操作的方便,我就暂时考虑了非负大整数的运算,以下内容都是基于此假设进行的。
#ifndef BIGINT_H
#define BIGINT_H
#include <iostream>
#include "Node.h"
#include "List.h"
class BigInt {
private:
List data;
public:
BigInt();
BigInt(int num);
BigInt(const std::string& str);
BigInt(const BigInt&) = default;
BigInt& operator = (const BigInt&) = default;
// 由于链表类已经有了完善的深拷贝,因而直接default即可
void setZero(); // 数据归零
void normalize(); // 除去前导零
void toTargetLength(int len); // 结果变量初始化
const List& getData() const;
int getLength() const;
bool isZero() const;
// 基本接口
bool operator < (const BigInt& oth) const;
// 解决小数-大数问题
BigInt operator + (const BigInt& oth);
BigInt operator - (const BigInt& oth);
BigInt operator * (const BigInt& oth);
BigInt operator / (const int& x);
BigInt pow(int x);
friend std::ostream& operator << (std::ostream& out, const BigInt& num);
};
#endif
基本操作都没什么好说的,就直接贴了:
BigInt::BigInt() {
data.insertBack(0);
}
BigInt::BigInt(int num) {
if (num == 0) {
data.insertBack(0);
return;
}
while (num) {
data.insertBack(num % 10);
num /= 10;
}
}
BigInt::BigInt(const std::string& str) {
if (str.empty()) {
data.insertBack(0);
return;
}
for (int i = str.size() - 1; i >= 0; i--) {
if(str[i] >= '0' && str[i] <= '9')
data.insertBack(str[i] - '0');
}
if (data.isEmpty()) {
data.insertBack(0);
}
}
void BigInt::setZero() {
data.clear();
data.insertBack(0);
}
void BigInt::normalize() {
while (data.getTail()->getNum() == 0) {
data.removeBack();
}
if (data.isEmpty()) {
data.insertBack(0);
}
}
void BigInt::toTargetLength(int len) {
int cur = data.getSize();
while (cur < len) {
data.insertBack(0);
cur++;
}
}
const List& BigInt::getData() const {
return data;
}
int BigInt::getLength() const {
return data.getSize();
}
bool BigInt::isZero() const {
return data.getSize() == 1 && data.getHead()->getNext()->getNum() == 0;
}
小于号重载(主要注意保证严格小于):
bool BigInt::operator < (const BigInt& oth) const {
int m = data.getSize(), n = oth.data.getSize();
if (m != n) return m < n;
for (int i = m - 1; i >= 0; i--) {
int x = data[i]->getNum(), y = oth.data[i]->getNum();
if (x == y) continue;
else return x < y;
}
return false;
}
加号重载:
BigInt BigInt::operator + (const BigInt& oth) {
BigInt ret;
int m = data.getSize(), n = oth.data.getSize();
ret.toTargetLength(std::max(m, n) + 1); //增加一位,增加冗余
int siz = ret.getLength();
for (int i = 0; i < siz; i++) {
int L = (i < m) ? data[i]->getNum() : 0;
int R = (i < n) ? oth.data[i]->getNum() : 0;
ret.data[i]->setNum(L + R);
}
bool needCarry;
do {
needCarry = false;
Node* cur = ret.data.getHead()->getNext();
while (cur != nullptr) {
if (cur->getNum() >= 10) {
int carry = cur->getNum() / 10;
int remainder = cur->getNum() % 10;
cur->setNum(remainder);
if (cur->getNext() != nullptr) {
cur->getNext()->setNum(cur->getNext()->getNum() + carry);
} else {
cur->setNext(new Node(carry));
ret.data.getTail()->setNext(cur->getNext());
}
needCarry = true;
}
cur = cur->getNext();
}
} while (needCarry);
ret.normalize();
return ret;
}
其实这次实现改变了我以往的误区。
我一直认为只要一次遍历就可以完成所有进位了,但事实上这并不安全。
更好的操作就是循环地检测是否有进位的需要,直到不需要再进位为止。
BigInt BigInt::operator - (const BigInt& oth) {
BigInt ret, A(*this), B(oth);
if (*this < oth) {
ret.data.getHead()->setNum(-2);
BigInt tmp(A);
A = B;
B = tmp;
} else {
ret.data.getHead()->setNum(-1);
}
int m = A.data.getSize(), n = B.data.getSize();
ret.toTargetLength(std::max(m, n) + 1);
int siz = ret.getLength();
for (int i = 0; i < siz; i++) {
int L = (i < m) ? A.data[i]->getNum() : 0;
int R = (i < n) ? B.data[i]->getNum() : 0;
ret.data[i]->setNum(L - R);
}
bool needBorrow;
do {
needBorrow = false;
Node *cur = ret.data.getHead()->getNext();
while (cur->getNum() < 0) {
if (cur->getNext()) {
cur->setNum(10 + cur->getNum());
cur->getNext()->setNum(cur->getNext()->getNum() - 1);
needBorrow = true;
}
cur = cur->getNext();
}
} while (needBorrow);
ret.normalize();
return ret;
}
一开始思考是否允许小数-大数我是有些犹豫的,因为不知道怎么体现负号。
这时候哨兵结点的作用就出来了,本身哨兵head的值是无用的,这个时候假如给正数和负数不同对应的head值就可以进行区分了,后续输出只需要通过head的值来判断是否需要添加负号。
BigInt BigInt::operator * (const BigInt& oth) {
int m = data.getSize(), n = oth.data.getSize();
BigInt ret;
ret.toTargetLength(m + n + 2);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int ori = ret.data[i + j]->getNum();
int pro = data[i]->getNum() * oth.data[j]->getNum();
ret.data[i + j]->setNum(ori + pro);
}
}
bool needCarry;
do {
needCarry = false;
Node* cur = ret.data.getHead()->getNext();
while (cur != nullptr) {
if (cur->getNum() >= 10) {
int carry = cur->getNum() / 10;
int remainder = cur->getNum() % 10;
cur->setNum(remainder);
if (cur->getNext() != nullptr) {
cur->getNext()->setNum(cur->getNext()->getNum() + carry);
} else {
cur->setNext(new Node(carry));
ret.data.getTail()->setNext(cur->getNext());
}
needCarry = true;
}
cur = cur->getNext();
}
} while (needCarry);
ret.normalize();
return ret;
}
乘法就是模拟了一个竖式计算,复杂度\(O(n^2)\),后续可能会实现一个降低复杂度的做法。
不过实际复杂度并不是\(O(n^2)\),因为链表内部只能做到\(O(n)\)的下标查询。
BigInt BigInt::operator / (const int& x) {
int m = data.getSize();
BigInt ret;
ret.toTargetLength(m);
long long cur = 0, siz = ret.getLength();
for (int i = siz - 1; i >= 0; i--) {
long long ori = data[i]->getNum();
ret.data[i]->setNum((cur * 10 + ori) / x);
cur = (cur * 10 + ori) % x;
}
ret.normalize();
return ret;
}
其实一开始这里出了个看起来比较严重的bug,程序陷入了死循环。
查了之后发现其实是除数x大到一定程度后,cur如果是int的话会溢出,所以改到long long。
接下来是个乘方运算:
BigInt BigInt::pow(int x) {
BigInt ret(1), mul(*this);
while (x) {
if (x & 1) {
ret = ret * mul;
}
mul = mul * mul;
x >>= 1;
}
return ret;
}
这就是个算法常用的快速幂了。
首先,因为是直接算的乘方,暴力算的话是\(O(x)\)复杂度,完全可降。
将x拆成二进制数,让乘数不断作为\(a^{2^k}\),可以降到\(O(\log n)\)。
最后是<<的重载,我了解到这个做法的时候也是很高兴了。
因为大整数的输出和操作事实上是逆序的,而如果手工逆序的话就会需要空间和反转函数,我觉得这个做法不是很直观。
然后我想到其实竞赛中,数的快读快写有一个递归的做法,类似下面的实现:
void write(int x) {
if (x > 9) write(x / 10);
cout << x % 10;
}
但是前提是需要有一个函数名,但是重载函数本身是没有名字的。
那么重载调用一个外置函数呢?涉及到变量的共享,感觉不太美观。
于是我了解到C++的经典语法糖:lambda表达式,但是具名,于是就有了如下实现:
std::ostream& operator << (std::ostream& out, const BigInt& num) {
if (num.getData().getHead()->getNum() == -2) out << "-";
//负号的判断
std::function<void(Node*)> printReverse = [&](Node* node) {
if (node == nullptr) return;
printReverse(node->getNext());
out << node->getNum();
};
printReverse(num.getData().getHead()->getNext());
return out;
}
给输出的递归函数一个名字的同时,又能使用内部变量,写完我也感觉很美观。
关键组件的实现就到此为止,接下来是一些基于此大整数类的探索。
进一步探索
控制台界面
先把接口和实现放出来
#ifndef BIGINTCALCULATOR_H
#define BIGINTCALCULATOR_H
#include <string>
#include "BigInt.h"
class BigIntCalculator {
public:
void run();
private:
void clearScreen();
void showHeader();
void showMainMenu();
void flushInput();
BigInt getInput(const std::string& prompt);
int getMenuChoice();
void handleAdd();
void handleSub();
void handleMul();
void handleDiv();
void handlePow();
};
#endif
#include <iostream>
#include <string>
#include <limits>
#include "BigInt.h"
#include "BigIntCalculator.h"
void BigIntCalculator::clearScreen() {
system("cls");
}
void BigIntCalculator::showHeader() {
std::cout << "==========================================\n";
std::cout << " BigInt 计算器\n";
std::cout << "==========================================\n";
}
void BigIntCalculator::showMainMenu() {
std::cout << "请选择操作(0 - 5):\n";
std::cout << "1. 两数相加\n";
std::cout << "2. 两数相减\n";
std::cout << "3. 两数相乘\n";
std::cout << "4. 两数相除\n";
std::cout << "5. 大数乘方\n";
std::cout << "0. 退出程序\n";
}
void BigIntCalculator::flushInput() {
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
int BigIntCalculator::getMenuChoice() {
int choice;
std::cout << "\n请输入你的选择:";
while (!(std::cin >> choice)) {
std::cout << "输入无效,请输入数字:";
flushInput();
}
flushInput();
return choice;
}
BigInt BigIntCalculator::getInput(const std::string& prompt) {
std::string s;
while (true) {
std::cout << prompt;
if (!std::getline(std::cin, s)) {
std::cout << "输入失败,程序退出。\n";
exit(0);
}
if (s.empty()) {
std::cout << "输入不能为空,请重新输入。\n";
continue;
}
bool ok = true;
for (size_t i = 0; i < s.size(); i++) {
char c = s[i];
if (i == 0 && (c == '+' || c == '-')) continue;
if (!isdigit((unsigned char)c)) { ok = false; break; }
}
if (!ok) {
std::cout << "请输入合法整数。\n";
continue;
}
return BigInt(s);
}
}
void BigIntCalculator::handleAdd() {
clearScreen();
showHeader();
BigInt a = getInput("请输入第一个大数:");
BigInt b = getInput("请输入第二个大数:");
BigInt c = a + b;
std::cout << "\n结果:\n" << a << " + " << b << " = " << c << "\n";
}
void BigIntCalculator::handleSub() {
clearScreen();
showHeader();
BigInt a = getInput("请输入被减数:");
BigInt b = getInput("请输入减数:");
BigInt c = a - b;
std::cout << "\n结果:\n" << a << " - " << b << " = " << c << "\n";
}
void BigIntCalculator::handleMul() {
clearScreen();
showHeader();
BigInt a = getInput("请输入第一个大数:");
BigInt b = getInput("请输入第二个大数:");
BigInt c = a * b;
std::cout << "\n结果:\n" << a << " × " << b << " = " << c << "\n";
}
void BigIntCalculator::handleDiv() {
clearScreen();
showHeader();
BigInt a = getInput("请输入被除数:");
std::cout << "请输入整数除数:";
int divisor;
while (!(std::cin >> divisor)) {
std::cout << "请输入整数:";
flushInput();
}
flushInput();
if (divisor == 0) {
std::cout << "错误:除数不能为 0。\n";
return;
}
BigInt c = a / divisor;
std::cout << "\n结果:\n" << a << " ÷ " << divisor << " = " << c << "\n";
}
void BigIntCalculator::handlePow() {
clearScreen();
showHeader();
BigInt base = getInput("请输入底数:");
std::cout << "请输入非负整数指数:";
int exp;
while (!(std::cin >> exp)) {
std::cout << "请输入整数:";
flushInput();
}
flushInput();
if (exp < 0) {
std::cout << "暂不支持负指数。\n";
return;
}
BigInt c = base.pow(exp);
std::cout << "\n结果:\n" << base << "^" << exp << " = " << c << "\n";
}
void BigIntCalculator::run() {
while (true) {
clearScreen();
showHeader();
showMainMenu();
int choice = getMenuChoice();
switch (choice) {
case 1: handleAdd(); break;
case 2: handleSub(); break;
case 3: handleMul(); break;
case 4: handleDiv(); break;
case 5: handlePow(); break;
case 0:
std::cout << "程序已退出。\n";
return;
default:
std::cout << "无效选择。\n";
break;
}
std::cout << "\n按回车返回菜单...";
std::cin.get();
}
}
这里面有一个函数值得细讲
void BigIntCalculator::flushInput() {
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
对于一个输入的操作,如果输入时正确的,那就是:
123\n
这时候'\n'会留在缓冲区,就需要把缓冲区清空再进行输入
如果是各种非法的输入:
abc\n
会将"abc\n"全部清除,等待后续的操作
乘法的优化
本来想做个傅里叶变换的优化的,但是对于链表来说随机访问拆解过于麻烦,所以不做了
但是我又得知了一个Karatsuba算法,复杂度约等于\(O(n^{1.58})\)
首先传统大整数乘法是这样的:
对于大整数
这个复杂度是\(O(n^2)\)的(假设随机访问\(O(1)\))
接着是Karatsuba的拆解方式,将数掰成两块
直接乘要算四次乘法,Karatsuba的观察是:
并且
那么就可以令
从而
从结果上来看,这样就把四次乘法压到了三次乘法
复杂度分析:
在此也学习一下复杂度计算,从具象化的观点看一下
将递归过程展开成一棵树
第\(k\)层的代价就是
并且这棵树的高度为\(\log_2 n\)
因此总复杂度就是
求和的话关注最大的一项就行了
到此数学上的说明结束,下面先贴代码:
private:
static BigInt karatsuba(const BigInt& a, const BigInt& b);
static BigInt slice(const BigInt& x, int l, int r);
static BigInt shift(const BigInt& x, int k);
public:
static BigInt multiplyKaratsuba(const BigInt& a, const BigInt& b);
静态的用意是,可以发现这个multiplyKaratsuba的调用是不绑定对象的,需要用BigInt::multiplyKaratsuba调用,因而需要静态化
这个事情也说明了,在未声明的前提下,一个成员函数其实是这样的:
T func(T* this, ...) {}
自带一个*this对象
而static在这个语境下代表这个函数并不依赖于对象状态
实现如下:
BigInt BigInt::slice(const BigInt& x, int l, int r) {
BigInt ret;
ret.data.clear();
int n = x.getLength();
for (int i = l; i < r && i < n; i++) {
ret.data.insertBack(x.data[i]->getNum());
}
if (ret.data.isEmpty()) {
ret.data.insertBack(0);
}
ret.normalize();
return ret;
}
BigInt BigInt::shift(const BigInt& x, int k) {
if (x.isZero()) return x;
BigInt ret;
ret.data.clear();
for (int i = 0; i < k; i++) {
ret.data.insertBack(0);
}
Node* cur = x.data.getHead()->getNext();
while (cur) {
ret.data.insertBack(cur->getNum());
cur = cur->getNext();
}
return ret;
}
BigInt BigInt::karatsuba(const BigInt& a, const BigInt& b) {
int n = std::max(a.getLength(), b.getLength());
if (n <= 32) return a * b;
int k = n / 2;
BigInt a0 = slice(a, 0, k);
BigInt a1 = slice(a, k, n);
BigInt b0 = slice(b, 0, k);
BigInt b1 = slice(b, k, n);
BigInt z0 = karatsuba(a0, b0);
BigInt z2 = karatsuba(a1, b1);
BigInt z1 = karatsuba(a0 + a1, b0 + b1);
BigInt mid = z1 - z2 - z0;
return shift(z2, 2 * k) + shift(mid, k) + z0;
}
BigInt BigInt::multiplyKaratsuba(const BigInt& a, const BigInt& b) {
if (a.isZero() || b.isZero()) return BigInt(0);
return karatsuba(a, b);
}
另外在实现过程中有另一个修正,需要把四则运算转化为常成员函数:
BigInt operator + (const BigInt& oth) const;
BigInt operator - (const BigInt& oth) const;
BigInt operator * (const BigInt& oth) const;
BigInt operator / (const int& x) const;
还是拆解开的问题,其实不加const的话表示下来是这样的:
BigInt operator * (BigInt* this, const BigInt& oth);
而常成员只能调用常成员函数,因而要在末尾加上const

浙公网安备 33010602011771号