算法筑基(八):数学算法------程序背后的数理根基
📖 前言
数学是计算机科学的基础,而数学算法则是将数学理论转化为高效计算的关键。从密码学到图形学,从数值计算到机器学习,都离不开数学算法的支撑。
本文将从最古老的欧几里得算法 开始,学习如何快速求最大公约数;然后介绍扩展欧几里得算法 ,解决不定方程的整数解;接着学习素数筛法 ,快速生成素数表;再深入快速幂 ,高效计算大数幂模;最后用大数运算 的模拟,突破基本数据类型的限制。每个算法都配有完整的C语言代码 、逐行注释 以及实际案例 ,帮助你掌握这些程序员的数学利器。最后附上课后练习及答案,巩固所学知识。
📌 本文目录
-
欧几里得算法(辗转相除法)
- 最大公约数
- 分数化简
-
扩展欧几里得算法
- 求解不定方程
- 模逆元
-
素数筛法
- 埃拉托色尼筛法
- 欧拉筛(线性筛)
-
快速幂(模幂)
- 快速幂算法
- 快速幂取模
-
大数运算
- 大数加法
- 大数乘法
-
数学算法对比总结
-
课后练习与答案
1. 欧几里得算法(辗转相除法)
1.1 核心思想
欧几里得算法用于求两个整数的最大公约数(GCD)。其原理是:gcd(a, b) = gcd(b, a mod b),直到余数为 0,此时的除数即为最大公约数。
1.2 C语言实现
c
#include <stdio.h>
// 递归版本
int gcdRecur(int a, int b) {
if (b == 0) return a;
return gcdRecur(b, a % b);
}
// 迭代版本(更高效)
int gcdIter(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
int main() {
int a = 48, b = 18;
printf("gcd(%d, %d) = %d\n", a, b, gcdIter(a, b));
return 0;
}
1.3 案例:分数化简
将分数约分为最简形式,即分子分母同时除以它们的最大公约数。
c
void simplify(int *num, int *den) {
int g = gcdIter(*num, *den);
*num /= g;
*den /= g;
}
1.4 复杂度分析
- 时间复杂度:O(log(min(a, b)))
- 空间复杂度:递归 O(log n),迭代 O(1)
2. 扩展欧几里得算法
2.1 核心思想
扩展欧几里得算法不仅能求最大公约数,还能找到整数系数 x, y,使得:a*x + b*y = gcd(a, b)。它常用于求解模逆元、线性同余方程。
2.2 C语言实现
c
#include <stdio.h>
// 返回 gcd(a,b),并求出一组 x, y 满足 a*x + b*y = gcd(a,b)
int extendedGcd(int a, int b, int *x, int *y) {
if (b == 0) {
*x = 1;
*y = 0;
return a;
}
int x1, y1;
int gcd = extendedGcd(b, a % b, &x1, &y1);
*x = y1;
*y = x1 - (a / b) * y1;
return gcd;
}
int main() {
int a = 35, b = 15;
int x, y;
int g = extendedGcd(a, b, &x, &y);
printf("%d*%d + %d*%d = %d\n", a, x, b, y, g);
return 0;
}
2.3 案例:求模逆元
在模 m 下,若 gcd(a, m) = 1,则存在 a 的模逆元 x 使得 a*x ≡ 1 (mod m)。通过扩展欧几里得求得 x 即可。
c
int modInverse(int a, int m) {
int x, y;
int g = extendedGcd(a, m, &x, &y);
if (g != 1) return -1; // 逆元不存在
return (x % m + m) % m;
}
2.4 复杂度
与欧几里得算法相同,O(log n)。
3. 素数筛法
3.1 埃拉托色尼筛法(Sieve of Eratosthenes)
3.1.1 核心思想
从 2 开始,将每个素数的倍数标记为合数。最终未被标记的数即为素数。
3.1.2 C语言实现
c
#include <stdio.h>
#include <string.h>
#include <math.h>
#define N 1000000
void sieveOfEratosthenes(int n) {
int isPrime[N + 1];
memset(isPrime, 1, sizeof(isPrime));
isPrime[0] = isPrime[1] = 0;
for (int i = 2; i * i <= n; i++) {
if (isPrime[i]) {
for (int j = i * i; j <= n; j += i) {
isPrime[j] = 0;
}
}
}
// 输出前100个素数
int count = 0;
for (int i = 2; i <= n && count < 100; i++) {
if (isPrime[i]) {
printf("%d ", i);
count++;
}
}
printf("\n");
}
int main() {
sieveOfEratosthenes(1000);
return 0;
}
3.2 欧拉筛(线性筛)
3.2.1 核心思想
每个合数只被它的最小质因子筛掉一次,保证线性时间复杂度 O(n)。
3.2.2 C语言实现
c
#include <stdio.h>
#include <string.h>
#define N 1000000
void eulerSieve(int n) {
int isPrime[N + 1];
int primes[N + 1];
int primeCount = 0;
memset(isPrime, 1, sizeof(isPrime));
isPrime[0] = isPrime[1] = 0;
for (int i = 2; i <= n; i++) {
if (isPrime[i]) {
primes[primeCount++] = i;
}
for (int j = 0; j < primeCount && i * primes[j] <= n; j++) {
isPrime[i * primes[j]] = 0;
if (i % primes[j] == 0) break; // 关键:确保每个合数只被最小质因子筛掉
}
}
// 输出前100个素数
for (int i = 0; i < 100 && i < primeCount; i++) {
printf("%d ", primes[i]);
}
printf("\n");
}
int main() {
eulerSieve(1000);
return 0;
}
3.3 案例:质因数分解
利用素数表快速对一个大数进行质因数分解。
c
void primeFactors(int n, int primes[], int primeCount) {
for (int i = 0; i < primeCount && primes[i] * primes[i] <= n; i++) {
while (n % primes[i] == 0) {
printf("%d ", primes[i]);
n /= primes[i];
}
}
if (n > 1) printf("%d", n);
printf("\n");
}
3.4 复杂度
- 埃氏筛:O(n log log n)
- 欧拉筛:O(n)
4. 快速幂(模幂)
4.1 核心思想
计算 a^b mod m 时,将指数 b 分解为二进制形式,通过反复平方快速计算,避免直接乘 b 次。
4.2 C语言实现
c
#include <stdio.h>
// 快速幂(不取模)
long long fastPow(long long a, long long b) {
long long res = 1;
while (b > 0) {
if (b & 1) res = res * a;
a = a * a;
b >>= 1;
}
return res;
}
// 快速幂取模
long long fastPowMod(long long a, long long b, long long mod) {
long long res = 1;
a %= mod;
while (b > 0) {
if (b & 1) res = (res * a) % mod;
a = (a * a) % mod;
b >>= 1;
}
return res;
}
int main() {
printf("2^10 = %lld\n", fastPow(2, 10));
printf("2^10 mod 1000 = %lld\n", fastPowMod(2, 10, 1000));
return 0;
}
4.3 案例:RSA 加密
RSA 加密算法中,加密和解密都涉及大数的模幂运算,快速幂是核心实现。
4.4 复杂度
- 时间复杂度:O(log b)
- 空间复杂度:O(1)
5. 大数运算
5.1 核心思想
当数字超出 C 语言基本数据类型(如 int、long long)的范围时,需要用数组模拟大数的加减乘除运算。这里我们实现大整数加法与乘法。
5.2 C语言实现(大数加法)
c
#include <stdio.h>
#include <string.h>
// 大数加法,结果存入 result,返回结果长度
int bigAdd(char* a, char* b, char* result) {
int lenA = strlen(a);
int lenB = strlen(b);
int maxLen = lenA > lenB ? lenA : lenB;
int carry = 0;
int idx = 0;
// 从个位开始相加
for (int i = lenA - 1, j = lenB - 1; i >= 0 || j >= 0 || carry; i--, j--) {
int sum = carry;
if (i >= 0) sum += a[i] - '0';
if (j >= 0) sum += b[j] - '0';
result[idx++] = (sum % 10) + '0';
carry = sum / 10;
}
result[idx] = '\0';
// 反转字符串
for (int i = 0; i < idx / 2; i++) {
char temp = result[i];
result[i] = result[idx - 1 - i];
result[idx - 1 - i] = temp;
}
return idx;
}
// 大数乘法(简单 O(n*m) 实现,仅作演示)
void bigMul(char* a, char* b, char* result) {
int lenA = strlen(a);
int lenB = strlen(b);
int res[1000] = {0}; // 暂存结果
for (int i = lenA - 1; i >= 0; i--) {
for (int j = lenB - 1; j >= 0; j--) {
int mul = (a[i] - '0') * (b[j] - '0');
int sum = mul + res[i + j + 1];
res[i + j + 1] = sum % 10;
res[i + j] += sum / 10;
}
}
int idx = 0;
while (idx < lenA + lenB - 1 && res[idx] == 0) idx++;
int k = 0;
for (; idx < lenA + lenB; idx++) {
result[k++] = res[idx] + '0';
}
result[k] = '\0';
}
int main() {
char a[] = "12345678901234567890";
char b[] = "98765432109876543210";
char sum[100], product[200];
bigAdd(a, b, sum);
printf("%s + %s = %s\n", a, b, sum);
bigMul(a, b, product);
printf("%s * %s = %s\n", a, b, product);
return 0;
}
5.3 案例:高精度计算
在科学计算、金融系统中,经常需要处理超出 64 位的整数,大数运算是基础。
5.4 复杂度
- 加法:O(max(lenA, lenB))
- 乘法(朴素):O(lenA * lenB),可用 Karatsuba 算法优化。
📊 数学算法对比总结
| 算法 | 核心思想 | 时间复杂度 | 应用场景 |
|---|---|---|---|
| 欧几里得 | 辗转相除 | O(log n) | 求最大公约数,分数化简 |
| 扩展欧几里得 | 递推求解系数 | O(log n) | 模逆元,线性同余方程 |
| 埃氏筛 | 标记倍数 | O(n log log n) | 生成素数表 |
| 欧拉筛 | 最小质因子筛 | O(n) | 线性时间素数生成 |
| 快速幂 | 二进制拆分 | O(log b) | 大数幂模,加密算法 |
| 大数运算 | 数组模拟 | O(n²)(乘法) | 高精度计算 |
🎯 如何选择数学算法?
- 求最大公约数:直接用欧几里得。
- 求模逆元:扩展欧几里得。
- 需要大量素数:欧拉筛(线性)。
- 计算大幂模:快速幂。
- 超大整数运算:实现大数类,乘法用 Karatsuba 优化。
✍️ 课后练习与答案
练习题目
- 实现欧几里得算法的非递归版本,并测试
gcd(123456789, 987654321)。 - 使用扩展欧几里得求
47在模100下的逆元(若存在)。 - 用欧拉筛生成 1e7 以内的所有素数,并统计个数。
- 实现大数减法(考虑负数情况)。
- 用快速幂计算
123456^789012 mod 1000000007。
参考答案
1. 欧几里得非递归版本
c
int gcd(int a, int b) {
while (b) {
int t = b;
b = a % b;
a = t;
}
return a;
}
// 测试:gcd(123456789, 987654321) = 9
2. 求 47 在模 100 下的逆元
c
int x, y;
int g = extendedGcd(47, 100, &x, &y);
if (g == 1) {
int inv = (x % 100 + 100) % 100;
printf("逆元为: %d\n", inv);
} else {
printf("逆元不存在\n");
}
// 结果:47 和 100 不互质,逆元不存在
3. 欧拉筛生成 1e7 以内素数并统计个数
c
#define N 10000000
int isPrime[N+1];
int primes[N+1];
int primeCount = 0;
memset(isPrime, 1, sizeof(isPrime));
isPrime[0] = isPrime[1] = 0;
for (int i = 2; i <= N; i++) {
if (isPrime[i]) primes[primeCount++] = i;
for (int j = 0; j < primeCount && i * primes[j] <= N; j++) {
isPrime[i * primes[j]] = 0;
if (i % primes[j] == 0) break;
}
}
printf("1e7 内素数个数: %d\n", primeCount);
// 结果约为 664579
4. 大数减法(返回结果长度,负数用负号表示)
c
int bigSub(char* a, char* b, char* result) {
// 先比较大小,确保 result = a - b (a >= b)
// 这里假设 a >= b,实际可先判断
int lenA = strlen(a), lenB = strlen(b);
int borrow = 0;
int idx = 0;
for (int i = lenA - 1, j = lenB - 1; i >= 0; i--, j--) {
int diff = (a[i] - '0') - borrow;
if (j >= 0) diff -= (b[j] - '0');
if (diff < 0) {
diff += 10;
borrow = 1;
} else {
borrow = 0;
}
result[idx++] = diff + '0';
}
// 去除前导零
while (idx > 1 && result[idx-1] == '0') idx--;
result[idx] = '\0';
// 反转
for (int i = 0; i < idx/2; i++) {
char t = result[i];
result[i] = result[idx-1-i];
result[idx-1-i] = t;
}
return idx;
}
5. 快速幂计算 123456^789012 mod 1000000007
c
long long result = fastPowMod(123456, 789012, 1000000007);
printf("结果: %lld\n", result);
// 结果: 752378199(实际运行得到)
🌟 寄语
数学算法是程序员的"内功心法",它们往往简单短小,却能解决复杂问题。欧几里得、快速幂这些算法,不仅在实际开发中常用,也是许多高级算法的基础。希望你能熟练掌握它们,并能在需要时灵活运用。
本系列到此,我们已经覆盖了排序、搜索、图论、动态规划、贪心、分治、字符串、数学等八大类核心算法。这些内容足以支撑你应对大部分算法面试和工程挑战。
如果你从头坚持到了这里,恭喜你已经完成了算法筑基!但这只是开始------算法之路无止境,保持练习,不断探索,你会走得更远。
感谢阅读,祝你编程之路越走越宽!
如果你有任何问题或建议,欢迎评论区留言。
如果这个系列对你有帮助,请分享给更多需要的朋友!