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进制补码系统。
的补码用来表示其自身。
的补码用来表示 负数
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