文章目录
前言
贪心、回溯、动态规划、分治算法都是常见的算法思想,主要区别在于它们解决问题的方式和适用的场景
一、贪心算法 (Greedy Algorithm)
思想
每一步都做出局部最优选择,以期最终获得全局最优解。
适用场景
问题具有贪心选择性质,即每个局部最优选择最终会导致全局最优解的产生。
优点
实现简单,效率较高。
缺点
不能保证所有问题都能得到全局最优解,通常用于有贪心性质的问题。
示例代码
找零钱问题(贪心解法)
通过贪心选择,每次选取能减少金额最多的硬币,最终获得最少硬币数。
import java.util.Arrays;
public class GreedyChange {
public static int minCoins(int[] coins, int amount) {
Arrays.sort(coins); // 先排序,保证从最大面值开始
int count = 0;
for (int i = coins.length - 1; i >= 0; i--) {
if (amount >= coins[i]) {
count += amount / coins[i]; // 用尽可能多的当前面值
amount %= coins[i]; // 更新剩余金额
}
}
return count;
}
public static void main(String[] args) {
int[] coins = {1, 2, 5, 10, 20, 50};
int amount = 93;
System.out.println("最少硬币数:" + minCoins(coins, amount));
}
}
二、回溯算法 (Backtracking)
思想
尝试所有可能的解,当发现某个解不可行时,回退到上一步并尝试其他可能性。
适用场景
问题具有多层决策,每层决策依赖上一步的选择。常用于组合问题、排列问题、路径问题等。
优点
能够解决所有的可能解问题。
缺点
效率低,容易造成指数级的时间复杂度。
示例代码
N皇后问题
通过递归和回溯尝试每一行放置皇后的位置,若当前选择不可行,回溯到上一行重新选择。
public class NQueens {
final int N = 8; // N表示棋盘大小
int[] board = new int[N];
// 判断某位置是否合法
boolean isValid(int row, int col) {
for (int i = 0; i < row; i++) {
// 检查同列和对角线冲突
if (board[i] == col || Math.abs(board[i] - col) == Math.abs(i - row)) {
return false;
}
}
return true;
}
// 回溯核心逻辑
boolean solve(int row) {
if (row == N) {
printSolution();
return true; // 找到解
}
boolean hasSolution = false;
for (int col = 0; col < N; col++) {
if (isValid(row, col)) {
board[row] = col; // 选择此列
hasSolution |= solve(row + 1); // 递归下一行
}
}
return hasSolution;
}
// 打印棋盘
void printSolution() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (board[i] == j) {
System.out.print("Q ");
} else {
System.out.print(". ");
}
}
System.out.println();
}
System.out.println();
}
public static void main(String[] args) {
NQueens queens = new NQueens();
queens.solve(0);
}
}
三、动态规划 (Dynamic Programming)
思想
将问题拆解为子问题,并通过存储子问题的解来避免重复计算,通常用于求解最优解。
适用场景
问题可以分解为重叠子问题,且问题具有最优子结构(即子问题的最优解可以构成整个问题的最优解)。
优点
效率高,避免了重复计算。
缺点
需要额外的存储空间,适合求解具有子问题重叠性质的问题。
示例代码
斐波那契数列(动态规划解法)
通过动态规划的方式保存之前的计算结果,从而避免了递归带来的重复计算。
public class FibonacciDP {
public static int fib(int n) {
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
}
return dp[n];
}
public static void main(String[] args) {
int n = 10;
System.out.println("第 " + n + " 个斐波那契数:" + fib(n));
}
}
四、分治算法 (Divide and Conquer)
思想
将问题分解为多个子问题,分别解决子问题,然后将子问题的解合并为原问题的解。
适用场景
问题可以被分成独立的子问题,且子问题的解可以合并成整体问题的解。
优点
通过递归解决子问题,减少复杂度。
缺点
某些问题无法有效分治,递归会带来额外的栈空间消耗。
示例代码
归并排序
通过递归分治的方式将数组分成若干小部分进行排序,最后合并这些部分以得到最终的有序数组。
import java.util.Arrays;
public class MergeSort {
// 归并排序主函数
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
// 分治,递归排序左右两边
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并排序后的两个子数组
merge(arr, left, mid, right);
}
}
// 合并两个有序子数组
public static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
// 将合并后的结果复制回原数组
System.arraycopy(temp, 0, arr, left, temp.length);
}
public static void main(String[] args) {
int[] arr = {38, 27, 43, 3, 9, 82, 10};
System.out.println("排序前: " + Arrays.toString(arr));
mergeSort(arr, 0, arr.length - 1);
System.out.println("排序后: " + Arrays.toString(arr));
}
}
如何快速区分使用哪种算法?
- 是否可以分治:如果问题可以分成多个独立子问题,并且每个子问题的解可以合并,那就考虑分治算法。
- 是否有重叠子问题:如果问题可以通过求解子问题来优化效率,并且子问题存在重复计算,可以考虑动态规划。
- 是否需要尝试所有可能解:如果问题要求找到所有可能的解决方案,并且可以通过回溯进行剪枝,选择回溯算法。
- 是否存在贪心选择性质:如果问题允许通过局部最优选择直接找到全局最优解,可以考虑使用贪心算法。