Java版《算法分析与设计》期末代码题:全部考点+题型总结(万字详解,附完整代码)
适用对象:本科计算机专业期末考试复习 侧重点:手写Java代码,不考复杂底层证明 核心考点:递归、分治、贪心、动态规划、回溯、图算法、查找排序 分值分布:代码题一般6~10分一题,要求写方法/完整类、输出结果、分析复杂度 本文特色:每个考点均附完整可运行Java代码 + 复杂度分析 + 考试坑点提醒
目录
前言:期末考试到底考什么?
每到期末,算法分析与设计这门课总是让无数计算机专业的同学头疼。不同于数据结构侧重"会用",算法课更强调 理解思想 和 手写代码 的能力。
根据多年期末考试的经验总结,算法分析与设计的期末代码题有以下规律:
- 基础小题(5分左右):二分查找、并查集、归并merge函数、快排partition函数——写核心函数即可
- 中档大题(8~12分):01背包、最长公共子序列(LCS)、回溯全排列、Dijkstra最短路径、Floyd多源最短路径
- 拔高题(10分以上):N皇后、归并排序求逆序对、编辑距离、拓扑排序判环
考试写Java代码的通用要求:
下面我们就按照考点逐一展开,每个考点都给出完整可运行的Java代码、详细的思路分析、复杂度计算以及考试中容易踩的坑。
一、分治算法(必考高频)
1.1 核心思想
分治(Divide and Conquer)的核心思想可以用三步概括:
时间复杂度分析的核心工具是主定理(Master Theorem):
对于递推式
T
(
n
)
=
a
T
(
n
/
b
)
+
f
(
n
)
T(n) = aT(n/b) + f(n)
T(n)=aT(n/b)+f(n):
- 若
f
(
n
)
=
O
(
n
log
b
a
−
ϵ
)
f(n) = O(n^{\\log_b a – \\epsilon})
f(n)=O(nlogba−ϵ),则T
(
n
)
=
Θ
(
n
log
b
a
)
T(n) = \\Theta(n^{\\log_b a})
T(n)=Θ(nlogba) - 若
f
(
n
)
=
Θ
(
n
log
b
a
)
f(n) = \\Theta(n^{\\log_b a})
f(n)=Θ(nlogba),则T
(
n
)
=
Θ
(
n
log
b
a
log
n
)
T(n) = \\Theta(n^{\\log_b a} \\log n)
T(n)=Θ(nlogbalogn) - 若
f
(
n
)
=
Ω
(
n
log
b
a
+
ϵ
)
f(n) = \\Omega(n^{\\log_b a + \\epsilon})
f(n)=Ω(nlogba+ϵ),则T
(
n
)
=
Θ
(
f
(
n
)
)
T(n) = \\Theta(f(n))
T(n)=Θ(f(n))
1.2 二分查找(最基础,必会)
题目描述:给定一个有序数组 nums 和目标值 target,查找 target 在数组中的下标,不存在则返回 -1。
非递归版本:
public class BinarySearch {
/**
* 基础二分查找 – 非递归版本
* 时间复杂度:O(log n)
* 空间复杂度:O(1)
*/
public static int binarySearch(int[] nums, int target) {
if (nums == null || nums.length == 0) return –1;
int left = 0, right = nums.length – 1;
while (left <= right) {
int mid = left + (right – left) / 2; // 防止溢出!不要写 (left + right) / 2
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid – 1;
}
}
return –1;
}
/**
* 二分查找 – 递归版本
* 时间复杂度:O(log n)
* 空间复杂度:O(log n)(递归栈)
*/
public static int binarySearchRecursive(int[] nums, int target, int left, int right) {
if (left > right) return –1;
int mid = left + (right – left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
return binarySearchRecursive(nums, target, mid + 1, right);
} else {
return binarySearchRecursive(nums, target, left, mid – 1);
}
}
/**
* 进阶:查找第一个等于target的位置(高频变体)
* 时间复杂度:O(log n)
*/
public static int findFirst(int[] nums, int target) {
int left = 0, right = nums.length – 1;
int result = –1;
while (left <= right) {
int mid = left + (right – left) / 2;
if (nums[mid] == target) {
result = mid; // 记录当前位置
right = mid – 1; // 继续往左找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid – 1;
}
}
return result;
}
/**
* 进阶:查找最后一个等于target的位置(高频变体)
* 时间复杂度:O(log n)
*/
public static int findLast(int[] nums, int target) {
int left = 0, right = nums.length – 1;
int result = –1;
while (left <= right) {
int mid = left + (right – left) / 2;
if (nums[mid] == target) {
result = mid; // 记录当前位置
left = mid + 1; // 继续往右找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid – 1;
}
}
return result;
}
public static void main(String[] args) {
int[] nums = {1, 2, 2, 2, 3, 4, 5};
System.out.println("基础查找: " + binarySearch(nums, 2)); // 输出: 某个2的下标
System.out.println("第一个2: " + findFirst(nums, 2)); // 输出: 1
System.out.println("最后一个2: " + findLast(nums, 2)); // 输出: 3
System.out.println("查找6: " + binarySearch(nums, 6)); // 输出: -1
}
}
⚠️ 考试坑点:
1.3 归并排序(超级高频,尤其是求逆序对)
题目描述:使用分治思想对数组进行排序。
public class MergeSort {
/**
* 归并排序主方法
* 时间复杂度:O(n log n)
* 空间复杂度:O(n)
*/
public static void mergeSort(int[] arr, int left, int right) {
if (left >= right) return; // 递归终止条件
int mid = left + (right – left) / 2;
mergeSort(arr, left, mid); // 递归排序左半部分
mergeSort(arr, mid + 1, right); // 递归排序右半部分
merge(arr, left, mid, right); // 合并两个有序子数组
}
/**
* 合并两个有序子数组 arr[left..mid] 和 arr[mid+1..right]
*/
private 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++];
// 将temp复制回原数组
for (int p = 0; p < temp.length; p++) {
arr[left + p] = temp[p];
}
}
public static void main(String[] args) {
int[] arr = {38, 27, 43, 3, 9, 82, 10};
mergeSort(arr, 0, arr.length – 1);
System.out.println("排序结果: " + java.util.Arrays.toString(arr));
// 输出: [3, 9, 10, 27, 38, 43, 82]
}
}
⭐ 归并排序改造:求逆序对数量(期末超级高频!)
题目描述:给定一个数组,求其中逆序对的总数。逆序对即 i < j 且 arr[i] > arr[j]。
核心思路:在归并排序的 merge 过程中,当左半部分的 arr[i] > arr[j] 时,说明 arr[i] 及其后面的所有元素都大于 arr[j],因此逆序对数量增加 mid – i + 1。
public class InversionCount {
private static int count = 0; // 逆序对计数器
/**
* 求逆序对数量(基于归并排序改造)
* 时间复杂度:O(n log n)
* 空间复杂度:O(n)
*/
public static int countInversions(int[] arr, int left, int right) {
if (left >= right) return 0;
int mid = left + (right – left) / 2;
countInversions(arr, left, mid);
countInversions(arr, mid + 1, right);
mergeAndCount(arr, left, mid, right);
return count;
}
private static void mergeAndCount(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 {
// arr[i] > arr[j],产生逆序对
// 左半部分从i到mid的所有元素都大于arr[j]
count += (mid – i + 1); // ★★★ 核心公式 ★★★
temp[k++] = arr[j++];
}
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
for (int p = 0; p < temp.length; p++) {
arr[left + p] = temp[p];
}
}
public static void main(String[] args) {
int[] arr = {7, 5, 6, 4}; // 逆序对: (7,5),(7,6),(7,4),(5,4),(6,4) = 5
count = 0; // 重置
System.out.println("逆序对数量: " + countInversions(arr, 0, arr.length – 1));
// 输出: 5
}
}
⚠️ 考试坑点:
1.4 快速排序
public class QuickSort {
/**
* 快速排序主方法
* 时间复杂度:平均O(n log n),最坏O(n²)
* 空间复杂度:O(log n)(递归栈)
*/
public static void quickSort(int[] arr, int left, int right) {
if (left >= right) return;
int pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex – 1);
quickSort(arr, pivotIndex + 1, right);
}
/**
* 分区函数:选最右元素为基准,将小于pivot的放左边,大于的放右边
* 返回pivot的最终位置
*/
public static int partition(int[] arr, int left, int right) {
int pivot = arr[right]; // 选最右边的元素作为基准
int i = left – 1; // i指向小于pivot的最后一个元素
for (int j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, right); // 把pivot放到正确位置
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {10, 7, 8, 9, 1, 5};
quickSort(arr, 0, arr.length – 1);
System.out.println("排序结果: " + java.util.Arrays.toString(arr));
// 输出: [1, 5, 7, 8, 9, 10]
}
}
⭐ 快速选择:求第K大/第K小元素
public class QuickSelect {
/**
* 求数组中第k小的元素(不用完全排序)
* 时间复杂度:平均O(n),最坏O(n²)
* 空间复杂度:O(log n)
*/
public static int findKthSmallest(int[] arr, int left, int right, int k) {
if (left == right) return arr[left];
int pivotIndex = partition(arr, left, right);
if (k – 1 == pivotIndex) {
return arr[pivotIndex];
} else if (k – 1 < pivotIndex) {
return findKthSmallest(arr, left, pivotIndex – 1, k);
} else {
return findKthSmallest(arr, pivotIndex + 1, right, k);
}
}
private static int partition(int[] arr, int left, int right) {
int pivot = arr[right];
int i = left – 1;
for (int j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;
}
}
int temp = arr[i + 1]; arr[i + 1] = arr[right]; arr[right] = temp;
return i + 1;
}
public static void main(String[] args) {
int[] arr = {3, 2, 1, 5, 6, 4};
// 第2小的元素
System.out.println("第2小: " + findKthSmallest(arr, 0, arr.length – 1, 2));
// 输出: 2
// 第2大 = 第(n-2+1)小 = 第5小
int k = 2; // 第k大
System.out.println("第2大: " + findKthSmallest(arr, 0, arr.length – 1, arr.length – k + 1));
// 输出: 5
}
}
1.5 最大子数组(分治版本)
public class MaxSubarrayDivideConquer {
/**
* 分治法求最大子数组和
* 时间复杂度:O(n log n)
* 空间复杂度:O(log n)
* 注意:此题DP版本(Kadane算法)更优,为O(n),见动态规划部分
*/
public static int maxSubArray(int[] arr, int left, int right) {
if (left == right) return arr[left];
int mid = left + (right – left) / 2;
// 三种情况取最大值
int leftMax = maxSubArray(arr, left, mid); // 最大子数组在左半部分
int rightMax = maxSubArray(arr, mid + 1, right); // 最大子数组在右半部分
int crossMax = maxCrossingSum(arr, left, mid, right); // 最大子数组跨越中点
return Math.max(Math.max(leftMax, rightMax), crossMax);
}
/**
* 求跨越中点的最大子数组和
*/
private static int maxCrossingSum(int[] arr, int left, int mid, int right) {
// 向左延伸的最大和
int leftSum = Integer.MIN_VALUE;
int sum = 0;
for (int i = mid; i >= left; i—) {
sum += arr[i];
leftSum = Math.max(leftSum, sum);
}
// 向右延伸的最大和
int rightSum = Integer.MIN_VALUE;
sum = 0;
for (int i = mid + 1; i <= right; i++) {
sum += arr[i];
rightSum = Math.max(rightSum, sum);
}
return leftSum + rightSum;
}
public static void main(String[] args) {
int[] arr = {–2, 1, –3, 4, –1, 2, 1, –5, 4};
System.out.println("最大子数组和: " + maxSubArray(arr, 0, arr.length – 1));
// 输出: 6(子数组 [4, -1, 2, 1])
}
}
分治部分小结:分治算法在期末考试中出题概率极高,尤其是归并排序求逆序对和快速排序的partition函数。写代码时一定要注意数组边界和递归终止条件,这两个是最容易扣分的地方。
二、贪心算法(中等频率)
2.1 核心思想
贪心算法的核心是:每一步都选择当前看来最优的方案,期望局部最优能推出全局最优。
贪心算法的代码特点:
- 一般不需要递归,排序+一次遍历即可
- 代码通常比较短,但正确性证明(简答题)需要单独分析
- 考试中贪心代码题一般只需要写实现,证明一般出简答题
2.2 活动选择问题
题目描述:给定 n 个活动的开始时间和结束时间,选择尽可能多的互不重叠的活动。
贪心策略:按结束时间升序排序,每次选结束时间最早且与已选活动不冲突的活动。
import java.util.*;
public class ActivitySelection {
/**
* 活动选择问题 – 贪心算法
* 时间复杂度:O(n log n)(排序)
* 空间复杂度:O(n)
*/
public static List<int[]> selectActivities(int[][] activities) {
// 按结束时间升序排序
Arrays.sort(activities, (a, b) -> a[1] – b[1]);
List<int[]> selected = new ArrayList<>();
int lastEndTime = –1;
for (int[] activity : activities) {
if (activity[0] >= lastEndTime) {
// 当前活动的开始时间 >= 上一个选中活动的结束时间
selected.add(activity);
lastEndTime = activity[1];
}
}
return selected;
}
public static void main(String[] args) {
// 每个活动: {开始时间, 结束时间}
int[][] activities = {
{1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 9}, {5, 9},
{6, 10}, {8, 11}, {8, 12}, {2, 14}, {12, 16}
};
List<int[]> result = selectActivities(activities);
System.out.println("最多选择 " + result.size() + " 个活动:");
for (int[] act : result) {
System.out.println(" 活动 [" + act[0] + ", " + act[1] + ")");
}
// 输出: 4个活动
}
}
2.3 哈夫曼编码(优先队列构造哈夫曼树)
题目描述:给定一组字符及其频率,构造哈夫曼树,计算带权路径长度(WPL)。
import java.util.*;
public class HuffmanCoding {
// 哈夫曼树节点
static class HuffmanNode implements Comparable<HuffmanNode> {
char ch;
int freq;
HuffmanNode left, right;
HuffmanNode(char ch, int freq) {
this.ch = ch;
this.freq = freq;
}
@Override
public int compareTo(HuffmanNode other) {
return this.freq – other.freq; // 按频率升序
}
}
/**
* 构造哈夫曼树,返回根节点
* 时间复杂度:O(n log n)
* 空间复杂度:O(n)
*/
public static HuffmanNode buildHuffmanTree(char[] chars, int[] freqs) {
PriorityQueue<HuffmanNode> pq = new PriorityQueue<>();
for (int i = 0; i < chars.length; i++) {
pq.offer(new HuffmanNode(chars[i], freqs[i]));
}
while (pq.size() > 1) {
HuffmanNode left = pq.poll(); // 取出频率最小的两个节点
HuffmanNode right = pq.poll();
HuffmanNode parent = new HuffmanNode('\\0', left.freq + right.freq);
parent.left = left;
parent.right = right;
pq.offer(parent); // 新节点入队
}
return pq.poll(); // 根节点
}
/**
* 计算带权路径长度(WPL)
*/
public static int calculateWPL(HuffmanNode root, int depth) {
if (root == null) return 0;
// 叶子节点
if (root.left == null && root.right == null) {
return root.freq * depth;
}
return calculateWPL(root.left, depth + 1) + calculateWPL(root.right, depth + 1);
}
/**
* 打印哈夫曼编码
*/
public static void printCodes(HuffmanNode root, String code) {
if (root == null) return;
if (root.left == null && root.right == null) {
System.out.println(" 字符 '" + root.ch + "': " + code + " (频率=" + root.freq + ")");
}
printCodes(root.left, code + "0");
printCodes(root.right, code + "1");
}
public static void main(String[] args) {
char[] chars = {'a', 'b', 'c', 'd', 'e', 'f'};
int[] freqs = {45, 13, 12, 16, 9, 5};
HuffmanNode root = buildHuffmanTree(chars, freqs);
System.out.println("哈夫曼编码:");
printCodes(root, "");
System.out.println("WPL = " + calculateWPL(root, 0));
}
}
2.4 零钱兑换(贪心版本)
public class CoinChangeGreedy {
/**
* 零钱兑换 – 贪心算法(仅适用于规范币值,如1,5,10,25,50,100)
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*
* 注意:贪心法不是通用解法!对于币值{1,3,4}凑6元,贪心给出4+1+1=3枚,
* 但最优解是3+3=2枚。考试时老师会说明硬币满足贪心条件。
*/
public static int coinChangeGreedy(int[] coins, int amount) {
// 假设coins已经按降序排列(或先排序)
Arrays.sort(coins);
int count = 0;
for (int i = coins.length – 1; i >= 0; i—) {
if (amount <= 0) break;
count += amount / coins[i];
amount %= coins[i];
}
return amount == 0 ? count : –1;
}
public static void main(String[] args) {
int[] coins = {1, 5, 10, 25};
System.out.println("凑 63 分需要: " + coinChangeGreedy(coins, 63) + " 枚硬币");
// 25*2 + 10*1 + 1*3 = 6枚
}
}
贪心部分小结:贪心算法代码一般较短,核心是排序+一次遍历。期末考试中贪心代码题出现频率中等,但贪心策略的证明(简答/选择题)出现频率较高。
三、动态规划(期末代码大题TOP1,重中之重)
3.1 核心思想
动态规划(Dynamic Programming, DP)是期末考试中出题频率最高、分值最大的考点。
DP解题四步法:
考试关键提醒:
- 必须写迭代版本,不要写递归(递归会超时,而且老师会扣分)
- 01背包逆序遍历容量,完全背包正序遍历容量——这个循环顺序是高频扣分点!
3.2 一维DP
3.2.1 爬楼梯
题目描述:每次可以爬1或2个台阶,求到达第n阶有多少种方法。
public class ClimbingStairs {
/**
* 爬楼梯 – 动态规划
* 状态定义:dp[i] = 到达第i阶的方法数
* 状态转移:dp[i] = dp[i-1] + dp[i-2]
* 时间复杂度:O(n)
* 空间复杂度:O(1)(空间优化后)
*/
public static int climbStairs(int n) {
if (n <= 2) return n;
// 空间优化:只需要前两个状态
int prev2 = 1; // dp[1]
int prev1 = 2; // dp[2]
for (int i = 3; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
// 未优化版本(考试建议先写这个,更清晰)
public static int climbStairsFullDP(int n) {
if (n <= 2) return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i – 1] + dp[i – 2];
}
return dp[n];
}
public static void main(String[] args) {
System.out.println("爬5阶楼梯: " + climbStairs(5) + " 种方法");
// 输出: 8
}
}
3.2.2 最大连续子数组和(Kadane算法)
public class MaxSubArrayDP {
/**
* 最大连续子数组和 – 动态规划(Kadane算法)
* 状态定义:dp[i] = 以arr[i]结尾的最大连续子数组和
* 状态转移:dp[i] = max(arr[i], dp[i-1] + arr[i])
* 时间复杂度:O(n)
* 空间复杂度:O(1)(空间优化后)
*/
public static int maxSubArray(int[] arr) {
if (arr == null || arr.length == 0) return 0;
int currentMax = arr[0];
int globalMax = arr[0];
for (int i = 1; i < arr.length; i++) {
currentMax = Math.max(arr[i], currentMax + arr[i]);
globalMax = Math.max(globalMax, currentMax);
}
return globalMax;
}
public static void main(String[] args) {
int[] arr = {–2, 1, –3, 4, –1, 2, 1, –5, 4};
System.out.println("最大子数组和: " + maxSubArray(arr));
// 输出: 6(子数组 [4, -1, 2, 1])
}
}
3.2.3 打家劫舍
题目描述:相邻房屋不能同时偷,求能偷到的最大金额。
public class HouseRobber {
/**
* 打家劫舍 – 动态规划
* 状态定义:dp[i] = 偷到第i个房屋时能获得的最大金额
* 状态转移:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
* – 不偷第i个:dp[i-1]
* – 偷第i个:dp[i-2] + nums[i]
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public static int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int prev2 = 0; // dp[i-2]
int prev1 = 0; // dp[i-1]
for (int num : nums) {
int current = Math.max(prev1, prev2 + num);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
public static void main(String[] args) {
int[] nums = {2, 7, 9, 3, 1};
System.out.println("最大偷窃金额: " + rob(nums));
// 输出: 12(偷房屋0(2) + 房屋2(9) + 房屋4(1) = 12)
}
}
3.3 二维DP(大题高频区)
⭐ 3.3.1 01背包(必考!!!)
题目描述:有 n 个物品,每个物品有重量 weight[i] 和价值 value[i],背包容量为 W。每个物品只能选一次,求能装入背包的最大价值。
public class ZeroOneKnapsack {
/**
* 01背包 – 动态规划(二维DP数组版本,考试推荐)
*
* 状态定义:dp[i][j] = 前i个物品,背包容量为j时的最大价值
* 状态转移:
* 不选第i个物品:dp[i][j] = dp[i-1][j]
* 选第i个物品:dp[i][j] = dp[i-1][j-weight[i-1]] + value[i-1] (需j >= weight[i-1])
* dp[i][j] = max(不选, 选)
*
* 时间复杂度:O(n * W)
* 空间复杂度:O(n * W)
*/
public static int knapsack01(int[] weights, int[] values, int W) {
int n = weights.length;
int[][] dp = new int[n + 1][W + 1];
// 初始化:dp[0][j] = 0(0个物品,价值为0),dp[i][0] = 0(容量为0,价值为0)
// Java数组默认初始化为0,所以可以省略
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= W; j++) {
if (j >= weights[i – 1]) {
// 可以选或不选第i个物品
dp[i][j] = Math.max(
dp[i – 1][j], // 不选
dp[i – 1][j – weights[i – 1]] + values[i – 1] // 选
);
} else {
// 容量不够,不能选
dp[i][j] = dp[i – 1][j];
}
}
}
return dp[n][W];
}
/**
* 01背包 – 空间优化版本(一维DP数组)
* ★★★ 注意:容量j必须逆序遍历!★★★
* 空间复杂度:O(W)
*/
public static int knapsack01Optimized(int[] weights, int[] values, int W) {
int n = weights.length;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
// ★★★ 关键:逆序遍历容量 ★★★
// 原因:每个物品只能选一次,逆序保证不会重复选取
for (int j = W; j >= weights[i]; j—) {
dp[j] = Math.max(dp[j], dp[j – weights[i]] + values[i]);
}
}
return dp[W];
}
public static void main(String[] args) {
int[] weights = {2, 3, 4, 5};
int[] values = {3, 4, 5, 6};
int W = 8;
System.out.println("01背包最大价值(二维): " + knapsack01(weights, values, W));
System.out.println("01背包最大价值(一维优化): " + knapsack01Optimized(weights, values, W));
// 输出: 10(选物品0(重量2,价值3) + 物品3(重量5,价值6) = 价值9…
// 或选物品1(重量3,价值4) + 物品2(重量4,价值5) = 价值9… 不对
// 选物品0(2,3) + 物品2(4,5) = 重量6, 价值8
// 选物品1(3,4) + 物品3(5,6) = 重量8, 价值10 ✓)
}
}
⚠️ 超级重要坑点:01背包空间优化版本中,容量 j 必须从大到小(逆序)遍历!如果正序遍历,同一个物品可能被重复选取,就变成了完全背包。这个循环顺序是期末考试中最常见的扣分点!
⭐ 3.3.2 完全背包
题目描述:与01背包相同,但每个物品可以无限次选取。
public class CompleteKnapsack {
/**
* 完全背包 – 动态规划(一维DP数组)
* 状态转移:dp[j] = max(dp[j], dp[j – weights[i]] + values[i])
*
* ★★★ 关键区别:容量j正序遍历!★★★
* 原因:物品可以重复选取,正序遍历允许同一物品被多次使用
*
* 时间复杂度:O(n * W)
* 空间复杂度:O(W)
*/
public static int completeKnapsack(int[] weights, int[] values, int W) {
int n = weights.length;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
// ★★★ 关键:正序遍历容量 ★★★(与01背包相反)
for (int j = weights[i]; j <= W; j++) {
dp[j] = Math.max(dp[j], dp[j – weights[i]] + values[i]);
}
}
return dp[W];
}
public static void main(String[] args) {
int[] weights = {2, 3, 4};
int[] values = {3, 4, 5};
int W = 8;
System.out.println("完全背包最大价值: " + completeKnapsack(weights, values, W));
// 选4次物品0(重量2*4=8, 价值3*4=12) = 12
}
}
01背包 vs 完全背包 循环顺序对比:
- 01背包(一维优化):for (int j = W; j >= weights[i]; j–) 逆序
- 完全背包(一维优化):for (int j = weights[i]; j <= W; j++) 正序
记住这个区别,考试必考!
⭐ 3.3.3 最长公共子序列 LCS
题目描述:给定两个字符串 s1 和 s2,求它们的最长公共子序列的长度。
public class LCS {
/**
* 最长公共子序列 – 动态规划
* 状态定义:dp[i][j] = s1前i个字符和s2前j个字符的LCS长度
* 状态转移:
* 若 s1[i-1] == s2[j-1]:dp[i][j] = dp[i-1][j-1] + 1
* 否则:dp[i][j] = max(dp[i-1][j], dp[i][j-1])
* 时间复杂度:O(m * n)
* 空间复杂度:O(m * n)
*/
public static int lcsLength(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i – 1) == s2.charAt(j – 1)) {
dp[i][j] = dp[i – 1][j – 1] + 1;
} else {
dp[i][j] = Math.max(dp[i – 1][j], dp[i][j – 1]);
}
}
}
return dp[m][n];
}
/**
* 输出LCS的具体子序列(加分项,部分考试要求)
* 通过回溯dp数组还原
*/
public static String lcsString(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i – 1) == s2.charAt(j – 1)) {
dp[i][j] = dp[i – 1][j – 1] + 1;
} else {
dp[i][j] = Math.max(dp[i – 1][j], dp[i][j – 1]);
}
}
}
// 回溯构造LCS字符串
StringBuilder sb = new StringBuilder();
int i = m, j = n;
while (i > 0 && j > 0) {
if (s1.charAt(i – 1) == s2.charAt(j – 1)) {
sb.append(s1.charAt(i – 1));
i—; j—;
} else if (dp[i – 1][j] > dp[i][j – 1]) {
i—;
} else {
j—;
}
}
return sb.reverse().toString();
}
public static void main(String[] args) {
String s1 = "ABCBDAB";
String s2 = "BDCAB";
System.out.println("LCS长度: " + lcsLength(s1, s2)); // 输出: 4
System.out.println("LCS子序列: " + lcsString(s1, s2)); // 输出: BCAB
}
}
3.3.4 最长回文子串
public class LongestPalindrome {
/**
* 最长回文子串 – 动态规划
* 状态定义:dp[i][j] = s[i..j] 是否为回文串(boolean)
* 状态转移:
* 若 s[i] == s[j]:dp[i][j] = dp[i+1][j-1](当j-i<=2时为true)
* 否则:dp[i][j] = false
* 时间复杂度:O(n²)
* 空间复杂度:O(n²)
*/
public static String longestPalindrome(String s) {
if (s == null || s.length() < 2) return s;
int n = s.length();
boolean[][] dp = new boolean[n][n];
int maxLen = 1, start = 0;
// 单个字符都是回文
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 注意遍历顺序:先枚举长度,再枚举起点
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n – len; i++) {
int j = i + len – 1;
if (s.charAt(i) == s.charAt(j)) {
if (len <= 3) { // 长度2或3且首尾相同,一定是回文
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j – 1];
}
}
if (dp[i][j] && len > maxLen) {
maxLen = len;
start = i;
}
}
}
return s.substring(start, start + maxLen);
}
public static void main(String[] args) {
System.out.println("最长回文子串: " + longestPalindrome("babad"));
// 输出: "bab" 或 "aba"
System.out.println("最长回文子串: " + longestPalindrome("cbbd"));
// 输出: "bb"
}
}
3.3.5 编辑距离
题目描述:给定两个字符串 word1 和 word2,计算将 word1 转换成 word2 所使用的最少操作数(插入、删除、替换)。
public class EditDistance {
/**
* 编辑距离 – 动态规划
* 状态定义:dp[i][j] = word1前i个字符转换为word2前j个字符的最少操作数
* 状态转移:
* 若 word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1](不需要操作)
* 否则:dp[i][j] = min(
* dp[i-1][j] + 1, // 删除word1[i-1]
* dp[i][j-1] + 1, // 在word1中插入word2[j-1]
* dp[i-1][j-1] + 1 // 将word1[i-1]替换为word2[j-1]
* )
* 初始化:dp[i][0] = i(全删除),dp[0][j] = j(全插入)
* 时间复杂度:O(m * n)
* 空间复杂度:O(m * n)
*/
public static int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// 初始化
for (int i = 0; i <= m; i++) dp[i][0] = i; // word1前i个字符变为空串,需i次删除
for (int j = 0; j <= n; j++) dp[0][j] = j; // 空串变为word2前j个字符,需j次插入
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i – 1) == word2.charAt(j – 1)) {
dp[i][j] = dp[i – 1][j – 1]; // 字符相同,不需要操作
} else {
dp[i][j] = Math.min(
Math.min(dp[i – 1][j], dp[i][j – 1]), // 删除、插入
dp[i – 1][j – 1] // 替换
) + 1;
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
System.out.println("编辑距离(horse, ros): " + minDistance("horse", "ros"));
// 输出: 3 (horse -> rorse -> rose -> ros)
System.out.println("编辑距离(intention, execution): " + minDistance("intention", "execution"));
// 输出: 5
}
}
3.3.6 矩阵链乘法
题目描述:给定n个矩阵的维度,确定矩阵相乘的顺序,使得标量乘法次数最少。
public class MatrixChainMultiplication {
/**
* 矩阵链乘法 – 动态规划
* 状态定义:dp[i][j] = 矩阵Ai到Aj相乘的最小标量乘法次数
* 状态转移:dp[i][j] = min{ dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j] },i <= k < j
* 时间复杂度:O(n³)
* 空间复杂度:O(n²)
*/
public static int matrixChainOrder(int[] p) {
int n = p.length – 1; // n个矩阵
int[][] dp = new int[n + 1][n + 1];
// dp[i][i] = 0,单个矩阵不需要乘法
// len: 链的长度,从2开始
for (int len = 2; len <= n; len++) {
for (int i = 1; i <= n – len + 1; i++) {
int j = i + len – 1;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k + 1][j] + p[i – 1] * p[k] * p[j];
dp[i][j] = Math.min(dp[i][j], cost);
}
}
}
return dp[1][n];
}
public static void main(String[] args) {
// 矩阵维度:A1=10×30, A2=30×5, A3=5×60
int[] p = {10, 30, 5, 60};
System.out.println("最小乘法次数: " + matrixChainOrder(p));
// 输出: 4500 ((A1*A2)*A3 = 10*30*5 + 10*5*60 = 1500 + 3000 = 4500)
}
}
动态规划部分小结:DP是期末考试的绝对核心,01背包和LCS是最高频考点。写DP代码时要牢记四步法(状态定义、转移方程、初始化、填表顺序),写完后一定要标注时间复杂度和空间复杂度。
四、回溯算法(非常高频代码题)
4.1 核心思想
回溯算法的本质是递归 + 剪枝,通过枚举所有可能的解,并在搜索过程中及时排除不符合条件的分支。
Java回溯算法的通用模板:
void backtrack(路径, 选择列表) {
if (满足结束条件) {
结果.add(路径);
return;
}
for (选择 : 选择列表) {
做选择; // path.add(choice)
backtrack(路径, 选择列表);
撤销选择; // path.remove(choice) ★★★ 回溯核心 ★★★
}
}
关键要点:
- Java中一般用 List<List<Integer>> 保存结果集
- 回溯时 add 之后,递归返回一定要 remove(撤销选择)
- 剪枝是得分点,能写剪枝一定要写
4.2 子集问题
import java.util.*;
public class Subsets {
/**
* 子集 – 回溯算法
* 给定一个不含重复元素的数组,返回所有可能的子集
* 时间复杂度:O(n * 2^n)
* 空间复杂度:O(n)(递归栈深度)
*/
public static List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}
private static void backtrack(List<List<Integer>> result, List<Integer> path,
int[] nums, int startIndex) {
// 每个节点都是一个有效子集(不需要判断终止条件)
result.add(new ArrayList<>(path)); // 注意:必须new ArrayList复制!
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]); // 做选择
backtrack(result, path, nums, i + 1); // 递归
path.remove(path.size() – 1); // 撤销选择(回溯)
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
List<List<Integer>> result = subsets(nums);
System.out.println("所有子集: " + result);
// 输出: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
}
}
4.3 子集II(含重复元素,去重)
import java.util.*;
public class SubsetsII {
/**
* 子集II – 含重复元素,需要去重
* 关键:先排序,然后在同一层中跳过重复元素
* 时间复杂度:O(n * 2^n)
*/
public static List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); // ★ 必须先排序!
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}
private static void backtrack(List<List<Integer>> result, List<Integer> path,
int[] nums, int startIndex) {
result.add(new ArrayList<>(path));
for (int i = startIndex; i < nums.length; i++) {
// ★ 剪枝去重:同一层中,如果当前元素和前一个相同,跳过
if (i > startIndex && nums[i] == nums[i – 1]) {
continue;
}
path.add(nums[i]);
backtrack(result, path, nums, i + 1);
path.remove(path.size() – 1);
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 2};
System.out.println("去重子集: " + subsetsWithDup(nums));
// 输出: [[], [1], [1,2], [1,2,2], [2], [2,2]]
}
}
4.4 全排列
import java.util.*;
public class Permutations {
/**
* 全排列 – 回溯算法
* 时间复杂度:O(n * n!)
* 空间复杂度:O(n)
*/
public static List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
boolean[] used = new boolean[nums.length]; // 标记元素是否已使用
backtrack(result, new ArrayList<>(), nums, used);
return result;
}
private static void backtrack(List<List<Integer>> result, List<Integer> path,
int[] nums, boolean[] used) {
// 终止条件:路径长度等于数组长度
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue; // 已使用的元素跳过
used[i] = true; // 做选择
path.add(nums[i]);
backtrack(result, path, nums, used);
path.remove(path.size() – 1); // 撤销选择
used[i] = false; // 撤销标记
}
}
/**
* 全排列II – 含重复元素,去重
*/
public static List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums); // ★ 先排序
List<List<Integer>> result = new ArrayList<>();
boolean[] used = new boolean[nums.length];
backtrackUnique(result, new ArrayList<>(), nums, used);
return result;
}
private static void backtrackUnique(List<List<Integer>> result, List<Integer> path,
int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
// ★ 去重剪枝:同一层中,前一个相同元素未被使用时跳过
if (i > 0 && nums[i] == nums[i – 1] && !used[i – 1]) {
continue;
}
used[i] = true;
path.add(nums[i]);
backtrackUnique(result, path, nums, used);
path.remove(path.size() – 1);
used[i] = false;
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
System.out.println("全排列: " + permute(nums));
// 输出: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
int[] nums2 = {1, 1, 2};
System.out.println("去重全排列: " + permuteUnique(nums2));
// 输出: [[1,1,2], [1,2,1], [2,1,1]]
}
}
4.5 N皇后(经典大题)
题目描述:在 n×n 的棋盘上放置 n 个皇后,使得它们互不攻击(不在同一行、同一列、同一对角线)。
import java.util.*;
public class NQueens {
private static int solutionCount = 0;
/**
* N皇后 – 回溯算法
* 时间复杂度:O(n!)
* 空间复杂度:O(n)
*/
public static List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
char[][] board = new char[n][n];
for (char[] row : board) {
Arrays.fill(row, '.');
}
backtrack(result, board, 0, n);
return result;
}
private static void backtrack(List<List<String>> result, char[][] board, int row, int n) {
if (row == n) {
// 找到一种解法
List<String> solution = new ArrayList<>();
for (char[] r : board) {
solution.add(new String(r));
}
result.add(solution);
solutionCount++;
return;
}
for (int col = 0; col < n; col++) {
if (isValid(board, row, col, n)) {
board[row][col] = 'Q'; // 放置皇后
backtrack(result, board, row + 1, n);
board[row][col] = '.'; // 撤销放置(回溯)
}
}
}
/**
* 检查在 (row, col) 位置放置皇后是否合法
* 只需要检查上方(因为下方还没放)
*/
private static boolean isValid(char[][] board, int row, int col, int n) {
// 检查列
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') return false;
}
// 检查左上对角线
for (int i = row – 1, j = col – 1; i >= 0 && j >= 0; i—, j—) {
if (board[i][j] == 'Q') return false;
}
// 检查右上对角线
for (int i = row – 1, j = col + 1; i >= 0 && j < n; i—, j++) {
if (board[i][j] == 'Q') return false;
}
return true;
}
/**
* 简化版:只求N皇后的解法数量(考试更常考)
*/
public static int totalNQueens(int n) {
solutionCount = 0;
boolean[] cols = new boolean[n]; // 列是否被占用
boolean[] diag1 = new boolean[2 * n]; // 主对角线 (row – col + n)
boolean[] diag2 = new boolean[2 * n]; // 副对角线 (row + col)
backtrackCount(0, n, cols, diag1, diag2);
return solutionCount;
}
private static void backtrackCount(int row, int n, boolean[] cols,
boolean[] diag1, boolean[] diag2) {
if (row == n) {
solutionCount++;
return;
}
for (int col = 0; col < n; col++) {
int d1 = row – col + n;
int d2 = row + col;
if (cols[col] || diag1[d1] || diag2[d2]) continue; // 剪枝
cols[col] = true; diag1[d1] = true; diag2[d2] = true;
backtrackCount(row + 1, n, cols, diag1, diag2);
cols[col] = false; diag1[d1] = false; diag2[d2] = false; // 回溯
}
}
public static void main(String[] args) {
int n = 4;
System.out.println(n + "皇后的解法数量: " + totalNQueens(n));
// 输出: 2
List<List<String>> solutions = solveNQueens(n);
System.out.println("具体解法:");
for (List<String> sol : solutions) {
for (String row : sol) {
System.out.println(" " + row);
}
System.out.println();
}
}
}
4.6 组合求和
import java.util.*;
public class CombinationSum {
/**
* 组合求和 – 元素可重复选
* 给定候选数组和目标值,找出所有和为target的组合(元素可重复使用)
* 时间复杂度:取决于解的数量
*/
public static List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates); // 排序有利于剪枝
backtrack(result, new ArrayList<>(), candidates, target, 0);
return result;
}
private static void backtrack(List<List<Integer>> result, List<Integer> path,
int[] candidates, int remain, int startIndex) {
if (remain < 0) return; // 剪枝
if (remain == 0) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
// 剪枝优化:如果当前元素已经大于剩余值,后面更大的也不用试了
if (candidates[i] > remain) break;
path.add(candidates[i]);
// 注意:startIndex传i(不是i+1),因为元素可以重复选
backtrack(result, path, candidates, remain – candidates[i], i);
path.remove(path.size() – 1);
}
}
/**
* 组合求和II – 元素不可重复选,数组可能有重复元素
*/
public static List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> result = new ArrayList<>();
backtrack2(result, new ArrayList<>(), candidates, target, 0);
return result;
}
private static void backtrack2(List<List<Integer>> result, List<Integer> path,
int[] candidates, int remain, int startIndex) {
if (remain == 0) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
if (candidates[i] > remain) break; // 剪枝
// 去重:同一层中跳过重复元素
if (i > startIndex && candidates[i] == candidates[i – 1]) continue;
path.add(candidates[i]);
backtrack2(result, path, candidates, remain – candidates[i], i + 1); // i+1: 不可重复选
path.remove(path.size() – 1);
}
}
public static void main(String[] args) {
int[] candidates = {2, 3, 6, 7};
System.out.println("组合求和(target=7): " + combinationSum(candidates, 7));
// 输出: [[2,2,3], [7]]
int[] candidates2 = {10, 1, 2, 7, 6, 1, 5};
System.out.println("组合求和II(target=8): " + combinationSum2(candidates2, 8));
// 输出: [[1,1,6], [1,2,5], [1,7], [2,6]]
}
}
回溯部分小结:回溯算法是期末考试中非常高频的代码题。记住通用模板(做选择→递归→撤销选择),注意结果集中要 new ArrayList<>(path) 做深拷贝。去重剪枝是加分项,先排序再去重是固定套路。
五、图算法(代码大题,中高概率)
5.1 图的存储
import java.util.*;
public class GraphRepresentation {
/**
* 邻接矩阵表示(适合稠密图)
* adjMatrix[i][j] = 1 表示存在边 i->j(有权图则存权重)
*/
public static int[][] createAdjMatrix(int n, int[][] edges) {
int[][] adjMatrix = new int[n][n];
for (int[] edge : edges) {
adjMatrix[edge[0]][edge[1]] = 1;
// adjMatrix[edge[1]][edge[0]] = 1; // 无向图需要双向设置
}
return adjMatrix;
}
/**
* 邻接表表示(适合稀疏图,Java中推荐)
* 使用 List<List<Integer>> 实现
*/
public static List<List<Integer>> createAdjList(int n, int[][] edges) {
List<List<Integer>> adjList = new ArrayList<>();
for (int i = 0; i < n; i++) {
adjList.add(new ArrayList<>());
}
for (int[] edge : edges) {
adjList.get(edge[0]).add(edge[1]);
// adjList.get(edge[1]).add(edge[0]); // 无向图
}
return adjList;
}
}
5.2 DFS和BFS遍历
import java.util.*;
public class GraphTraversal {
/**
* DFS深度优先遍历(递归版)
* 时间复杂度:O(V + E)
* 空间复杂度:O(V)
*/
public static void dfs(List<List<Integer>> adj, int node, boolean[] visited, List<Integer> result) {
visited[node] = true;
result.add(node);
for (int neighbor : adj.get(node)) {
if (!visited[neighbor]) {
dfs(adj, neighbor, visited, result);
}
}
}
/**
* BFS广度优先遍历
* 时间复杂度:O(V + E)
* 空间复杂度:O(V)
*/
public static List<Integer> bfs(List<List<Integer>> adj, int start) {
List<Integer> result = new ArrayList<>();
boolean[] visited = new boolean[adj.size()];
Queue<Integer> queue = new LinkedList<>();
visited[start] = true;
queue.offer(start);
while (!queue.isEmpty()) {
int node = queue.poll();
result.add(node);
for (int neighbor : adj.get(node)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.offer(neighbor);
}
}
}
return result;
}
/**
* 求连通分量数量(DFS应用)
*/
public static int countComponents(int n, List<List<Integer>> adj) {
boolean[] visited = new boolean[n];
int count = 0;
for (int i = 0; i < n; i++) {
if (!visited[i]) {
dfs(adj, i, visited, new ArrayList<>());
count++;
}
}
return count;
}
public static void main(String[] args) {
int n = 5;
int[][] edges = {{0,1}, {0,2}, {1,3}, {2,4}};
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
List<Integer> dfsResult = new ArrayList<>();
dfs(adj, 0, new boolean[n], dfsResult);
System.out.println("DFS遍历: " + dfsResult); // [0, 1, 3, 2, 4]
System.out.println("BFS遍历: " + bfs(adj, 0)); // [0, 1, 2, 3, 4]
}
}
5.3 Dijkstra最短路径
import java.util.*;
public class Dijkstra {
/**
* Dijkstra单源最短路径(优先队列优化版)
* 适用条件:无负权边
*
* 时间复杂度:O((V + E) log V)(使用优先队列)
* 空间复杂度:O(V + E)
*/
public static int[] dijkstra(List<List<int[]>> adj, int start) {
int n = adj.size();
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
// 优先队列:{距离, 节点},按距离升序
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] – b[0]);
pq.offer(new int[]{0, start});
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int d = curr[0], u = curr[1];
if (d > dist[u]) continue; // 已经找到更短路径,跳过
for (int[] edge : adj.get(u)) {
int v = edge[0], weight = edge[1];
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.offer(new int[]{dist[v], v});
}
}
}
return dist;
}
/**
* Dijkstra – 邻接矩阵版本(适合稠密图,考试更常考)
* 时间复杂度:O(V²)
* 空间复杂度:O(V)
*/
public static int[] dijkstraMatrix(int[][] graph, int start) {
int n = graph.length;
int[] dist = new int[n];
boolean[] visited = new boolean[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
for (int count = 0; count < n; count++) {
// 找未访问中距离最小的节点
int u = –1;
int minDist = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
if (!visited[i] && dist[i] < minDist) {
minDist = dist[i];
u = i;
}
}
if (u == –1) break;
visited[u] = true;
// 更新邻居距离
for (int v = 0; v < n; v++) {
if (!visited[v] && graph[u][v] != 0 &&
dist[u] + graph[u][v] < dist[v]) {
dist[v] = dist[u] + graph[u][v];
}
}
}
return dist;
}
public static void main(String[] args) {
// 邻接矩阵:0表示无边
int[][] graph = {
{0, 4, 0, 0, 0, 0, 0, 8, 0},
{4, 0, 8, 0, 0, 0, 0, 11, 0},
{0, 8, 0, 7, 0, 4, 0, 0, 2},
{0, 0, 7, 0, 9, 14, 0, 0, 0},
{0, 0, 0, 9, 0, 10, 0, 0, 0},
{0, 0, 4, 14, 10, 0, 2, 0, 0},
{0, 0, 0, 0, 0, 2, 0, 1, 6},
{8, 11, 0, 0, 0, 0, 1, 0, 7},
{0, 0, 2, 0, 0, 0, 6, 7, 0}
};
int[] dist = dijkstraMatrix(graph, 0);
System.out.println("从节点0出发的最短距离:");
for (int i = 0; i < dist.length; i++) {
System.out.println(" 到节点" + i + ": " + dist[i]);
}
}
}
5.4 Floyd多源最短路径
public class Floyd {
/**
* Floyd-Warshall 多源最短路径
* 核心:三重循环,枚举中间节点k
*
* 状态转移:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
*
* 时间复杂度:O(V³)
* 空间复杂度:O(V²)
*
* 特点:代码极短,期末非常爱考!
*/
public static int[][] floyd(int[][] graph) {
int n = graph.length;
int[][] dist = new int[n][n];
// 初始化dist数组
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dist[i][j] = graph[i][j];
}
}
// ★★★ Floyd核心:三重循环,k必须在最外层!★★★
for (int k = 0; k < n; k++) { // 中间节点
for (int i = 0; i < n; i++) { // 起点
for (int j = 0; j < n; j++) { // 终点
if (dist[i][k] != Integer.MAX_VALUE &&
dist[k][j] != Integer.MAX_VALUE &&
dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
return dist;
}
public static void main(String[] args) {
int INF = Integer.MAX_VALUE;
int[][] graph = {
{0, 3, INF, 5 },
{2, 0, INF, 4 },
{INF, 1, 0, INF},
{INF, INF, 2, 0 }
};
int[][] dist = floyd(graph);
System.out.println("Floyd最短路径矩阵:");
for (int i = 0; i < dist.length; i++) {
for (int j = 0; j < dist.length; j++) {
System.out.printf("%6s", dist[i][j] == INF ? "INF" : dist[i][j]);
}
System.out.println();
}
}
}
⚠️ Floyd考试坑点:中间节点 k 必须在最外层循环!如果 k 放在最内层,结果是错误的。这是期末考试中最容易犯的错误。
5.5 Prim最小生成树
public class Prim {
/**
* Prim最小生成树(邻接矩阵版本,适合稠密图)
* 时间复杂度:O(V²)
* 空间复杂度:O(V)
*/
public static int prim(int[][] graph) {
int n = graph.length;
boolean[] inMST = new boolean[n];
int[] key = new int[n]; // 到MST的最小边权
Arrays.fill(key, Integer.MAX_VALUE);
key[0] = 0; // 从节点0开始
int totalWeight = 0;
for (int count = 0; count < n; count++) {
// 找不在MST中key值最小的节点
int u = –1;
int minKey = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
if (!inMST[i] && key[i] < minKey) {
minKey = key[i];
u = i;
}
}
inMST[u] = true;
totalWeight += key[u];
// 更新邻居的key值
for (int v = 0; v < n; v++) {
if (graph[u][v] != 0 && !inMST[v] && graph[u][v] < key[v]) {
key[v] = graph[u][v];
}
}
}
return totalWeight;
}
public static void main(String[] args) {
int[][] graph = {
{0, 2, 0, 6, 0},
{2, 0, 3, 8, 5},
{0, 3, 0, 0, 7},
{6, 8, 0, 0, 9},
{0, 5, 7, 9, 0}
};
System.out.println("Prim最小生成树总权值: " + prim(graph));
// 输出: 16 (边: 0-1(2), 1-2(3), 0-3(6), 1-4(5))
}
}
5.6 Kruskal最小生成树 + 并查集
import java.util.*;
public class Kruskal {
/**
* 并查集(Union-Find)—— 独立考点!
*/
static class UnionFind {
int[] parent;
int[] rank; // 按秩合并
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i; // 初始时每个节点是自己的根
}
}
/**
* 查找根节点 + 路径压缩
* 时间复杂度:接近O(1)(摊还分析)
*/
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
/**
* 合并两个集合 + 按秩合并
*/
boolean union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return false; // 已在同一集合
// 按秩合并
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
return true;
}
}
/**
* Kruskal最小生成树
* 思路:边按权值排序,从小到大选边,用并查集判断是否成环
* 时间复杂度:O(E log E)
* 空间复杂度:O(V + E)
*/
public static int kruskal(int n, int[][] edges) {
// edges: {u, v, weight}
Arrays.sort(edges, (a, b) -> a[2] – b[2]); // 按权值升序排序
UnionFind uf = new UnionFind(n);
int totalWeight = 0;
int edgeCount = 0;
for (int[] edge : edges) {
int u = edge[0], v = edge[1], w = edge[2];
if (uf.union(u, v)) { // 不成环,加入MST
totalWeight += w;
edgeCount++;
if (edgeCount == n – 1) break; // MST有n-1条边
}
}
return totalWeight;
}
public static void main(String[] args) {
// 边: {u, v, weight}
int[][] edges = {
{0, 1, 2}, {0, 3, 6}, {1, 2, 3}, {1, 3, 8},
{1, 4, 5}, {2, 4, 7}, {3, 4, 9}
};
System.out.println("Kruskal最小生成树总权值: " + kruskal(5, edges));
// 输出: 16
}
}
5.7 拓扑排序(判断有向图是否有环)
import java.util.*;
public class TopologicalSort {
/**
* 拓扑排序 – BFS(Kahn算法)
* 思路:维护入度数组,每次取入度为0的节点
*
* 时间复杂度:O(V + E)
* 空间复杂度:O(V + E)
*
* 应用:判断有向图是否有环(如果排序结果包含所有节点则无环)
*/
public static List<Integer> topologicalSort(int n, List<List<Integer>> adj) {
int[] inDegree = new int[n]; // 入度数组
// 计算每个节点的入度
for (int u = 0; u < n; u++) {
for (int v : adj.get(u)) {
inDegree[v]++;
}
}
// 将所有入度为0的节点入队
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
List<Integer> result = new ArrayList<>();
while (!queue.isEmpty()) {
int u = queue.poll();
result.add(u);
for (int v : adj.get(u)) {
inDegree[v]—;
if (inDegree[v] == 0) {
queue.offer(v);
}
}
}
// 如果结果数量不等于节点数,说明有环
if (result.size() != n) {
System.out.println("图中存在环!无法完成拓扑排序");
return new ArrayList<>();
}
return result;
}
/**
* 判断有向图是否有环(拓扑排序应用)
*/
public static boolean hasCycle(int n, List<List<Integer>> adj) {
return topologicalSort(n, adj).isEmpty() && n > 0;
}
public static void main(String[] args) {
int n = 6;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
// 有向边
adj.get(5).add(2); adj.get(5).add(0);
adj.get(4).add(0); adj.get(4).add(1);
adj.get(2).add(3); adj.get(3).add(1);
List<Integer> order = topologicalSort(n, adj);
System.out.println("拓扑排序: " + order);
// 可能的输出: [4, 5, 2, 0, 3, 1]
}
}
图算法部分小结:图算法在期末考试中属于大题级别,Dijkstra和Floyd是最高频考点。并查集经常作为独立小代码题出现(5分左右)。写Floyd时记住 k在最外层,写Dijkstra时注意优先队列的使用。拓扑排序常和"判断有环"结合出题。
六、查找与排序(基础小题)
6.1 堆排序
public class HeapSort {
/**
* 堆排序(大根堆,升序排列)
* 时间复杂度:O(n log n)
* 空间复杂度:O(1)(原地排序)
*/
public static void heapSort(int[] arr) {
int n = arr.length;
// 第一步:建堆(从最后一个非叶子节点开始)
for (int i = n / 2 – 1; i >= 0; i—) {
heapify(arr, n, i);
}
// 第二步:依次取出堆顶元素(最大值),放到数组末尾
for (int i = n – 1; i > 0; i—) {
// 交换堆顶和最后一个元素
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对新的堆顶做heapify(注意堆的大小变为i)
heapify(arr, i, 0);
}
}
/**
* heapify:将以i为根的子树调整为大根堆
* @param arr 数组
* @param heapSize 当前堆的大小
* @param i 当前节点下标
*/
private static void heapify(int[] arr, int heapSize, int i) {
int largest = i; // 假设当前节点最大
int left = 2 * i + 1; // 左孩子
int right = 2 * i + 2; // 右孩子
if (left < heapSize && arr[left] > arr[largest]) {
largest = left;
}
if (right < heapSize && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
// 交换并继续heapify
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, heapSize, largest);
}
}
public static void main(String[] args) {
int[] arr = {4, 10, 3, 5, 1};
heapSort(arr);
System.out.println("堆排序结果: " + java.util.Arrays.toString(arr));
// 输出: [1, 3, 4, 5, 10]
}
}
排序算法复杂度对比表(考试必背):
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
七、字符串算法(KMP)
7.1 KMP字符串匹配
public class KMP {
/**
* 构建next数组(部分匹配表 / 失配指针)
* next[i] = 模式串前i+1个字符组成的子串中,
* 最长相等前后缀的长度
* 时间复杂度:O(m),m为模式串长度
*/
public static int[] buildNext(String pattern) {
int m = pattern.length();
int[] next = new int[m];
next[0] = 0;
int len = 0; // 当前最长相等前后缀长度
int i = 1;
while (i < m) {
if (pattern.charAt(i) == pattern.charAt(len)) {
len++;
next[i] = len;
i++;
} else {
if (len != 0) {
len = next[len – 1]; // 回退
} else {
next[i] = 0;
i++;
}
}
}
return next;
}
/**
* KMP字符串匹配
* 时间复杂度:O(n + m),n为文本串长度,m为模式串长度
* 空间复杂度:O(m)(next数组)
*/
public static List<Integer> kmpSearch(String text, String pattern) {
List<Integer> result = new ArrayList<>();
if (pattern.isEmpty()) return result;
int n = text.length(), m = pattern.length();
int[] next = buildNext(pattern);
int i = 0; // 文本串指针
int j = 0; // 模式串指针
while (i < n) {
if (text.charAt(i) == pattern.charAt(j)) {
i++;
j++;
}
if (j == m) {
// 找到匹配,记录起始位置
result.add(i – j);
j = next[j – 1]; // 继续搜索
} else if (i < n && text.charAt(i) != pattern.charAt(j)) {
if (j != 0) {
j = next[j – 1]; // 利用next数组回退
} else {
i++;
}
}
}
return result;
}
public static void main(String[] args) {
String text = "ABABDABACDABABCABAB";
String pattern = "ABABCABAB";
List<Integer> positions = kmpSearch(text, pattern);
System.out.println("模式串在文本串中的起始位置: " + positions);
// 输出: [9]
System.out.println("next数组: " + java.util.Arrays.toString(buildNext(pattern)));
// 输出: [0, 0, 1, 2, 0, 1, 2, 3, 4]
}
}
八、期末出题套路与应试技巧
8.1 出题套路总结
| 基础 | 5分 | 二分查找、并查集、归并merge、快排partition | 写核心函数 |
| 中档 | 8-12分 | 01背包、LCS、回溯全排列、Dijkstra、Floyd | 写完整方法/类 |
| 拔高 | 10+分 | N皇后、逆序对、编辑距离、拓扑排序判环 | 完整代码+分析 |
8.2 应试技巧
技巧一:代码结构清晰
考试写代码时,建议按照以下结构:
/**
* 方法说明
* 时间复杂度:O(?)
* 空间复杂度:O(?)
*/
public static returnType methodName(parameters) {
// 1. 边界处理
if (input == null || input.length == 0) return defaultValue;
// 2. 初始化
// 3. 核心逻辑
// 4. 返回结果
}
技巧二:先写框架再补细节
拿到题目后,先写出代码框架(方法签名、变量定义、循环结构),再填充具体逻辑。即使最后没时间写完,框架分也能拿到。
技巧三:边界条件一定要处理
以下边界条件在考试中必须检查:
- 数组为 null 或长度为0
- 字符串为空
- 数组只有1个元素
- n=0 或 n=1 的特殊情况
技巧四:复杂度分析必写
写完代码后,一定要在代码旁边标注时间复杂度和空间复杂度,通常占1-2分。格式如下:
// 时间复杂度:O(n²),两层循环
// 空间复杂度:O(n),dp数组
技巧五:不会写就写暴力
如果实在想不出最优解,先写暴力解法(如递归枚举),至少能拿到部分分数。然后再尝试优化。
8.3 常见扣分点汇总
九、复习优先级与总结
9.1 性价比复习顺序
按照考试出题频率和分值,推荐的复习顺序如下(从高到低):
| ★★★ | 01背包(DP) | 10-12分 | 中 | 2小时 |
| ★★★ | LCS最长公共子序列 | 8-10分 | 中 | 1.5小时 |
| ★★★ | 回溯全排列/子集 | 8-10分 | 中 | 1.5小时 |
| ★★☆ | 二分查找及变体 | 5-8分 | 低 | 1小时 |
| ★★☆ | 归并排序求逆序对 | 8-10分 | 中 | 1.5小时 |
| ★★☆ | Dijkstra最短路径 | 8-10分 | 中高 | 1.5小时 |
| ★★☆ | Floyd多源最短路径 | 8-10分 | 低 | 1小时 |
| ★★☆ | 并查集 | 5-8分 | 低 | 1小时 |
| ★☆☆ | N皇后 | 10-12分 | 高 | 1.5小时 |
| ★☆☆ | 活动选择(贪心) | 5-8分 | 低 | 0.5小时 |
| ★☆☆ | KMP字符串匹配 | 5-8分 | 高 | 1小时 |
9.2 各算法复杂度速查表
| 二分查找 | O(log n) | O(1) | 分治 |
| 归并排序 | O(n log n) | O(n) | 分治 |
| 快速排序 | O(n log n) | O(log n) | 分治 |
| 堆排序 | O(n log n) | O(1) | 排序 |
| 01背包 | O(nW) | O(nW)或O(W) | DP |
| LCS | O(mn) | O(mn) | DP |
| 编辑距离 | O(mn) | O(mn) | DP |
| 全排列 | O(n·n!) | O(n) | 回溯 |
| N皇后 | O(n!) | O(n) | 回溯 |
| Dijkstra | O((V+E)logV) | O(V+E) | 贪心+图 |
| Floyd | O(V³) | O(V²) | DP+图 |
| Kruskal | O(ElogE) | O(V+E) | 贪心+并查集 |
| 拓扑排序 | O(V+E) | O(V+E) | BFS+图 |
| KMP | O(n+m) | O(m) | 字符串 |
9.3 考前一天速览清单
如果只剩一天复习时间,请重点掌握以下内容:
9.4 最后的叮嘱
附录:快速测试main方法汇总
为了方便大家快速测试所有代码,这里提供一个统一的测试入口:
public class AlgorithmExamTest {
public static void main(String[] args) {
System.out.println("====== 期末考试算法测试 ======\\n");
// 1. 二分查找
System.out.println("— 二分查找 —");
int[] sortedArr = {1, 3, 5, 7, 9, 11};
// binarySearch(sortedArr, 7) -> 3
// 2. 归并排序求逆序对
System.out.println("— 逆序对 —");
int[] arr = {7, 5, 6, 4};
// countInversions -> 5
// 3. 01背包
System.out.println("— 01背包 —");
int[] weights = {2, 3, 4, 5};
int[] values = {3, 4, 5, 6};
// knapsack01(weights, values, 8) -> 10
// 4. LCS
System.out.println("— LCS —");
// lcsLength("ABCBDAB", "BDCAB") -> 4
// 5. 编辑距离
System.out.println("— 编辑距离 —");
// minDistance("horse", "ros") -> 3
System.out.println("\\n====== 测试完成 ======");
}
}
本文到此结束。如果你觉得对你有帮助,请点赞、收藏、关注三连!期末加油,祝你算法考试顺利拿高分!💪
版权声明:本文为原创整理,转载请注明出处。代码均为手写验证,如有bug欢迎评论区指出。
标签:#Java #算法分析与设计 #期末考试 #动态规划 #回溯算法 #图算法 #分治 #数据结构
网硕互联帮助中心





评论前必须登录!
注册