云计算百科
云计算领域专业知识百科平台

Java版《算法分析与设计》期末代码题:全部考点+题型总结(万字详解,附完整代码)

Java版《算法分析与设计》期末代码题:全部考点+题型总结(万字详解,附完整代码)


适用对象:本科计算机专业期末考试复习 侧重点:手写Java代码,不考复杂底层证明 核心考点:递归、分治、贪心、动态规划、回溯、图算法、查找排序 分值分布:代码题一般6~10分一题,要求写方法/完整类、输出结果、分析复杂度 本文特色:每个考点均附完整可运行Java代码 + 复杂度分析 + 考试坑点提醒


目录

  • 前言:期末考试到底考什么?
  • 一、分治算法(必考高频)
  • 二、贪心算法(中等频率)
  • 三、动态规划(期末代码大题TOP1)
  • 四、回溯算法(非常高频代码题)
  • 五、图算法(代码大题,中高概率)
  • 六、查找与排序(基础小题)
  • 七、字符串算法(KMP)
  • 八、期末出题套路与应试技巧
  • 九、复习优先级与总结

  • 前言:期末考试到底考什么?

    每到期末,算法分析与设计这门课总是让无数计算机专业的同学头疼。不同于数据结构侧重"会用",算法课更强调 理解思想 和 手写代码 的能力。

    根据多年期末考试的经验总结,算法分析与设计的期末代码题有以下规律:

    • 基础小题(5分左右):二分查找、并查集、归并merge函数、快排partition函数——写核心函数即可
    • 中档大题(8~12分):01背包、最长公共子序列(LCS)、回溯全排列、Dijkstra最短路径、Floyd多源最短路径
    • 拔高题(10分以上):N皇后、归并排序求逆序对、编辑距离、拓扑排序判环

    考试写Java代码的通用要求:

  • 优先写迭代版本DP,递归容易栈溢出、超时
  • 方法尽量独立,不要依赖额外复杂包,java.util.* 允许导入
  • 写完最好标注时间复杂度和空间复杂度(通常有1~2分)
  • 注意边界条件:空数组、长度为1的数组、空字符串
  • 下面我们就按照考点逐一展开,每个考点都给出完整可运行的Java代码、详细的思路分析、复杂度计算以及考试中容易踩的坑。


    一、分治算法(必考高频)

    1.1 核心思想

    分治(Divide and Conquer)的核心思想可以用三步概括:

  • 分解(Divide):将原问题分解为若干个规模更小的子问题
  • 求解(Conquer):递归地求解各个子问题
  • 合并(Combine):将子问题的解合并为原问题的解
  • 时间复杂度分析的核心工具是主定理(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(nlogb​a−ϵ),则

      T

      (

      n

      )

      =

      Θ

      (

      n

      log

      ⁡

      b

      a

      )

      T(n) = \\Theta(n^{\\log_b a})

      T(n)=Θ(nlogb​a)

    • 若

      f

      (

      n

      )

      =

      Θ

      (

      n

      log

      ⁡

      b

      a

      )

      f(n) = \\Theta(n^{\\log_b a})

      f(n)=Θ(nlogb​a),则

      T

      (

      n

      )

      =

      Θ

      (

      n

      log

      ⁡

      b

      a

      log

      ⁡

      n

      )

      T(n) = \\Theta(n^{\\log_b a} \\log n)

      T(n)=Θ(nlogb​alogn)

    • 若

      f

      (

      n

      )

      =

      Ω

      (

      n

      log

      ⁡

      b

      a

      +

      ϵ

      )

      f(n) = \\Omega(n^{\\log_b a + \\epsilon})

      f(n)=Ω(nlogb​a+ϵ),则

      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
    }
    }

    ⚠️ 考试坑点:

  • mid = left + (right – left) / 2 防止 left + right 整型溢出,写这个老师会加分
  • 循环条件是 left <= right 不是 left < right
  • "查找第一个/最后一个等于target"是期末高频变体,注意找到后不能直接return,要继续搜索
  • 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
    }
    }

    ⚠️ 考试坑点:

  • count += (mid – i + 1) 这个公式必须记住,是左半部分剩余元素个数
  • 全局变量 count 每次使用前要重置为0
  • 如果考试要求返回 long 类型(防溢出),注意修改
  • 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解题四步法:

  • 状态定义:dp[i] 或 dp[i][j] 代表什么含义
  • 状态转移方程:dp[i] 如何从前面的状态推导出来
  • 初始化:base case,最小子问题的解
  • 填表顺序:一维从左到右,二维从左到右、从上到下
  • 考试关键提醒:

    • 必须写迭代版本,不要写递归(递归会超时,而且老师会扣分)
    • 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 常见扣分点汇总

  • 01背包循环顺序写错(逆序写成正序)→ 扣2-3分
  • Floyd的k循环不在最外层 → 结果全错,扣大量分
  • 二分查找的 mid 计算溢出 → 扣1分
  • 回溯忘记撤销选择 → 结果全错
  • DP数组初始化错误 → 结果全错
  • 递归没有终止条件 → 栈溢出,0分
  • 图的DFS/BFS忘记标记visited → 死循环
  • 并查集忘记路径压缩 → 可能超时

  • 九、复习优先级与总结

    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 考前一天速览清单

    如果只剩一天复习时间,请重点掌握以下内容:

  • 手写01背包(二维DP版本 + 一维优化版本,记住逆序遍历)
  • 手写LCS(状态转移方程 + 回溯输出子序列)
  • 手写回溯全排列(通用模板 + 去重版本)
  • 手写二分查找(基础版 + 查找第一个/最后一个)
  • 手写归并排序 + 求逆序对
  • 手写Dijkstra(邻接矩阵版本)
  • 手写Floyd(记住k在最外层)
  • 手写并查集(find + 路径压缩 + union + 按秩合并)
  • 背诵排序算法复杂度对比表
  • 了解N皇后的回溯写法
  • 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 #算法分析与设计 #期末考试 #动态规划 #回溯算法 #图算法 #分治 #数据结构

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java版《算法分析与设计》期末代码题:全部考点+题型总结(万字详解,附完整代码)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!