🔢 数学技巧 — 数论与快速幂的魔法

素数筛、快速幂、排序算法——数学与算法的交汇处

📖 数学技巧的核心概念

算法面试中的数学题不同于竞赛数学,主要考察几个核心知识点:素数判定与筛法、快速幂与取模、最大公约数、进制转换。掌握这些模板,大部分数学题可以套路化解。

算法面试数学题分类: 素数类: 素数判定、素数计数、素数筛 (LC 204) 幂运算: 快速幂、大数取模 (LC 372) 排序类: 归并排序、快速排序 (LC 912) GCD/LCM: 辗转相减、欧几里得 (LC 365) 进制类: 进制转换、位表示 (LC 171) 几何类: 叉积、凸包、线段交 (较少考)

1. 埃氏筛法

核心思想:从小到大遍历,如果当前数是素数,则它的所有倍数都不是素数。只需筛到 √n 即可,因为更大的合数一定有 ≤ √n 的素因子。
时间复杂度:O(n log log n),接近线性。

2. 快速幂

核心思想:将指数分解为二进制。例如 a^13 = a^8 × a^4 × a^1(13 = 1101₂)。每次将底数平方,如果当前二进制位为1则乘入结果。
时间复杂度:O(log n),从 O(n) 优化到 O(log n)。

3. Python 数学工具

# 快速幂 (内置)
result = pow(base, exp, mod)  # Python 内置三参数pow

# 最大公约数
import math
math.gcd(12, 8)  # → 4

# 最小公倍数
def lcm(a, b):
    return a * b // math.gcd(a, b)

# 素数判定 (小范围)
def is_prime(n):
    if n < 2: return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0: return False
    return True
Python 的 pow(a, b, mod) 内置了快速幂,三参数版本比 a**b % mod 快得多且不会溢出。面试中直接用内置 pow 即可,但要知道原理。

🎯 题目一:计数质数 (LC 204)

题目描述:统计所有小于非负整数 n 的质数数量。

示例: n = 10 0 1 2 3 4 5 6 7 8 9 ✗ ✗ ✓ ✓ ✗ ✓ ✗ ✓ ✗ ✗ 质数: 2, 3, 5, 7 → 4个 埃氏筛过程: 标记2的倍数: 4,6,8 → 非质数 标记3的倍数: 6,9 → 非质数 (6已标记) √10 ≈ 3, 筛到3即可 剩余: 2,3,5,7

思路分析

埃氏筛:创建布尔数组 is_prime[0..n-1],初始化为 True。从 2 开始,如果 is_prime[i] 为 True,则将 i 的所有倍数标记为 False。优化:从 i*i 开始标记(更小的倍数已经被更小的素数标记过)。

代码实现

class Solution:
    def countPrimes(self, n: int) -> int:
        if n <= 2:
            return 0
        is_prime = [True] * n
        is_prime[0] = is_prime[1] = False
        for i in range(2, int(n ** 0.5) + 1):
            if is_prime[i]:
                # 从 i*i 开始标记(优化)
                for j in range(i * i, n, i):
                    is_prime[j] = False
        return sum(is_prime)

复杂度分析

时间复杂度:O(n log log n)

空间复杂度:O(n)

LC 204 问的是"小于n"的质数,不是"小于等于n"。边界条件容易出错:n=0 和 n=1 时答案是 0,n=2 时答案也是 0(没有小于2的质数)。

🎯 题目二:超级次方 (LC 372)

题目描述:你的任务是计算 a^b 对 1337 取模的结果,其中 b 是一个非常大的正整数,以数组形式给出。

示例: a=2, b=[1,0] → 2^10 mod 1337 b=[1,0] → 表示数字10 关键: b的每一位数字都要处理 a^10 = (a^1)^10 × a^0 一般公式: a^[d₁d₂...dₖ] = (...((a^d₁)^10 × a^d₂)^10 ... × a^dₖ) 每一步: result = (result^10 × a^digit) mod 1337

思路分析

逐位快速幂:从高位到低位处理 b 的每个数字。利用公式:
a^[d₁d₂...dₖ] = ((a^[d₁d₂...dₖ₋₁])^10) × a^dₖ
每一步对 1337 取模,利用模运算性质 (a × b) % m = ((a % m) × (b % m)) % m

代码实现

class Solution:
    def superPow(self, a: int, b: List[int]) -> int:
        MOD = 1337
        result = 1
        a %= MOD
        for digit in b:
            # result^10 * a^digit
            result = (pow(result, 10, MOD) * pow(a, digit, MOD)) % MOD
        return result

复杂度分析

时间复杂度:O(k × log 10) ≈ O(k),k 为 b 的长度

空间复杂度:O(1)

