模拟算法就是按照题目的要求或者情境的设定完整实现其描述过程的方法。
Tips:它虽然在思维上没有太高的要求,但是对于一些复杂的过程,选择实现的方法从而形成一段代码简洁而运行高效的算法,很考验程序员的基本功和经验积累。
ISBN 号码问题:
每一本正式出版的图书都有一个ISBN号码与之对应,ISBN码包括9位数字、1位识别码和3位分隔符,其规定格式如“x-xxx-xxxxx-x”,其中符号“-”是分隔符(键盘上的减号),最后一位是识别码,例如0-670-82162-4就是一个标准的ISBN码。ISBN码的首位数字表示书籍的出版语言,例如0代表英语;第一个分隔符“-”之后的三位数字代表出版社,例如670代表维京出版社;第二个分隔之后的五位数字代表该书在出版社的编号;最后一位为识别码。
识别码的计算方法如下:首位数字乘以1加上次位数字乘以2……以此类推,用所得的结果mod 11,所得的余数即为识别码,如果余数为10,则识别码为大写字母X。例如ISBN号码0-670-82162-4中的识别码4是这样得到的:对067082162这9个数字,从左至右,分别乘以1,2,…,9,再求和,即0×1+6×2+……+2×9=158,然后取158 mod 11的结果4作为识别码。
编写程序判断输入的ISBN号码中识别码是否正确,如果正确,则仅输出“Right”;如果错误,则输出是正确的ISBN号码。
完整代码:
#include
using namespace std;
char str[20];
int main() {
scanf("%s", str);
int idx = 0;
int tmp = 0; // 识别码
int len = strlen(str);
for (int i = 0; i < len - 1; ++i)
if ('0' <= str[i] && str[i] <= '9') {
++idx;
// 在对字符数组中的数字字符做乘法运算时不要忘记减’0’转换为阿拉伯数字再进行计算;
tmp = (tmp + (str[i] - '0') * idx) % 11;
}
char c = tmp == 10 ? 'X' : tmp + '0'; // 计算识别码
if (str[len - 1] == c)
printf("Right");
else {
str[len - 1] = c;
printf("%s", str);
}
return 0;
}
高精度运算
高精度整数可以由数位数组和长度两部分组成。数位数组存储整数时使用的是小端序。回顾一下小端序的存储方式:数字的低位在地址的低位。
使用小端序的理由:
因为加法、减法及后面介绍的乘法等,都是从低位算到高位。这样存储符合我们平时习惯的枚举顺序。
因为数位计算结束后,需要更新数位数组的长度。把高位放在数组后面比较方便数组伸缩。
高精度整数使用字符串输入。由于存储顺序和打印顺序不一致,所以输入输出时都需要翻转操作。
高精度加法、减法和乘法都可分为数位操作和维护长度两部分。维护长度时,注意不要将长度减小到0。
以高精度减法为例:
数位操作:
根据减法竖式计算的规则:从低位开始,逐位相减,若该位不够减,需向下一位借位,并且借一当十。
上面的例子中,可以发现,第i位的结果等于被减数第i位减去减数第i位和低位的借位。
维护长度在两个数相减时,若我们另初始长度为被减数的长度。
可以想象,最终计算的差的长度,和被减数相比,很可能变小。
代码实现
这里,我们总假设被减数大于等于减数。
下面的代码依旧分成数位操作、维护长度和输出三个部分。
#include
#define N 110
using namespace std;
// 同样,这里采用小端序存储
int a_digits[N] = {8, 6, 3, 2}, a_len = 4;
int b_digits[N] = {7, 9, 9}, b_len = 3;
// 计算2368-799
int ans_digits[N], ans_len;
int main() {
// 1. 数位操作
// 我们依旧是从低位到高位开始逐位相减
// 因为我们总假设a>=b,所以初始长度先设为a的长度
// 考虑每一位,需要计算的部分是被减数的当前位,减去减数的当前位,再减去低位的借位
// 如果上一步的计算得出当前位<0,那我们需要向高位借位,然后给当前位+10
ans_len = a_len; // 初始长度
int k = 0; // 维护借位
for (int i = 0; i < ans_len; ++i) {
ans_digits[i] = a_digits[i] - b_digits[i] - k;
if (ans_digits[i] < 0) {
k = 1;
ans_digits[i] += 10;
} else k = 0; // 这里赋值成0很关键,而且容易遗漏
}
// 2. 维护长度
// 想象一下,如果实际数字是1,但是长度记录是4的话,那么输出该数字结果将是0001,
// 也就是出现了“前导0”,所以维护长度的目的是为了去掉前导0
// 所以,我们用while循环实现这样的逻辑:只要最高位是0,我们就把位数缩小1位。
// 但是需要注意,只有位数>1的时候才可以缩小,否则当保存的数字是0时,长度也会减为0.
while (ans_len > 1 && !ans_digits[ans_len - 1]) // 只有长度大于1才可以去掉前导零
--ans_len;
// 3. 输出
for (int i = ans_len - 1; i >= 0; --i) cout << ans_digits[i];
cout << endl;
return 0;
}
算法评价:时间复杂度、空间复杂度
算法运行环境是一个物理的机器,该机器的计算能力和存储空间都是有限的。所以,对于一个算法能不能在有限资源下成功输出结果,我们需要对其时间和空间进行评估。
时间复杂度:
时间复杂度通常是该算法在用最坏情况下的时间复杂度,使用大O记号用来表示估算的结果。
举例:
O(1)——常数条语句:交换两个元素
// 输入
int a = 4;
int b = 6;
// 计算
int t = a;
a = b;
b = t;
O(n)——单重循环求数组和
// 输入
int a[] = {2, 0, 1, 3, 5};
int n = 5;
int sum = 0;
for (int i = 0; i < n; ++i) sum += a[i];
O( n 2 n^2 n2)——双重循环求数组中相等元素对数
// 输入
int a[] = {1, 1, 3, 5, 5};
int n = 5;
// 计算
int cnt = 0;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
cnt += (a[i] == a[j]);
O( 2 n n 2^nn 2nn)——枚举n个数字组成集合的所有子集,输出子集和。
// 输入
int a[] = {2, 1, 3, 6, 5};
int n = 5;
// 计算
int tot = 1 << 5; // 相当于求2的5次方
for (int i = 0; i < tot; ++i) {
// 变量i的二进制形式用于表示每个元素选(1)与不选(0)。
int sum = 0;
for (int j = 0; j < n; ++j)
if ((i >> j) & 1) sum += a[j]; // 检查i的第j位是否是1
cout << sum << endl;
}
空间复杂度:
举例:
// 假设数据规模最大为N
int a; // 常数空间复杂度
int a[N]; // 此时空间复杂度为 O(N)
int a[N][N]; // 此时空间复杂度为 O(N^2)