Java算法之数论基础
一、最大公约数(Greatest Common Divisor, GCD)
在Java中,我们可以使用欧几里得算法(辗转相除法)来求两个数的最大公约数:
辗转相除法(欧几里得算法)
辗转相除法,也称为欧几里得算法(Euclidean Algorithm),是一种用于计算两个非负整数最大公约数(Greatest Common Divisor, GCD)的经典算法。该算法基于这样一个事实:对于任何两个非零自然数a和b,它们的最大公约数等于a除以b的余数c和b之间的最大公约数。
算法步骤如下:
- 初始化两个整数
a
和b
,其中a
>=b
且b
> 0。 - 计算
a
除以b
的余数,记作r
(即r = a % b
)。 - 如果
r
为0,则b
就是a
和b
的最大公约数。 - 否则,令
a
等于原来的b
,b
等于刚才得到的余数r
,然后回到第二步继续迭代。
下面是Java实现辗转相除法求最大公约数的代码:
public static int gcd(int a, int b) {
// 确保a >= b
if (a < b) {
int temp = a;
a = b;
b = temp;
}
// 辗转相除直到余数为0
while (b != 0) {
int remainder = a % b;
a = b;
b = remainder;
}
// 这时a就是a和b的最大公约数
return a;
}
这个算法的关键在于通过不断替换较大的数为较小数和余数,使得每次迭代中,两个数的差值逐渐缩小,并最终找到它们的最大公约数。由于每次迭代都会使其中一个数至少减小一半,因此该算法具有较高的效率。
除了辗转相除法,还有其他几种算法,但是与辗转相除法相比,效率会稍微差一些,下面列举几种其他算法:
-
更相减损法:
更相减损法是中国古代数学家刘徽提出的求解最大公约数的方法,其原理是连续用较大数减去较小数,直至两数相等,此时的数即为两数的最大公约数。但这种方法在数值相差悬殊的情况下迭代次数可能会较多,不如辗转相除法高效。例如,对于数a和b,先比较a和b的大小,若a>b,则用a-b并将结果代替a;若a<b,则用b-a并将结果代替b。重复上述过程,直到a=b为止。
-
素因数分解法:
将两个数分别进行素因数分解,找出它们共有的素因数,并把相同的素因数各取最小指数次幂相乘,所得积即为两数的最大公约数。这种方法直观易理解,但对于大数可能不如辗转相除法快速有效。相同的素因数各取最小指数次幂相乘,这似乎有些拗口,对它简单解释一下,举个例子
a = 12 = 2 2 × 3 1 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ b = 18 = 2 1 × 3 2 a = 12 = 2² × 3¹ ······ b = 18 = 2¹ × 3² a=12=22×31⋅⋅⋅⋅⋅⋅b=18=21×32
a和b这两个数,有2和3这两个相同的素因数,在a中,2的指数是2,在b中,2的指数是1,那么素因数2的最小指数次幂就是1,3同理也是1。
但是对于找最大公约数这个问题,素数分解法明显麻烦许多,因为光是找共同的素因数,就已经比较麻烦了,所以个人并不推荐用这种方法来求最大公约数。只是提供一个思想。
以上方法各有特点,对于不同的应用场景和需求可以选择最合适的算法。在大多数情况下,特别是中小规模的整数求最大公约数,辗转相除法是最常用且效率最高的。
二、最小公倍数(Least Common Multiple, LCM)
最小公倍数,当看到这个题目时,我们可能最先想到拿较小的那个数加倍,然后判断另一个数能否被整除,这里提供另一个方法
求两个正整数 (a) 和 (b) 的最小公倍数的一种常见方法是利用它们的最大公约数,记作 (gcd(a, b))。根据数学性质,(a) 和 (b) 的最小公倍数可以通过它们的乘积除以它们的最大公约数来得到:
L
C
M
(
a
,
b
)
=
∣
a
×
b
∣
g
c
d
(
a
,
b
)
LCM(a, b) = \frac{|a \times b|}{gcd(a, b)}
LCM(a,b)=gcd(a,b)∣a×b∣
这里的绝对值符号仅是为了确保在 (a) 或 (b) 中有负数时仍能得出正确的结果,但由于最小公倍数定义应用于正整数,所以一般情况下 (a) 和 (b) 都是正数,因此实际上不需要取绝对值。
如果你已经知道 (a) 和 (b) 的最大公约数,可以直接套用上述公式。若不知道最大公约数,可以先用例如欧几里得算法或者其他更高级的算法来求出 (gcd(a, b))。
举个例子,假设要找出 (a = 12) 和 (b = 18) 的最小公倍数:
首先计算它们的最大公约数 (gcd(12, 18))。我们可以直接使用欧几里得算法,计算出他们的最大公约数为6。
接下来,代入上面的公式计算最小公倍数:
L
C
M
(
12
,
18
)
=
12
×
18
6
=
36
LCM(12, 18) = \frac{12 \times 18}{6} = 36
LCM(12,18)=612×18=36
所以,(12) 和 (18) 的最小公倍数是 (36)。
三、快速幂算法
Java快速幂算法是一种高效的算法,用于计算任意数 a
的非负整数指数 n
的次方,即计算 (a^n) 的值。传统方法是通过连续乘法,时间复杂度为 (O(n)),但快速幂算法利用了指数运算法则以及二进制表示,将时间复杂度降低到了 (O(\log_2 n))。
快速幂的基本思想:
- 将指数
n
转换成二进制形式,例如n = b_{m-1}...b_1b_0
,其中 (b_i) 是二进制位。 - 利用指数的二进制表示,将 (a^n) 分解成一系列的平方操作和乘法操作。
- 对于每一位 (b_i),如果它是1,则我们需要将当前的结果与基数
a
相乘;如果是0,则不需要。 - 因为每次都是平方后选择性地乘以基数,所以问题转化为 (a{2k}) 的计算,这可以通过递归或者循环迭代完成。
- 对于每一位 (b_i),如果它是1,则我们需要将当前的结果与基数
以下是一个基本的Java实现示例:
public long power(long base, int exponent) {
// 边界条件
//当指数为0时,返回结果1
if (exponent == 0)
return 1;
//当底数为0时,返回结果0
if (base == 0)
return 0;
//定义初始化结果为1
long result = 1;
//定义一个数记录每次循环后的平方数
long currentBase = base;
//将指数转换为二进制并从最高位开始处理
while (exponent != 0) {
// 进行与运算,如果当前位是1,则进行乘法
if ((exponent & 1) != 0) {
result *= currentBase;
}
//把基数平方,因为此时是二进制的表示,如果为1的话,那么就代表要乘这个数两次
currentBase *= currentBase;
//移动到下一个二进制位
exponent >>= 1;
}
return result;
}
对于需要同时计算 a^n
对某个模数 p
取模的情况,即求 (a^n mod p),可以在每一步乘法后立即对结果取模,避免溢出:
public long powerModulo(long base, int exponent, long mod) {
//边界条件
if (base == 0)
return 0;
if (exponent == 0)
return 1;
//初始化结果
long result = 1;
//基数为底数模上mod
long currentBase = base % mod;
//将指数转换为二进制并从最高位开始处理
while (exponent != 0) {
if ((exponent & 1) != 0) {
//对结果乘上基数,并模上mod
result = (result * currentBase) % mod;
}
currentBase = (currentBase * currentBase) % mod;
exponent >>= 1;
}
return result;
}
这样,无论 n
多大,都可以高效地计算出 a
的 n
次幂及其模运算结果。
对于取模运算的性质,(result * currentBase * currentBase)%mod = result %mod * currentBase % mod * currentBase % mod,所以每次计算都需要对结果取模。
四、矩阵快速幂
矩阵快速幂常用于处理矩阵乘法连乘问题,比如在某些数论和图论问题中:
矩阵快速幂就只是把乘法换成了矩阵乘法,掌握了快速幂运算再去学矩阵快速幂就很简单了,难一些的地方可能就是矩阵的乘法需要掌握。
public static int[][] matrixPower(int[][] matrix, int p) {
int n = matrix.length;
//初始化矩阵为单位矩阵
int[][] res = new int[n][n];
for (int i = 0; i < n; i++) {
res[i][i] = 1;
}
while (p > 0) {
if ((p & 1) != 0) { // 如果p是奇数
res = multiply(res, matrix);
}
matrix = multiply(matrix, matrix); // 把matrix自乘
p >>= 1; // p /= 2 (right shift)
}
return res;
}
// 定义矩阵乘法函数multiply
public static int[][] multiply(int[][] matrixA, int[][] matrixB) {
//n为第一个数组的行数,m为第二个数组的列数,p为第二个数组的行数
int n = matrixA.length, p = matrixB.length, m = matrixB[0].length;
//过第二个数组的行数不等于第一个数组的列数,不能相乘
if (p != matrixA[0].length)
return null;
int[][] ans = new int[n][m];
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) {
for (int k = 0; k < p; k++)
ans[i][j] += (matrixA[i][k] % mod) * (matrixB[k][j] % mod) % mod;
ans[i][j] = ans[i][j] % mod;
}
return ans;
}
五、高斯消元法
高斯消元用于求解线性方程组,不过在数论中可能更多的是用于理论推导和简化问题:
// 假设A是一个n*n的系数矩阵,b是一个n*1的常数列组成的数组,组合起来形成增广矩阵[A|b]
// 下面的代码将求解线性方程组Ax=b
public class GaussElimination {
public static double[] solveLinearEquations(double[][] augmentedMatrix) {
int n = augmentedMatrix.length;
for (int i = 0; i < n; i++) {
// 1. 选择主元(pivot): 通常选当前行中绝对值最大的元素对应的列,若有零元素则尝试交换行
int maxPivotRowIndex = findMaxPivotRowIndex(i, augmentedMatrix, n);
// 交换行(如果当前行的主元不是最大的)
swapRows(i, maxPivotRowIndex, augmentedMatrix);
// 2. 主元归一化:将主元所在的行除以其主元值,使其变为1
double pivot = augmentedMatrix[i][i];
divideRowByItsPivot(i, pivot, augmentedMatrix);
// 3. 消元:逐行消除当前主元下方元素
for (int j = i + 1; j < n; j++) {
double factor = augmentedMatrix[j][i] / augmentedMatrix[i][i];
subtractMultipleFromRow(j, i, factor, augmentedMatrix);
}
}
// 解决得到的上三角矩阵(RREF),从底部向上回代求解
double[] solutions = new double[n];
for (int i = n - 1; i >= 0; i--) {
solutions[i] = augmentedMatrix[i][n] / augmentedMatrix[i][i];
for (int j = i - 1; j >= 0; j--) {
augmentedMatrix[j][n] -= augmentedMatrix[j][i] * solutions[i];
}
}
return solutions;
}
// 辅助方法
private static int findMaxPivotRowIndex(int startRow, double[][] matrix, int n) {
double maxPivot = Math.abs(matrix[startRow][startRow]);
int maxRowIndex = startRow;
for (int i = startRow + 1; i < n; i++) {
if (Math.abs(matrix[i][startRow]) > maxPivot) {
maxPivot = Math.abs(matrix[i][startRow]);
maxRowIndex = i;
}
}
return maxRowIndex;
}
private static void swapRows(int row1, int row2, double[][] matrix) {
double[] tempRow = matrix[row1];
matrix[row1] = matrix[row2];
matrix[row2] = tempRow;
}
private static void divideRowByItsPivot(int rowIndex, double pivot, double[][] matrix) {
for (int j = rowIndex; j < matrix[rowIndex].length; j++) {
matrix[rowIndex][j] /= pivot;
}
}
private static void subtractMultipleFromRow(int targetRow, int sourceRow, double factor, double[][] matrix) {
for (int j = sourceRow; j < matrix[targetRow].length; j++) {
matrix[targetRow][j] -= factor * matrix[sourceRow][j];
}
}
}
六、素数筛法
埃拉托斯特尼筛法(Sieve of Eratosthenes)用于生成一定范围内的素数序列:
原理就是把2的倍数,3的倍数等等都标记一下,那么剩下的就是素数了,代码如下
public class PrimeSieve {
//筛选
public static boolean[] sieveOfEratosthenes(int limit) {
//初始化数组,先把数据都初始化为true
boolean[] isPrime = new boolean[limit + 1];
for (int i = 2; i <= limit; i++) {
isPrime[i] = true;
}
// 基本原理:标记2的倍数(除了2本身),接着标记3的倍数(除了3本身),以此类推...
for (int p = 2; p * p <= limit; p++) {
if (isPrime[p]) { // p是素数
for (int i = p * p; i <= limit; i += p) {
isPrime[i] = false; // 将p的倍数标记为非素数
}
}
}
return isPrime;
}
//筛选素数
public static List<Integer> getPrimesUpTo(int limit) {
boolean[] primes = sieveOfEratosthenes(limit);
List<Integer> primeList = new ArrayList<>();
for (int i = 2; i <= limit; i++) {
if (primes[i]) {
primeList.add(i);
}
}
return primeList;
}
public static void main(String[] args) {
int limit = 100;
List<Integer> primes = getPrimesUpTo(limit);
System.out.println("素数列表(小于或等于 " + limit + "):");
System.out.println(primes);
}
}
七、唯一分解定理
唯一分解定理指出每个大于1的整数都可以唯一地表示为若干个素数的积的形式。在Java中可以编写函数寻找一个数的所有素数因子:
public static class PrimeFactorization {
// 定义一个方法primeFactorize,接收一个整数参数n,用于计算并输出n的质因数分解
public static void primeFactorize(int n) {
// 使用HashMap来存储质因数及其对应的幂次
Map<Integer, Integer> factors = new HashMap<>();
// 遍历从2到n的平方根的所有整数,因为一个合数的最大质因数不会超过其平方根
for (int i = 2; i <= Math.sqrt(n); i++) {
// 当n能被当前整数i整除时,进入循环内部
while (n % i == 0) {
// 在factors映射表中,将质因数i作为键,幂次作为值。如果i已经在映射表中,则增加其幂次(值+1);否则插入新的键值对,幂次初始化为1
factors.put(i, factors.getOrDefault(i, 0) + 1);
// 更新n的值,去除已找到的质因数i的影响
n /= i;
}
}
// 检查是否还有未处理的质因数,即n本身(当n为质数时)
if (n > 1) {
// 将n作为质因数放入映射表中,并将幂次初始化或增加为1
factors.put(n, factors.getOrDefault(n, 0) + 1);
}
// 输出质因数分解结果
System.out.println("质因数分解结果:");
// 遍历映射表中的所有条目(键值对)
for (Map.Entry<Integer, Integer> entry : factors.entrySet()) {
// 获取当前质因数和对应的幂次
int prime = entry.getKey();
int power = entry.getValue();
// 格式化输出质因数和幂次
System.out.printf("%d^%d ", prime, power);
}
}
// 主函数,用于测试primeFactorize方法
public static void main(String[] args) {
// 设置要分解的数,例如120
int numberToFactorize = 120;
// 调用primeFactorize方法进行质因数分解并输出结果
primeFactorize(numberToFactorize);
}
}
**在上述提供的质因数分解代码中,我们并没有显式地检查每个 i
是否为质数。但实际上是隐含了这个性质:**由于我们是从2开始,每次找到一个能够整除 n
的数 i
后,都会立刻更新 n
的值为 n / i
,并且将 i
作为质因数存储到 factors
中。这样做的结果保证了 i
必然是质因数,原因如下:
- 我们从2开始,2是最小的质数。
- 对于每个可能的
i
,只有当n
能被i
整除(即n % i == 0
)时,才会将i
视为一个质因数,并将n
更新为n / i
。这意味着i
必须是n
的一个因数,而且由于我们在找质因数,因此i
必须是一个质数(因为它不能由更小的质数相乘得到)。 - 如果
i
不是质数,那么它可以表示为其他质数的乘积,而这些更小的质数肯定会在之前已经被找出并从n
中消去,所以当轮到i
时,n
不会被i
整除。
总之,虽然代码中没有明确写出判断 i
是否为质数的语句,但在整个分解过程中,实际找到的每个 i
都满足质因数的条件,这是因为分解的过程基于了质数的定义和性质。同时,由于我们仅检查到 Math.sqrt(n)
范围内的数,这也保证了不会遗漏大于 Math.sqrt(n)
的质因数,因为任何大于 Math.sqrt(n)
并且小于等于 n
的合数都必定包含至少一个不大于 Math.sqrt(n)
的质因数。
factors.getOrDefault(i, 0) + 1
这句代码在Java集合框架中,特别是对于 Map
接口的实现类(如 HashMap
)中使用,用来获取与指定键 i
关联的值,如果该键不存在于 Map
中,则返回默认值 0
。
具体来说:
factors.getOrDefault(i, 0)
:这会尝试从factors
映射表中获取键为i
的值。如果i
已经存在于映射表中,就返回对应的值;如果i
不存在于映射表中,则返回默认值0
。+ 1
:接着对获取的结果(无论它是从映射表中得到的实际值还是默认值0
)加1,表示增加质因数i
的幂次。
在质因数分解的过程中,这句代码的目的是统计每个质因数 i
出现的次数。每当我们发现 n
可以被 i
整除时,就认为找到了一次 i
这个质因数,于是就增加它在映射表中的计数(幂次)。如果 i
刚刚被发现,那么它在映射表中的值就是默认值 0
,加1之后就变为 1
,表示出现了1次;如果 i
已经出现过,那么增加的就是之前记录的次数基础上再加1。
八、约数定理
约数定理描述了正整数的约数个数与它的素因数分解之间的关系。在Java中可以实现函数计算一个数的约数个数:
Java中并不直接提供有关约数定理的内置功能或API,但你可以根据约数定理的数学原理编写程序来计算一个数的所有约数数量或列出所有约数。
约数定理(也称为约数个数定理)表明,对于一个正整数 ( n ) 可以写成质因数分解的形式:
n
=
p
1
a
1
×
p
2
a
2
×
⋯
×
p
k
a
k
n = p_1^{a_1} \times p_2^{a_2} \times \cdots \times p_k^{a_k}
n=p1a1×p2a2×⋯×pkak
其中 ( p_1, p_2, …, p_k ) 是不同的质数,而 ( a_1, a_2, …, a_k ) 是正整数。根据约数定理,( n ) 的正约数总数为:
d
(
n
)
=
(
a
1
+
1
)
×
(
a
2
+
1
)
×
⋯
×
(
a
k
+
1
)
d(n) = (a_1 + 1) \times (a_2 + 1) \times \cdots \times (a_k + 1)
d(n)=(a1+1)×(a2+1)×⋯×(ak+1)
下面是一个简单的Java示例,展示了如何利用约数定理计算一个数的约数个数:
import java.util.HashMap;
import java.util.Map;
public class DivisorCount {
public static int countDivisors(int n) {
Map<Integer, Integer> primePowers = new HashMap<>();
// 分解质因数并记录每个质因数的幂次
for (int i = 2; i * i <= n; i++) {
while (n % i == 0) {
primePowers.put(i, primePowers.getOrDefault(i, 0) + 1);
n /= i;
}
}
// 如果n大于1,意味着还有一个未记录的质因数(n自身)
if (n > 1) {
primePowers.put(n, primePowers.getOrDefault(n, 0) + 1);
}
// 计算约数个数
int divisorCount = 1;
for (int power : primePowers.values()) {
divisorCount *= (power + 1);
}
return divisorCount;
}
public static void main(String[] args) {
int number = 120;
System.out.println("Number of divisors of " + number + ": " + countDivisors(number));
}
}
这段代码首先找到给定整数的所有质因数及每个质因数的幂次,然后利用约数定理公式计算约数的数量。请注意,这段代码并没有直接使用“约数定理”的名称,但它体现了约数定理的内容和应用。
九、反素数
反素数是指其真因数个数(不包括1和自身)为素数的正整数。在Java中,可以先确定一个数的所有真因数,再检查其数量是否为素数:
public static boolean isPermutablePrime(int n) {
int divisorsCount = countDivisors(n) - 2; // 减去1和自身
return isPrime(divisorsCount);
}
// isPrime 是前面定义过的判断素数的辅助函数
总结,数论算法在Java编程中有着广泛的应用,理解和熟练掌握这些基础算法对于解决各类问题至关重要。上述代码仅为简要示例,实际编程时请根据具体需求和数据规模加以优化和完善。