C++字符串加减法

1. 方案一,两个正整数相加

处理不了负数,仅支持2个正整数相加。

#include <iostream>
#include <string>
#include <cstring>
using namespace std;

string StringAdd2PosNum(string str1, string str2, bool dropOverflow = false, bool keepZero = false) {
	/*
	@param bool dropOverflow, 
	---	为true时,抛弃加法溢出,保证结果位数为较大者的位数。
	--- 为false时,结果若有进位则保留进位。
	@param bool keepZero
	--- 为true时,保留首位的无效0
	*/

	//保证str1的位数较大
	if (str1.size() < str2.size()) {
		std::swap(str1, str2);
	}

	int len1 = str1.size(), len2 = str2.size();
	int d = len1 - len2;

	char* res = (char*)malloc(sizeof(char) * (len1 + 1));
	memset(res, 0, sizeof(char) * (len1 + 1));

	//加法运算
	for (int i = len1 - 1; i >= d; --i) {
		res[i + 1] += str1[i] - '0' + str2[i - d] - '0';
		res[i] += res[i + 1] / 10; //立即进位
		res[i + 1] %= 10; //进位结束
	}

	if(res[0]==1 && !dropOverflow){
		//变量复用节约空间
		len2 = len1 + 2;  // 较大者长度 + 进位 + '\0'
		d = 0; 
	}
	else {
		//抛弃首位的可能溢出1
		len2 = len1 + 1; // 较大者长度+'\0'
		d = 1;
	}

	char* ret = (char*)malloc(sizeof(char) * (len2));
	if (ret == NULL) { return "0"; }
	memset(ret, 0, sizeof(char) * (len2));

	//还原为字符串
	for (int i = d, j = 0; i <= len1; ++i, ++j) {
		ret[j] = res[i] + '0';
	}

	str1 = string(ret);
	free(res);
	free(ret);

	//有可能抛弃进位后,余位出现无效0,左规
	if (!keepZero) {
		int flow = 0;
		while (str1[flow] == '0') { flow++; }
		str1 = str1.substr(flow);
		//有可能抛弃进位后,余位全部是0
		if (str1.empty()) str1 = "0";
	}
	return str1;
}

初稿于19.01.28

2. 方案2,正整数减法

输入要求:仅含负号与0~9的数字。首位不能含有无效0,若需考虑无效0自行添加相应代码即可。

特别注意:该减法方案,支持任意符号的整数字符串输入,返回为为参数1-参数2。输入可以同正同负,也可以一正一负。

思路:先提出符号位,按需比较绝对值大小,按需用绝对值大的减去小的。

2.1 构造真值表

(第三列应该是str1绝对值大于等于str2)

获得主合取范式 ,计算得到

结果符号位res

=(~p∩~q∩~r)V(p∩q∩r)V(p∩~q∩~r)V(p∩~q∩r)
=(~p∩~q∩~r)V(p∩q∩r)V(p∩~q)
=(p==q)&&(q==r) || (p && !q)

当 (p xor q)==1时,结果的绝对值=输入的绝对值相加。

当 (p xor q)==0时,结果的绝对值=输入的绝对值相减。

2.2 减法器代码

#include <iostream>
#include <string>
#include <cstring>
using namespace std;

string StringSub2Num(string& str1, string& str2) {
	bool p = str1[0] == '-' ? 1 : 0;
	bool q = str2[0] == '-' ? 1 : 0;
	bool r, res;

	//绝对值部分
	string s1, s2;
	if (p) { s1 = str1.substr(1); }
	else { s1 = str1; }
	if (q) { s2 = str2.substr(1); }
	else { s2 = str2; }

	if (s1.size() != s2.size()) { r = s1.size() > s2.size() ? 1 : 0; }
	else {
		r = s1 >= s2; //位数相同时,使用c++内置的字符串大于号比较字典序
	}

	//采纳真值表法得到的结果符号位
	res = (p == q) && (q == r) || (p && !q);

	//再根据p xor q判断要加法还是减法。
	//相异则加法,加法函数为part1中已经实现的两个正整数加法。 
	if (p ^ q) {
		s1 = StringAdd2PosNum(s1, s2);

		return res ? '-' + s1 : s1;
	}

	//否则减法 	 
	//保证s1指向绝对值大的字符串 
	if (!r) { std::swap(s1, s2); }
	//绝对值大减小,s1-s2,右对齐
	int tmp, d = s1.size() - s2.size(), l = s1.size();
	for (int i = d; i < l; ++i) {
		tmp = s1[i] - s2[i - d]; //字符相减直接获得差值'9'-'0'=9
		if (tmp >= 0) s1[i] = tmp + '0';
		else {
			//借位
			int k = i - 1;
			while (k >= 0 && s1[k] < '1') { k--; } //寻找能借的位
			s1[k] -= 1;//k位扣1
			while (++k < i) { s1[k] += 9; } // [k+1,i-1] 补9
			s1[i] = tmp + 10 + '0'; //i补10
		}
	}

	//做完减法可能有首位无效0,左规 
	int flow = 0;
	while (s1[flow] == '0')flow++;
	s1 = s1.substr(flow);

	//判空
	if (s1.empty()) { s1 = "0"; }

	//补上结果的符号位,输出 
	return  res ? '-' + s1 : s1;
}

