Java刷算法题技巧笔记
一个基本事实是,对于 C/C++ 程序,运行时间为 1 秒对应的常数指令操作量大约是 1 0 8 10^8 108,而对于 Java、Python、Go 等其他语言,运行时间为 1 秒对应的常数指令操作量大约是 1 0 7 10^7 107。这个数量级在不同的测试平台和 CPU 上基本保持不变。
1. 数据范围的隐含信息
在刷算法题时,题目中提供的数据范围往往可以帮助我们选择合适的算法并估算其时间复杂度。以下是一些常见的数据范围及其隐含信息:
-
整数范围
- n < 30:指数级别;使用DFS+剪枝。
- n < 100:O( n 3 n^3 n3);使用动态规划;Floyd算法。
- n < 1000:O( n 2 n^2 n2), O( n 2 l o g n n^2 log n n2logn);使用动态规划;二分法。
- n < 1 0 4 10^4 104:O( n ∗ √ n n * √n n∗√n);使用块状链表。
- n < 1 0 5 10^5 105:O( n l o g n n log n nlogn);使用归并、快速、堆排序;线段树;树状数组;Set/Map;Heap;Dijkstra+Heap;二分法。
- n < 1 0 6 10^6 106:O(n);使用哈希;双指针扫描;KMP算法。
- n < 1 0 7 10^7 107:O(n);使用双指针扫描;KMP算法;线性筛素数。
- n < 1 0 9 10^9 109:O(n),O( √ n √n √n);用于判断质数。
- n < 1 0 18 10^{18} 1018:O( l o g n log n logn);求解最大公约数。
-
数组长度和值
- 数组长度较小(如小于1000):可以使用直接排序、暴力搜索等算法。
- 数组长度较大(如超过 1 0 6 10^6 106):应考虑使用高效的排序算法(如归并排序、堆排序)或高级数据结构(如树、堆)。
-
特殊值
- 题目给出特定的数值范围(如0-255):可能暗示可以使用计数排序、基数排序等适用于特定范围的排序算法。
- 值范围较小但数组长度大:适合使用桶排序或计数排序。
2. 时间复杂度总结
理解和分析算法的时间复杂度是刷题时的重要环节。以下是一些常见算法的时间复杂度总结:
在编写和优化代码时,理解这些时间复杂度有助于我们选择合适的算法,提高代码的执行效率。记得在刷题时结合题目给出的数据范围和时间复杂度来选择最佳解法。
3.常见数学工具
3.1最大公约数 (Greatest Common Divisor, GCD)
public class MathUtils {
/**
* 利用辗转相除法计算两个整数的最大公约数。
* @param a 第一个整数
* @param b 第二个整数
* @return 返回两个整数的最大公约数
*/
public static int gcd(int a, int b) {
// 辗转相除法,直到b为0,此时a即为最大公约数
while (b != 0) {
// 取余数
int temp = b;
// 更新a为b,b为a%b
b = a % b;
a = temp;
}
// 当b为0时,a即为最大公约数
return a;
}
}
3.2最小公倍数 (Least Common Multiple, LCM)
public class MathUtils {
/**
* 利用最大公约数计算两个整数的最小公倍数。
* @param a 第一个整数
* @param b 第二个整数
* @return 返回两个整数的最小公倍数
*/
public static int lcm(int a, int b) {
// 最小公倍数 = (a * b) / 最大公约数(a, b)
return a * (b / gcd(a, b));
}
}
3.3质数 (Prime Number)
public class MathUtils {
/**
* 检查一个数是否为质数。
* @param n 需要检查的数
* @return 如果n是质数返回true,否则返回false
*/
public static boolean isPrime(int n) {
// 小于等于1的数不是质数
if (n <= 1) {
return false;
}
// 从2到sqrt(n)检查是否有因数
for (int i = 2; i <= Math.sqrt(n); i++) {
// 如果n能被i整除,则n不是质数
if (n % i == 0) {
return false;
}
}
// 如果没有找到因数,则n是质数
return true;
}
}
3.4素数筛 (Sieve of Eratosthenes)
public class MathUtils {
/**
* 使用埃拉托斯特尼筛法找出所有小于等于n的质数。
* @param n 需要找出质数的最大值
* @return 返回一个布尔数组,数组中索引为质数的元素为true
*/
public static boolean[] sieveOfEratosthenes(int n) {
// 初始化布尔数组,所有元素设为true
boolean[] isPrime = new boolean[n + 1];
for (int i = 0; i <= n; i++) {
isPrime[i] = true;
}
// 0和1不是质数
isPrime[0] = false;
isPrime[1] = false;
// 从2开始遍历,将每个质数的倍数标记为非质数
for (int i = 2; i * i <= n; i++) {
if (isPrime[i]) {
// 从i*i开始,因为i*i之前的数已经被之前的质数筛过
for (int j = i * i; j <= n; j += i) {
isPrime[j] = false;
}
}
}
// 返回布尔数组
return isPrime;
}
}
3.5 快速幂 (Fast Powering)
public class MathUtils {
/**
* 使用快速幂算法计算base的exp次方。
* @param base 底数
* @param exp 指数
* @return 返回计算结果
*/
public static long fastPower(long base, int exp) {
long result = 1; // 初始化结果为1
// 循环直到指数exp为0
while (exp > 0) {
// 如果指数为奇数,则将当前底数乘到结果上
if ((exp & 1) == 1) {
result *= base;
}
// 将底数平方
base *= base;
// 指数除以2,实现指数的快速减少
exp >>= 1;
}
// 返回最终结果
return result;
}
}
3.6欧拉函数 (Euler’s Totient Function)
因式分解实现:
public class MathUtils {
/**
* 计算欧拉函数φ(n)。
* @param n 需要计算的数
* @return 返回φ(n)的值
*/
public static int eulerTotient(int n) {
int result = n; // 初始化结果为n
// 从2开始到sqrt(n),检查每个因子
for (int i = 2; i <= Math.sqrt(n); i++) {
// 如果i是n的因子
if (n % i == 0) {
// 从结果中减去n/i的整数部分
while (n % i == 0) {
n /= i;
}
result -= result / i;
}
}
// 如果n是质数,则单独处理
if (n > 1) {
result -= result / n;
}
// 返回φ(n)的值
return result;
}
}
筛法实现:
public static int phi(int n) {
// 初始化一个数组,下标表示数字,值表示与下标对应的数字互质的数的个数
int[] phi = new int[n + 1];
for (int i = 0; i <= n; i++) {
phi[i] = i; // 最开始都假设与自己互质
}
// 筛选素数并更新与素数倍数互质数的个数
for (int i = 2; i <= n; i++) {
if (phi[i] == i) { // 如果当前数字与自己互质,即当前数字为素数
for (int j = i; j <= n; j += i) { // 更新当前数字的所有倍数
phi[j] -= phi[j] / i; // j 与 i 的倍数互质的数个数减去 j/i
}
}
}
// 返回 n 的互质数个数
return phi[n];
}