算法筑基(八):数学算法——程序背后的数理根基

算法筑基(八):数学算法------程序背后的数理根基


📖 前言

数学是计算机科学的基础,而数学算法则是将数学理论转化为高效计算的关键。从密码学到图形学,从数值计算到机器学习,都离不开数学算法的支撑。

本文将从最古老的欧几里得算法 开始,学习如何快速求最大公约数;然后介绍扩展欧几里得算法 ,解决不定方程的整数解;接着学习素数筛法 ,快速生成素数表;再深入快速幂 ,高效计算大数幂模;最后用大数运算 的模拟,突破基本数据类型的限制。每个算法都配有完整的C语言代码逐行注释 以及实际案例 ,帮助你掌握这些程序员的数学利器。最后附上课后练习及答案,巩固所学知识。


📌 本文目录

  1. 欧几里得算法(辗转相除法)

    • 最大公约数
    • 分数化简
  2. 扩展欧几里得算法

    • 求解不定方程
    • 模逆元
  3. 素数筛法

    • 埃拉托色尼筛法
    • 欧拉筛(线性筛)
  4. 快速幂(模幂)

    • 快速幂算法
    • 快速幂取模
  5. 大数运算

    • 大数加法
    • 大数乘法
  6. 数学算法对比总结

  7. 课后练习与答案


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 优化。

✍️ 课后练习与答案

练习题目

  1. 实现欧几里得算法的非递归版本,并测试 gcd(123456789, 987654321)
  2. 使用扩展欧几里得求 47 在模 100 下的逆元(若存在)。
  3. 用欧拉筛生成 1e7 以内的所有素数,并统计个数。
  4. 实现大数减法(考虑负数情况)。
  5. 用快速幂计算 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(实际运行得到)

🌟 寄语

数学算法是程序员的"内功心法",它们往往简单短小,却能解决复杂问题。欧几里得、快速幂这些算法,不仅在实际开发中常用,也是许多高级算法的基础。希望你能熟练掌握它们,并能在需要时灵活运用。

本系列到此,我们已经覆盖了排序、搜索、图论、动态规划、贪心、分治、字符串、数学等八大类核心算法。这些内容足以支撑你应对大部分算法面试和工程挑战。

如果你从头坚持到了这里,恭喜你已经完成了算法筑基!但这只是开始------算法之路无止境,保持练习,不断探索,你会走得更远。

感谢阅读,祝你编程之路越走越宽!


如果你有任何问题或建议,欢迎评论区留言。
如果这个系列对你有帮助,请分享给更多需要的朋友!

相关推荐
查古穆2 小时前
堆-前 K 个高频元素
数据结构·算法·leetcode
啊哦呃咦唔鱼2 小时前
LeetCodehot100-23合并 K 个升序链表
算法
kobesdu2 小时前
laser_line_extraction线段提取开源功能包解读和使用例程
人工智能·算法·机器人·ros
abant22 小时前
leetcode 105 前序中序构建二叉树
算法·leetcode·职场和发展
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 438. 找到字符串中所有字母异位词 | C++ 滑动窗口题解
c++·算法·leetcode
Mr_Xuhhh2 小时前
深入理解Java数组:从定义到高阶应用
开发语言·python·算法
倦王2 小时前
力扣日刷复习:
算法·leetcode·职场和发展
py有趣2 小时前
力扣热门100题之二叉树的中序遍历
算法·leetcode·职场和发展
DFT计算杂谈2 小时前
eDMFT安装教程
java·服务器·前端·python·算法