2.3 统一的加法器接口

输入2个字符串,若为2个正整数,则调用方案1。

若为2个负整数,则提取负号,再调用方案1,然后结果补回负号。

若为一正一负,则调用方案2,正整数减法。

string StringAdd2Num(string& str1, string& str2) {
	//统一的加法接口
	bool p = str1[0] == '-' ? 1 : 0;
	bool q = str2[0] == '-' ? 1 : 0;

	string s;
	if (p ^ q) {
		s = p ? str1.substr(1) : str2.substr(1);
		//异号则调用减法 
		if (p) { return StringSub2Num(str2, s); }
		else { return StringSub2Num(str1, s); }
	}
	else if (p) {
		//同号且皆负 
		return '-' + StringAdd2PosNum(str2.substr(1), str1.substr(1));

	}
	else {
		//同号且皆正 
		return StringAdd2PosNum(str1, str2);
	}
}

这个接口稍微有点别扭的地方在于。

我写的StringSub2Num()是支持任意符号的两数相减的。

经过这个接口转换,等于最后仅会用到两个正数相减了。

好像有点白忙活。

--- 更新于 19.09.13

3. 方案3,10进制补码加法器。

采用补码表示的加法器,可统一加法与减法。

(待更...)

(22.06.11更新....万年大坑,正好看到了,补一下吧)

关于补码加法器。

我想了想,任意数字,先转为16进制或者2进制,再用补码做,这个方案应该比较成熟了。

但不太好,因为我们做字符串的大数加减法,就是想避免纯数字部分的运算。

如果要先转进制,就不太优美了。

一来,2进制数的长度会特别特别特别特别长。

二来,如果用16进制,长度固然解决,但这样就失去了,c++中 char 减 char 与 number 减 number 的优美映射关系。(仅限于'0'~'9')

我想和上文一样,保持整个运算在十进制范围内。

那么,

先介绍一下10's complement。 10进制补码。

3.1 约定

对任意K位的10进制补码系统。

[0, 10^K/2-1] 的补码用来表示其自身。

[10^K/2, 10^K-1]的补码用来表示 负数 [-1,-10^K/2]

3.2 举例:

对k=2的10进制补码系统。

[0,49] 表示 正数[0,49]。

[50,99] 表示负数 [-1,-50]。

特别地,'-1' 的补码是 '99' , '-50'的补码是 '50'。

3.3 符号

注意到,K位的10进制补码系统中,负数的起点从补码首位'5'开始。 

因此可以简单地通过判断 首位>'4'? 来判断当前补码代表的数的符号。

3.4 位扩展

正数的补码,前置补'0'。 如 '1'的补码'01',扩充到3位系统,即'001'。

负数的补码,前置补'9'。如'-1'的补码'99',扩充到3位系统,即'999'。

3.5 代码

想进行补码运算,需要先写出

(1)10进制数转补码的函数;

#define _CRT_SECURE_NO_WARNINGS
#include<iostream> 
#include<typeinfo>
#include<cstring>
using namespace std;



string Str2TensComplement(string str, unsigned int len = 0) {
	// @param int len 表示补码系统的位数,为0时表示自适应位数。
	unsigned int minLen;
	bool negative = str[0] == '-'; //符号位
	if (negative || str[0] == '+') { str = str.substr(1); }

	//如果首位大于等于5就需要加位
	minLen = str[0] > '4' ? str.length() + 1 : str.length();
	//校验len取值范围
	len = len < minLen ? minLen : len;

	//长度对齐,前置补0
	if (len > str.length()) {
		str = string(len - str.length(), '0') + str; //len
	}

	if (!negative) {
		return string(str); //正数直接返回位数合法的补码
	}
	else {
		//负数补码为10..0 - 正数补码
		string su = '1' + string(len, '0'); // len+1
		int tmp;
		for (unsigned int i = 1; i < su.length(); i++) {
			tmp = su[i] - str[i - 1]; //字符相减
			if (tmp >= 0) { su[i] = tmp + '0'; }
			else {
				//借位
				int k = i - 1;
				while (k >= 0 && su[k] < '1') { k--; } //寻找能借的位
				su[k] -= 1;//k位扣1
				while (++k < int(i)) { su[k] += 9; } // [k+1,i-1] 补9
				su[i] = tmp + 10 + '0'; //i补10
			}
		}

		//去掉首位
		return su.substr(1); //len
	}
}

(2)补码转10进制数的函数;

