广泛出现的模板子程序

287 阅读4分钟

广泛出现的模板子程序

作者:光火

邮箱:victor_b_zhang@163.com

  • 标题中的模板指的并非是 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 为容器间的转换提供了非常便捷的形式,上述代码中我们利用迭代器,很轻松地就完成了 setvector 的创建与赋值

翻转链表

// 递归法
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,当 curnullptr 时,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 << " ";
  • 枚举正整数 i\large{i} 的因数 j\large{j} 时,由于 i\large{i} 同时拥有因数 j\large{j}ij\large{\frac{i}{j}},而两者之间必有一个 i\large{\leq \sqrt{i}},因此只需要在 [1,i]\large{[1,\sqrt{i}]} 范围内枚举 jj 即可

埃拉托斯特尼筛

// 返回 < 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 ForceKaratsuba 快速乘法的性能更好。感兴趣的读者可以自行查阅相关内容,其原理不难理解,过程是标准的分治调用
  • Karatsuba 算法同时包含长整数加法和减法,但是如若要单独实现这两部分功能,其实未必要采用本题的addZeros 方法,因为向字符串首端插入字符进行补齐的操作还是比较暴力的。对于简单的加法和减法,可以考虑直接进行字符串的翻转然后逐位相加

此外,上述代码在实现过程中,用到了一些常见的函数和方法,我们做简要总结:

  • ios::sync_with_stdio(false) 用于提高 cincout 的速度,而 cin.tie(nullptr) 则用于将 cincout 解除绑定
  • 对于 string 而言,+ 运算符就相当于拼接,这没什么好说的。不过,需要注意的就是,str += "a" 的速度要显著快于 str = str + "a",这是因为后者在运算过程中会产生一个新的对象,导致性能受到影响
  • charint 的转换可以通过加减 '0' 来完成,于是我们就看到了这样一幕:result.push_back(tmp + '0')。对于 strMinus 函数而言,由于两个 '0' 会在相减过程中抵消,所以直接写成 str_one[len - i - 1] - str_two[len - i - 1] - carrier 即可
  • 翻转字符串时,我们再次使用了 algorithm 库中的 reverse 函数,它可以接收两个迭代器作为参数
  • str.find_first_ofstr.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_offind_last_of 的返回结果不同时,证明字符不唯一
  • str.substr 需要两个参数,分别为 str 中的元素下标和长度,返回值仍为 string
  • atoiC 库的一个函数,用于将字符串转换为整数,其标准定义如下所示。但由于我们的输入均为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)
    
    • onetwo 用于存放经过压缩四位法处理后的数据,len_onelen_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;
}