【项目】基于链表的大整数实现

基于链表的大整数实现

重要组件

  • 链表结点类:内置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})\)

首先传统大整数乘法是这样的:

对于大整数

\[x = \sum_{i = 0}^{n - 1} a_i \cdot 10^i\quad y = \sum_{i = 0}^{n - 1} b_i \cdot 10^i \]

\[xy = \sum_{i, j}a_ib_j \cdot 10^{i + j} \]

这个复杂度是\(O(n^2)\)的(假设随机访问\(O(1)\)

接着是Karatsuba的拆解方式,将数掰成两块

\[x = x_1 \cdot 10 ^k + x_0\quad y = y_1 \cdot 10 ^k + y_0 \]

直接乘要算四次乘法,Karatsuba的观察是:

\[(x_0 + x_1)(y_0 + y_1) = x_0y_0 + x_0y_1 + x_1y_0 + x_1y_1 \]

并且

\[xy = x_1y_1 \cdot 10^{2k} + (x_1y_0 + x_0y_1) \cdot 10^k + x_0y_0 \]

那么就可以令

\[z_0 = x_0y_0 \\ z_2 = x_1y_1 \\ z_1 = (x_0 + x_1)(y_0 + y_1) \]

从而

\[xy = z_2 \cdot 10^{2k} + (z_1 - z_0 - z_2) \cdot 10^k + z_0 \]

从结果上来看,这样就把四次乘法压到了三次乘法

复杂度分析:

\[T(n) = 3T(\frac n 2) + O(n) \Rightarrow O(n^{\log_2 3}) \]

在此也学习一下复杂度计算,从具象化的观点看一下

将递归过程展开成一棵树

\(k\)层的代价就是

\[3^k O(\frac{n}{2^k}) = O(n\cdot (\frac 3 2)^k) \]

并且这棵树的高度为\(\log_2 n\)

因此总复杂度就是

\[\sum_{k = 0}^{\log_2 n}O(n \cdot (\frac 3 2)^k) \]

求和的话关注最大的一项就行了

\[O(n\cdot (\frac 3 2)^{\log_2 n}) = O(n^{1 + \log_2{\frac 3 2}}) = O(n ^ {\log_2 3}) \]

到此数学上的说明结束,下面先贴代码:

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

posted @ 2025-12-05 22:54  R4y  阅读(11)  评论(0)    收藏  举报