string TensComplement2Str(string comp, unsigned int len = 0) {
	string res;
	//考虑溢出情况,此时需要用合法位数调整
	if (len && comp.length() > len) {
		return TensComplement2Str(comp.substr(comp.length() - len), len);
	}

	bool negative = comp[0] > '4';
	//正数的补码是自身,用copy构造防止后续修改污染输入
	if (!negative) {
		res = string(comp);
	}
	else {
		//负数需要重新减法运算
		res = '1' + string(comp.length(), '0');
		int tmp;
		for (unsigned int i = 1; i < res.length(); i++) {
			tmp = res[i] - comp[i - 1]; //字符相减
			if (tmp >= 0) { res[i] = tmp + '0'; }
			else {
				//借位
				int k = i - 1;
				while (k >= 0 && res[k] < '1') { k--; } //寻找能借的位
				res[k] -= 1;//k位扣1
				while (++k < int(i)) { res[k] += 9; } // [k+1,i-1] 补9
				res[i] = tmp + 10 + '0'; //i补10
			}
		}
	}


	//做完减法可能有首位无效0,左规 
	int flow = 0;
	while (res[flow] == '0') { flow++; }
	res = res.substr(flow);

	//补回负号
	return negative ? '-' + res : res;
}

再加一个接口函数

string Add2NumberByComplement(string num1,string num2) {
	string comp1 = Str2TensComplement(num1);
	string comp2 = Str2TensComplement(num2);

	//补码长度对齐
	if (comp1.length() != comp2.length()) {
		//保证comp1指向较长, comp2较短
		if (comp1.length() < comp2.length()) { std::swap(comp1, comp2); }
		//负数补'9',正数补 '0'
		char compChar = comp2[0] > '4' ? '9' : '0';
		comp2 = string(comp1.length() - comp2.length(), compChar) + comp2;
	}

	//对两个补码的运算,视作2个正数相加即可
	//补码系统的运算是保留位数,丢弃溢出值,保留前置0。
	string res = StringAdd2PosNum(comp1, comp2, true, true); //dropOverflow=true, keepZero=true
	res = TensComplement2Str(res);

	//也可以这么写, add时不截取溢出
	//string res = StringAdd2PosNum(comp1, comp2, false, true); //dropOverflow=false, keepZero=true
	//在转化函数里进行有效位数截取
	//res = TensComplement2Str(res,comp1.length());

	return res;
}

3.6 Notion

值得注意的是,我这里的2个“十进制——补码”互转函数的实现,事实上地用到了方案2里的“字符串减法函数”。

所以本质上没有减少代码量。

而我们采用补码器的原意,就是想避开“减法器”,只用一个“加法器”实现对任意正负号数字的运算。

如果追求洁癖的话,可以把这两个互转函数的核心部分,写成按位:

str[i] = '9'- str[i] +'0'; // for( i = 0; i< str.length(); ++i)

再调用方案1的字符串加法:

comp = StringAdd2PosNum(str, "1");

这样一来整个补码器的实现,都只依赖一个 'StringAdd2PosNum'正数加法器函数。

完全看不到“减法器”的影子了。

string Str2TensComplement2(string str, unsigned int len = 0) {
	// @param int len 表示补码系统的位数,为0时表示自适应位数。
	unsigned int minLen;
	bool negative = str[0] == '-'; //符号位
	if (negative || str[0] == '+') { str = str.substr(1); }

	//如果首位大于等于5就需要加位
	minLen = str[0] > '4' ? str.length() + 1 : str.length();
	//校验len取值范围
	len = len < minLen ? minLen : len;

	//长度对齐,前置补0
	if (len > str.length()) {
		str = string(len - str.length(), '0') + str; //len
	}

	if (!negative) {
		return string(str); //正数直接返回位数合法的补码
	}
	else {
		//求负数补码
		for (unsigned int i = 0; i < str.length(); i++) {
			str[i] = '9' - str[i] + '0';
		}
		return StringAdd2PosNum(str, "1", true, true);
	}
}


string TensComplement2Str2(string comp, unsigned int len = 0) {
	string res;
	//考虑溢出情况,此时需要用合法位数调整
	if (len && comp.length() > len) {
		return TensComplement2Str(comp.substr(comp.length() - len), len);
	}

	bool negative = comp[0] > '4';
	//正数的补码是自身
	if (!negative) {
		res = string(comp);
		//可能有首位无效0,左规 
		int flow = 0;
		while (res[flow] == '0') { flow++; }
		res = res.substr(flow);
	}
	else {
		//负数需要重新减法运算
		for (unsigned int i = 0; i < comp.length(); i++) {
			comp[i] = '9' - comp[i] + '0';
		}
		return StringAdd2PosNum(comp, "1", true, false);
	}

	//补回负号
	return negative ? '-' + res : res;
}

3.7 Remark

值得一提的是,方案3并不比方案2有性能优势。

转补码的过程中,最差情况(2负数)做两次O(n)减法转补码,再做一次O(n)补码加法,再O(n)减法转译回来。

如果直接用方案2,只需要提取符号,做一次O(n)加法/减法即可。

我能想到的应用场景,只有大量数据运算时,统一先用补码加法,在最后一步转回10进制数。

中间过程都是以补码形式存储的。

这样可能会比方案2稍有优势。

4. Test Case

-50, -50  -> -100

30 -120 -> -90

0021 0031 -> 52

030 -180 -> -150

//string - C++ Reference

//参考C++ string 实现大整数相加减_xiaozhuaixifu的博客-CSDN博客

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值