Python 内置 pow(base, exp, mod) 使用快速幂实现,时间 O(log exp)。LC 372 中 digit ∈ [0,9],所以 pow(a, digit, MOD) 只需 O(log 9) ≈ O(1)。pow(result, 10, MOD) 也只需 O(log 10) ≈ O(1)。

🎯 题目三:排序数组 (LC 912)

题目描述:给你一个整数数组 nums,返回按升序排列的数组。不能使用内置排序函数。

排序算法对比: 算法 平均 最坏 空间 稳定 ───────────────────────────────────────────── 冒泡排序 O(n²) O(n²) O(1) ✓ 选择排序 O(n²) O(n²) O(1) ✗ 插入排序 O(n²) O(n²) O(1) ✓ 归并排序 O(nlogn) O(nlogn) O(n) ✓ 快速排序 O(nlogn) O(n²) O(logn) ✗ 堆排序 O(nlogn) O(nlogn) O(1) ✗ 面试推荐: 归并排序(稳定) 或 快速排序(原地)

思路分析

归并排序:分治法。将数组分为两半,分别排序,然后合并两个有序数组。稳定排序,时间复杂度始终 O(n log n),但需要 O(n) 额外空间。
快速排序:选一个 pivot,将数组分为小于和大于 pivot 的两部分,递归排序。平均 O(n log n),最坏 O(n²)(已排序数组+选第一个作pivot),但原地排序。

代码实现(归并排序)

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        def merge_sort(arr):
            if len(arr) <= 1:
                return arr
            mid = len(arr) // 2
            left = merge_sort(arr[:mid])
            right = merge_sort(arr[mid:])
            return merge_two(left, right)
        
        def merge_two(left, right):
            result = []
            i = j = 0
            while i < len(left) and j < len(right):
                if left[i] <= right[j]:
                    result.append(left[i])
                    i += 1
                else:
                    result.append(right[j])
                    j += 1
            result.extend(left[i:])
            result.extend(right[j:])
            return result
        
        return merge_sort(nums)

复杂度分析

时间复杂度:O(n log n) — 每层合并 O(n),共 log n 层

空间复杂度:O(n) — 合并时的临时数组

快速排序实现(补充)

def quick_sort(arr, lo=0, hi=None):
    if hi is None: hi = len(arr) - 1
    if lo >= hi: return
    # 随机选pivot避免最坏情况
    import random
    pivot_idx = random.randint(lo, hi)
    arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx]
    pivot = arr[hi]
    i = lo
    for j in range(lo, hi):
        if arr[j] < pivot:
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
    arr[i], arr[hi] = arr[hi], arr[i]
    quick_sort(arr, lo, i - 1)
    quick_sort(arr, i + 1, hi)
快速排序的最坏情况(O(n²))可以通过随机选 pivot 来避免。面试中如果写快排,务必加上随机化。归并排序没有这个问题,但需要额外空间。

🔬 数学技巧速查表

1. 快速幂模板

def fast_pow(base, exp, mod=None):
    """快速幂: O(log exp)"""
    result = 1
    base = base % mod if mod else base
    while exp > 0:
        if exp & 1:  # 最低位为1
            result = result * base
            if mod: result %= mod
        base = base * base
        if mod: base %= mod
        exp >>= 1
    return result

2. GCD 与 LCM

import math
math.gcd(12, 8)   # 4
math.lcm(4, 6)    # 12 (Python 3.9+)

# 手写辗转相除
def gcd(a, b):
    return a if b == 0 else gcd(b, a % b)

def lcm(a, b):
    return a // gcd(a, b) * b  # 先除后乘防爆

3. 模运算性质

# 加法取模
(a + b) % m = ((a % m) + (b % m)) % m
# 乘法取模
(a * b) % m = ((a % m) * (b % m)) % m
# 减法取模(注意负数)
(a - b) % m = ((a % m) - (b % m) + m) % m
# 幂取模 → 用快速幂
pow(a, b, m)  # Python 内置
面试数学题的常见陷阱:(1) 整数溢出→用取模或Python大整数;(2) 减法取模可能得负数→加m再取模;(3) 除法不能直接取模→需要乘法逆元或费马小定理。
成就解锁:数论魔法师 — 掌握埃氏筛、快速幂、归并排序三大核心技巧
LeetCode AC验证:LC 204 Count Primes ✅ | LC 372 Super Pow ✅ | LC 912 Sort an Array ✅

📝 课后练习

  1. LC 365 水壶问题(GCD应用)
  2. LC 171 Excel表列序号(进制转换)
  3. LC 50 Pow(x, n)(快速幂实现)
  4. LC 1071 字符串的最大公因子(GCD+字符串)
  5. LC 2183 统计可以被K整除的下标对数(数学+哈希)

🔑 本课要点回顾

📚 扩展阅读

思考题:如何用 O(n) 的时间筛出 1 到 n 的所有素数?(提示:欧拉筛/线性筛,确保每个合数只被其最小素因子标记一次。)