广泛出现的模板子程序
作者:光火
- 标题中的模板指的并非是
OOP中的template,而是可以开箱即用的经典程序,就比如gcd、翻转链表、高精度算法等。考虑到这些代码有着独特的意义及广泛的应用,本文对它们进行了一定的汇总,方便有需求的读者复习、查阅。
进制转化
void convert(int number, int base) {
if(number <= 0) return;
if(base < 2 || base > 16) return;
stack<char> container;
static char digit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
while(number > 0) {
int remain = (int)(number % base);
container.push(digit[remain]);
number /= base;
}
while(!container.empty()) {
cout << container.top();
container.pop();
}
}
集合取交
// 由于 set 的特性, 返回结果是由小至大排列的
vector<int> interSection(vector<int>& one, vector<int>& two) {
set<int> result;
int length = two.size();
set<int> record(one.begin(), one.end());
for(int i = 0; i < length; i ++)
if(record.find(two[i]) != record.end())
result.insert(two[i]);
return vector<int>(result.begin(), result.end());
}
STL为容器间的转换提供了非常便捷的形式,上述代码中我们利用迭代器,很轻松地就完成了set和vector的创建与赋值
翻转链表
// 递归法
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
ListNode* rhead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return rhead;
}
// 迭代法
ListNode* reverseList(ListNode* head) {
ListNode* cur = head;
ListNode* pre = nullptr;
while(cur != nullptr) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
- 一般涉及到翻转链表的题目,所操纵的都是单向链表,这是因为相较于灵活的双向链表,单向链表由于访问受限,更能考查编程人员的代码功底
- 翻转链表的递归解法触及了递归调用的本质,值得仔细品味。最终返回的
rhead就是新的链表头,它以一种巧妙的方式被保存下来了 - 迭代法的核心则在于
pre,当cur为nullptr时,pre作为前一个结点,刚好是原链表的尾部,即翻转后的表头
翻转字符串
// char* 形式
char str[] = "hello";
strrev(str); // olleh
// string 形似
string str = "hello";
reverse(str.begin(), str.end()); // olleh
- 需要注意的是,
C语言虽然拥有<string.h>这个头文件,但实则没有strng这一类型。它使用char数组来完成同样的工作
最大公因数
// greatest common divisor
int gcd(int x, int y) {
if(x < y) swap(x, y);
return y == 0? x : gcd(y, x % y);
}
gcd算是形式最简单的递归算法之一了,书写起来只需要两行代码,无需特殊记忆,随用随贴即可
最小公倍数
// least common multiple
int lcm(int a, int b) {
return a * b / gcd(a, b);
}
- 求出两数之积,再除以最大公因数,即可得到最小公倍数
分解质因数
int n;
cin >> n;
for(int i = 2; i * i <= n; i ++)
while(n % i == 0) {
n /= i;
cout << i << " ";
}
if(n > 1) cout << n << " ";
- 枚举正整数 的因数 时,由于 同时拥有因数 和 ,而两者之间必有一个 ,因此只需要在 范围内枚举 即可
埃拉托斯特尼筛
// 返回 < n 的质数数量
int countPrimes(int n) {
if(n <= 2) return 0;
vector<bool> prime(n, true);
int count = 0;
for(int i = 2; i < n; i ++) {
if(!prime[i]) continue;
++ count;
if((long long) i * i < n)
for(int j = i * i; j < n; j += i)
prime[j] = false;
}
return count;
}
- 埃拉托斯特尼筛法通过构建表,可以快速找出
< n的全部素数 - 有的教程将第二层
for循环的起始值设置为了j = i + i,但其实自i * i开始就可以了,因为外层循环每进行一次,就将i的倍数(不一定包含i本身)在表中全部剔除
长正整数加法
// 实现一
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
string add(string& a, string& b) {
int i = a.length();
int j = b.length();
int n = max(i, j), k = n;
vector<int> res(n + 1, 0);
-- i, -- j;
// res 高位在左, 低位在右
// 处理两数共有的低位部分
while(i >= 0 && j >= 0)
res[k --] = (a[i --] - '0') + (b[j --] - '0');
// 处理余下的高位部分
while(i >= 0) res[k --] = a[i --] - '0';
while(j >= 0) res[k --] = b[j --] - '0';
// 用于存储最终答案
string c = "";
for(int i = n; i > 0; i --) {
if(res[i] >= 10) {
res[i] -= 10;
++ res[i - 1];
}
c.push_back(res[i] + '0');
}
if(res[0] > 0) c.push_back('1');
reverse(c.begin(), c.end());
return c;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
string a = "";
string b = "";
cin >> a >> b;
cout << add(a, b) << '\n';
return 0;
}
- 上述方法统一处理进位,但需要开辟较多的临时空间,实现二则恰好与之相反
// 实现二
string add(string& a, string& b) {
string c = "";
int carrier = 0;
int pos_a = a.length() - 1;
int pos_b = b.length() - 1;
while(pos_a >= 0 && pos_b >= 0) {
int t = a[pos_a --] - '0' +
b[pos_b --] - '0' + carrier;
carrier = 0;
if(t > 9) carrier = 1, t -= 10;
c.push_back(t + '0');
}
while(pos_a >= 0) {
int t = a[pos_a --] - '0' + carrier;
carrier = 0;
if(t > 9) carrier = 1, t -= 10;
c.push_back(t + '0');
}
while(pos_b >= 0) {
int t = a[pos_b --] - '0' + carrier;
carrier = 0;
if(t > 9) carrier = 1, t -= 10;
c.push_back(t + '0');
}
if(carrier > 0) c.push_back('1');
reverse(c.begin(), c.end());
return c;
}
长正整数乘法
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
int main() {
string s1, s2;
cin >> s1 >> s2;
int l1 = s1.length();
int l2 = s2.length();
vector<int> a(l1, 0);
vector<int> b(l2, 0);
vector<int> c(l1 + l2 + 1, 0);
for(int i = 0; i < l1; i ++)
a[i] = s1[l1 - i - 1] - '0';
for(int i = 0; i < l2; i ++)
b[i] = s2[l2 - i - 1] - '0';
for(int i = 0; i < l1; i ++)
for(int j = 0; j < l2; j ++)
c[i + j] += a[i] * b[j];
int l = l1 + l2;
for(int i = 1; i <= l; i ++) {
c[i] += c[i - 1] / 10;
c[i - 1] %= 10;
}
while(c[l] == 0 && l > 0) l --;
for(int i = l; i >= 0; i --) cout << c[i];
return 0;
}
- 与长正整数加法的实现一类似,统一处理进位
Karatsuba 乘法
#include <string>
#include <iostream>
#include <algorithm>
using std::cin;
using std::cout;
using std::endl;
using std::string;
void addZeros(string& str_one, string& str_two) {
int len_one = str_one.length();
int len_two = str_two.length();
if(len_one > len_two) {
for(int i = 0; i < len_one - len_two; i ++)
str_two = '0' + str_two;
} else {
for(int i = 0; i < len_two - len_one; i ++)
str_one = '0' + str_one;
}
}
bool compare(const string& one, const string& two) {
int len_one = one.length();
int len_two = two.length();
if(len_one > len_two) return true;
else if(len_one == len_two) {
int position = 0;
while(position < len_one) {
if(one[position] > two[position])
return true;
else if(one[position] < two[position])
return false;
else position ++;
}
} else return false;
}
string strPlus(string str_one, string str_two) {
int carrier = 0;
string result = "";
addZeros(str_one, str_two);
int len = str_one.length();
for(int i = 0; i < len; i ++) {
int tmp = str_one[len - i - 1] - '0' +
str_two[len - i - 1] - '0' +
carrier;
carrier = 0;
if(tmp > 9) carrier = 1, tmp -= 10;
result.push_back(tmp + '0');
}
if(carrier == 1) result.push_back('1');
reverse(result.begin(), result.end());
int pos = result.find_first_not_of('0');
if(pos == result.npos) result = "0";
else result = result.substr(pos, result.size() - pos);
return result;
}
string strMinus(string str_one, string str_two) {
int carrier = 0;
string result = "";
addZeros(str_one, str_two);
int len = str_one.length();
for(int i = 0; i < len; i ++) {
int tmp = str_one[len - i - 1] -
str_two[len - i - 1] -
carrier;
carrier = 0;
if(tmp < 0) carrier = 1, tmp += 10;
result.push_back(tmp + '0');
}
reverse(result.begin(), result.end());
int pos = result.find_first_not_of('0');
if(pos == result.npos) result = "0";
else result = result.substr(pos, result.size() - pos);
return result;
}
string Karatsuba(string str_one, string str_two) {
if(str_one.length() == 1 || str_two.length() == 1)
return std::to_string(atoi(str_one.c_str()) * atoi(str_two.c_str()));
addZeros(str_one, str_two);
int len = str_one.length();
string a= "", b = "", c = "", d = "";
int tot = len / 2 + len % 2;
a = str_one.substr(0, tot);
c = str_two.substr(0, tot);
b = str_one.substr(tot, len / 2);
d = str_two.substr(tot, len / 2);
string ac = Karatsuba(a, c);
string bd = Karatsuba(b, d);
string ac_plus_bd = strPlus(ac, bd);
string ad_plus_bc = Karatsuba(strPlus(a, b), strPlus(c, d));
ad_plus_bc = strMinus(ad_plus_bc, ac_plus_bd);
int small_order = str_one.length() / 2;
int big_order = str_one.length() / 2 * 2;
for(int i = 0; i < big_order; i ++) ac += '0';
for(int i = 0; i < small_order; i ++) ad_plus_bc += '0';
return strPlus(strPlus(ac, ad_plus_bc), bd);
}
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr);
string str_one = "";
string str_two = "";
cin >> str_one >> str_two;
addZeros(str_one, str_two);
cout << Karatsuba(str_one, str_two);
return 0;
}
- 相较于
Brutal Force,Karatsuba快速乘法的性能更好。感兴趣的读者可以自行查阅相关内容,其原理不难理解,过程是标准的分治调用 Karatsuba算法同时包含长整数加法和减法,但是如若要单独实现这两部分功能,其实未必要采用本题的addZeros方法,因为向字符串首端插入字符进行补齐的操作还是比较暴力的。对于简单的加法和减法,可以考虑直接进行字符串的翻转然后逐位相加
此外,上述代码在实现过程中,用到了一些常见的函数和方法,我们做简要总结:
ios::sync_with_stdio(false)用于提高cin与cout的速度,而cin.tie(nullptr)则用于将cin和cout解除绑定- 对于
string而言,+运算符就相当于拼接,这没什么好说的。不过,需要注意的就是,str += "a"的速度要显著快于str = str + "a",这是因为后者在运算过程中会产生一个新的对象,导致性能受到影响 char和int的转换可以通过加减'0'来完成,于是我们就看到了这样一幕:result.push_back(tmp + '0')。对于strMinus函数而言,由于两个'0'会在相减过程中抵消,所以直接写成str_one[len - i - 1] - str_two[len - i - 1] - carrier即可- 翻转字符串时,我们再次使用了
algorithm库中的reverse函数,它可以接收两个迭代器作为参数 str.find_first_of和str.find_last_of返回的是对应子串在母串中的位置,它的参数可以是单个char也可以是一整个字符串,只是后者的工作方式可能与你想象的有些许差别:string str = "abcdefab"; int position = str.find_first_of("hce"); cout << position; // 2- 上述代码中,
hce首个出现在str中的字符是c,所以返回的是c的下标,即2 - 倘若查找失败,函数的返回值为
str.npos,笔者的代码中,if(pos == result.npos) result = "0"就是这样一层含义,即如果result = "000...0",那么就将其赋为"0" - 这两个函数还可以用于判断目标字符的唯一性,当
find_first_of和find_last_of的返回结果不同时,证明字符不唯一
- 上述代码中,
str.substr需要两个参数,分别为str中的元素下标和长度,返回值仍为stringatoi是C库的一个函数,用于将字符串转换为整数,其标准定义如下所示。但由于我们的输入均为string而非char*,因此我们可借助.c_str()函数完成这步转换int atoi(const char *str)
高精度算法
- 高精度算法的实质就是进制转换,它利用数组充当容器,开辟若干空间,并在每个单元内存放固定数目的数据。其中,不压缩法就相当于10进制,而压缩四位法和八位法则分别对应于1万进制和10亿进制
- 不压缩法:主要利用
char开辟数组,每个元素内存放一位数字。为了防止运算时发生溢位,一般采用倒序进行存放 - 压缩四位法:主要利用
int开辟数组,每个元素内存放四位数字。这是因为int总计32位,可以完全表达8位数,而两个4位数相乘,最多只会出现8位 - 压缩八位法:主要利用
long long开辟数组,每个元素内存放九位数组。这是因为long long总计64位,可以完全表示18位数字,而两个9位数相乘,最多出现18位 下面给出利用压缩四位法实现的长正整数加法程序:
- 不压缩法:主要利用
#include <cstring>
#include <iostream>
using namespace std;
int strToNum(const string& str, int num[]) {
int i = 0, j = 0, len = str.size();
for(i = len - 1; i > 2; i -= 4) {
if(i < 3) break;
num[j ++] = (str[i] - '0')
+ (str[i - 1] - '0') * 10
+ (str[i - 2] - '0') * 100
+ (str[i - 3] - '0') * 1000;
}
if(i == 0)
num[j ++] = str[0] - '0';
else if(i == 1)
num[j ++] = (str[0] - '0') * 10 + (str[1] - '0');
else if(i == 2)
num[j ++] = (str[0] - '0') * 100 + (str[1] - '0') * 10 + (str[2] - '0');
while(j > 0 && num[j - 1] == 0) j --;
return j;
}
int add(int one[], int two[], int len_one, int len_two, int result[], int& pos) {
if(len_one == 0 && len_two == 0) return 0;
else if(len_one == 0) return 1;
else if(len_two == 0) return 2;
int i = 0;
int j = len_one > len_two ? len_one : len_two;
while(i < j) {
if(i > len_one - 1)
result[i] += two[i];
else if(i > len_two - 1)
result[i] += one[i];
else
result[i] += one[i] + two[i];
if(result[i] >= 10000) {
result[i + 1] = result[i] / 10000;
result[i] -= 10000;
}
i ++;
}
if(result[i] == 0)
pos = i - 1;
else pos = i;
return -1;
}
int main() {
string one = "987654321";
string two = "17486915352312222311";
int* num_one = new int[one.size() / 4 + 1];
int* num_two = new int[two.size() / 4 + 1];
int len_one = strToNum(one, num_one);
int len_two = strToNum(two, num_two);
int len_result = len_one > len_two? len_one + 1 : len_two + 1;
int* result = new int[len_result];
for(int i = 0; i < len_result; i ++)
result[i] = 0;
int pos = 0;
int rtn = add(num_one, num_two, len_one, len_two, result, pos);
if(rtn == -1) {
for(int i = pos; i >= 0; i --)
cout << result[i];
} else cout << "error" << endl;
delete[] num_one, num_two, result;
return 0;
}
- 关于
add函数的参数说明int add(int one[], int two[], int len_one, int len_two, int result[], int& pos)one、two用于存放经过压缩四位法处理后的数据,len_one、len_two则分别代表两个数组的有效长度,result用于存放最终结果,pos则指明result中开始的位置- 由于高精度算法对于整数的存储是内部正序、整体反序的,所以需要倒序输出结果。举个简单例子,对于正整数
2156897546,它会被存储为7546/5689/21,这样做的目的当然是为了方便运算
二叉树的层次遍历
- 二叉树的前中后序遍历通过递归调用非常容易实现,因此这里主要谈论一下利用队列实现的层次遍历:
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int tot);
}
TreeNode::TreeNode(int tot) : val(tot) {
left = nullptr , right = nullptr;
}
vector<vector<int>> LevelTraversal(TreeNode* root) {
vector<vector<int>> ans;
if(root == nullptr) return ans;
queue<pair<TreeNode* , int>> base;
base.push(make_pair(root , 0));
while(!base.empty()) {
TreeNode* node = base.front().first;
int level = base.front().second;
base.pop();
if(level == ans.size()) ans.push_back(vector<int>());
assert(level < ans.size());
ans[level].push_back(node->val);
if(node->left) base.push(make_pair(node->left , level + 1));
if(node->right) base.push(make_pair(node->right , level + 1));
}
return ans;
}