贪心算法深度解析:从理论到实战的完整指南

贪心算法深度解析:从理论到实战的完整指南

1. 贪心算法概述

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的算法策略。与动态规划不同,贪心算法不会回溯之前的决策,而是基于当前状态做出最优判断。

核心特点:

  • 局部最优选择:每一步都选择当前最优解
  • 无后效性:当前决策不会影响后续决策
  • 高效性:通常时间复杂度较低
  • 简洁性:算法逻辑清晰,代码实现简单

2. 贪心算法的理论基础

2.1 贪心算法的数学基础

贪心算法的有效性建立在严格的数学基础之上,主要包括以下两个关键性质:

贪心选择性质

问题的整体最优解可以通过一系列局部最优选择达到。这意味着我们不需要考虑所有可能的解,只需要在每个步骤中选择当前最优的选项。

最优子结构

问题的最优解包含其子问题的最优解。这个性质与动态规划相同,是贪心算法能够正确解决问题的前提。

2.2 贪心算法的证明方法

要证明贪心算法的正确性,通常使用以下方法:

  1. 贪心选择性质证明:证明每一步的贪心选择都是安全的
  2. 数学归纳法:通过归纳证明算法的正确性
  3. 交换论证:通过交换解中的元素来证明没有更好的解
  4. 拟阵理论:利用拟阵的性质来证明

3. 贪心算法的一般步骤

  1. 建立数学模型:将问题抽象为数学形式
  2. 分解子问题:把求解的问题分成若干个子问题
  3. 贪心选择:对每个子问题求解,得到子问题的局部最优解
  4. 合并解:把子问题的解合并成原问题的一个解
  5. 验证正确性:证明贪心策略能够得到全局最优解

4. 经典贪心算法问题及Java实现

4.1 零钱兑换问题

问题描述:给定不同面额的硬币和一个总金额,计算可以凑成总金额所需的最少的硬币个数。
import java.util.Arrays;

/**

* 零钱兑换问题的贪心算法实现

* 注意:贪心算法在零钱兑换问题中并不总是能得到最优解

* 只有在硬币面额满足特定条件时才适用

*/

public class CoinChange {

/**

* 零钱兑换的贪心算法实现

* @param coins 可用的硬币面额

* @param amount 目标金额

* @return 最少硬币数量,如果无法凑出返回-1

* @throws IllegalArgumentException 当参数不合法时抛出异常

*/

public static int coinChangeGreedy(int[] coins, int amount) {

// 参数校验

if (coins == null || coins.length == 0) {

throw new IllegalArgumentException("硬币数组不能为空");

}

if (amount < 0) {

throw new IllegalArgumentException("金额不能为负数");

}

if (amount == 0) {

return 0;

}

// 将硬币按面额从大到小排序

Arrays.sort(coins);

int count = 0;

int remaining = amount;

// 从最大面额开始尝试

for (int i = coins.length - 1; i >= 0; i--) {

if (coins[i] <= 0) {

throw new IllegalArgumentException("硬币面额必须为正数");

}

if (remaining >= coins[i]) {

int numCoins = remaining / coins[i];

count += numCoins;

remaining %= coins[i];

}

if (remaining == 0) {

break;

}

}

return remaining == 0 ? count : -1;

}

/**

* 验证贪心算法是否适用于给定的硬币系统

* 只有当硬币面额是倍数关系时,贪心算法才能保证最优解

*/

public static boolean isGreedyOptimal(int[] coins) {

Arrays.sort(coins);

for (int i = 1; i < coins.length; i++) {

if (coins[i] % coins[i - 1] != 0) {

return false;

}

}

return true;

}

public static void main(String[] args) {

// 测试用例1:标准情况

int[] coins1 = {1, 2, 5};

int amount1 = 11;

System.out.println("硬币系统: " + Arrays.toString(coins1));

System.out.println("贪心算法是否最优: " + isGreedyOptimal(coins1));

System.out.println("金额 " + amount1 + " 最少需要硬币: " +

coinChangeGreedy(coins1, amount1));

// 测试用例2:无法凑出的情况

int[] coins2 = {2};

int amount2 = 3;

System.out.println("\n硬币系统: " + Arrays.toString(coins2));

System.out.println("金额 " + amount2 + " 最少需要硬币: " +

coinChangeGreedy(coins2, amount2));

// 测试用例3:贪心算法不是最优的情况

int[] coins3 = {1, 3, 4};

int amount3 = 6;

System.out.println("\n硬币系统: " + Arrays.toString(coins3));

System.out.println("贪心算法是否最优: " + isGreedyOptimal(coins3));

System.out.println("贪心算法结果: " + coinChangeGreedy(coins3, amount3));

System.out.println("实际最优解: 2 (3+3)");

// 性能测试

int[] coins4 = {1, 5, 10, 25, 50, 100};

int amount4 = 378;

long startTime = System.nanoTime();

int result = coinChangeGreedy(coins4, amount4);

long endTime = System.nanoTime();

System.out.println("\n性能测试 - 金额 " + amount4 + " 结果: " + result);

System.out.println("执行时间: " + (endTime - startTime) + " 纳秒");

}

}
算法分析

  • 时间复杂度:O(n log n),主要来自排序操作
  • 空间复杂度:O(1),只使用了常数级别的额外空间
  • 适用条件:硬币面额互为倍数关系时保证最优解

4.2 区间调度问题

问题描述:给定一组区间,找到最大的不重叠区间集合。
import java.util.*;

/**

* 区间调度问题的贪心算法实现

* 经典贪心算法应用,保证得到最优解

*/

public class IntervalScheduling {

static class Interval {

int start;

int end;

String name;

public Interval(int start, int end, String name) {

if (start > end) {

throw new IllegalArgumentException("开始时间不能大于结束时间");

}

this.start = start;

this.end = end;

this.name = name;

}

public int getDuration() {

return end - start;

}

@Override

public String toString() {

return name + " [" + start + ", " + end + "]";

}

}

/**

* 使用贪心算法解决区间调度问题

* 策略:总是选择结束时间最早的区间

* @param intervals 区间数组

* @return 最大不重叠区间集合

*/

public static List<Interval> intervalScheduling(Interval[] intervals) {

if (intervals == null || intervals.length == 0) {

return new ArrayList<>();

}

// 按结束时间升序排序

Arrays.sort(intervals, Comparator.comparingInt(i -> i.end));

List<Interval> result = new ArrayList<>();

// 总是选择结束时间最早的区间

result.add(intervals[0]);

int lastEnd = intervals[0].end;

for (int i = 1; i < intervals.length; i++) {

if (intervals[i].start >= lastEnd) {

result.add(intervals[i]);

lastEnd = intervals[i].end;

}

}

return result;

}

/**

* 计算总占用时间

*/

public static int calculateTotalTime(List<Interval> intervals) {

return intervals.stream().mapToInt(Interval::getDuration).sum();

}

public static void main(String[] args) {

// 测试用例

Interval[] intervals = {

new Interval(1, 3, "会议A"),

new Interval(2, 4, "会议B"),

new Interval(3, 5, "会议C"),

new Interval(4, 6, "会议D"),

new Interval(5, 7, "会议E"),

new Interval(1, 2, "短会议"),

new Interval(6, 8, "晚会议")

};

System.out.println("所有区间:");

Arrays.stream(intervals).forEach(System.out::println);

List<Interval> result = intervalScheduling(intervals);

System.out.println("\n最大不重叠区间集合:");

result.forEach(System.out::println);

System.out.println("\n统计信息:");

System.out.println("总共选择区间数量: " + result.size());

System.out.println("总占用时间: " + calculateTotalTime(result));

// 正确性验证

System.out.println("\n正确性验证:");

for (int i = 0; i < result.size() - 1; i++) {

if (result.get(i).end > result.get(i + 1).start) {

System.out.println("错误: 区间重叠!");

break;

}

}

System.out.println("所有区间均不重叠");

}

}
算法分析

  • 时间复杂度:O(n log n),主要来自排序操作
  • 空间复杂度:O(n),存储结果需要线性空间
  • 最优性:保证得到最大不重叠区间集合

4.3 霍夫曼编码

问题描述:构建最优前缀编码,用于数据压缩。
import java.util.*;

import java.util.stream.Collectors;

/**

* 霍夫曼编码实现

* 经典贪心算法应用,用于数据压缩

*/

public class HuffmanCoding {

static class HuffmanNode implements Comparable<HuffmanNode> {

char character;

int frequency;

HuffmanNode left, right;

public HuffmanNode(char character, int frequency) {

this.character = character;

this.frequency = frequency;

}

public HuffmanNode(int frequency, HuffmanNode left, HuffmanNode right) {

this.frequency = frequency;

this.left = left;

this.right = right;

}

public boolean isLeaf() {

return left == null && right == null;

}

@Override

public int compareTo(HuffmanNode other) {

return Integer.compare(this.frequency, other.frequency);

}

}

/**

* 构建霍夫曼树

* @param text 输入文本

* @return 霍夫曼树的根节点

*/

public static HuffmanNode buildHuffmanTree(String text) {

if (text == null || text.isEmpty()) {

throw new IllegalArgumentException("输入文本不能为空");

}

// 统计字符频率

Map<Character, Integer> frequencyMap = new HashMap<>();

for (char c : text.toCharArray()) {

frequencyMap.put(c, frequencyMap.getOrDefault(c, 0) + 1);

}

// 创建优先队列(最小堆)

PriorityQueue<HuffmanNode> pq = new PriorityQueue<>();

// 为每个字符创建叶子节点

for (Map.Entry<Character, Integer> entry : frequencyMap.entrySet()) {

pq.offer(new HuffmanNode(entry.getKey(), entry.getValue()));

}

// 构建霍夫曼树:总是合并频率最小的两个节点

while (pq.size() > 1) {

HuffmanNode left = pq.poll();

HuffmanNode right = pq.poll();

HuffmanNode parent = new HuffmanNode(

left.frequency + right.frequency, left, right);

pq.offer(parent);

}

return pq.poll();

}

/**

* 生成霍夫曼编码

* @param root 霍夫曼树的根节点

* @return 字符到编码的映射

*/

public static Map<Character, String> generateCodes(HuffmanNode root) {

Map<Character, String> codes = new HashMap<>();

generateCodesHelper(root, "", codes);

return codes;

}

private static void generateCodesHelper(HuffmanNode node, String code,

Map<Character, String> codes) {

if (node == null) return;

if (node.isLeaf()) {

codes.put(node.character, code.isEmpty() ? "0" : code);

} else {

generateCodesHelper(node.left, code + "0", codes);

generateCodesHelper(node.right, code + "1", codes);

}

}

/**

* 使用霍夫曼编码压缩文本

*/

public static String encode(String text, Map<Character, String> huffmanCodes) {

StringBuilder encoded = new StringBuilder();

for (char c : text.toCharArray()) {

encoded.append(huffmanCodes.get(c));

}

return encoded.toString();

}

/**

* 计算压缩率

*/

public static double calculateCompressionRatio(String original, String encoded) {

double originalBits = original.length() * 8.0; // 假设原始是ASCII,每个字符8位

double encodedBits = encoded.length();

return (1 - encodedBits / originalBits) * 100;

}

public static void main(String[] args) {

String text = "this is an example for huffman encoding";

System.out.println("原始文本: " + text);

System.out.println("文本长度: " + text.length() + " 字符");

// 构建霍夫曼树和编码

HuffmanNode root = buildHuffmanTree(text);

Map<Character, String> huffmanCodes = generateCodes(root);

// 显示编码表

System.out.println("\n霍夫曼编码表:");

huffmanCodes.entrySet().stream()

.sorted(Map.Entry.comparingByValue())

.forEach(entry -> System.out.println("'" + entry.getKey() + "': " + entry.getValue()));

// 编码文本

String encoded = encode(text, huffmanCodes);

System.out.println("\n编码后的二进制: " + encoded);

System.out.println("编码后长度: " + encoded.length() + " 位");

// 计算压缩率

double compressionRatio = calculateCompressionRatio(text, encoded);

System.out.printf("压缩率: %.2f%%\n", compressionRatio);

// 统计分析

System.out.println("\n统计分析:");

Map<Character, Integer> frequencyMap = new HashMap<>();

for (char c : text.toCharArray()) {

frequencyMap.put(c, frequencyMap.getOrDefault(c, 0) + 1);

}

frequencyMap.entrySet().stream()

.sorted((a, b) -> Integer.compare(b.getValue(), a.getValue()))

.forEach(entry -> {

char c = entry.getKey();

int freq = entry.getValue();

String code = huffmanCodes.get(c);

System.out.printf("'%c': 频率=%d, 编码=%s, 编码长度=%d\n",

c, freq, code, code.length());

});

}

}
算法分析

  • 时间复杂度:O(n log n),构建优先队列和合并节点
  • 空间复杂度:O(n),存储字符频率和编码表
  • 最优性:保证得到最优前缀编码

4.4 最小生成树 - Prim算法

问题描述:在连通加权无向图中找到一棵包括所有顶点的树,且所有边的权值之和最小。
import java.util.*;

/**

* Prim算法实现最小生成树

* 贪心策略:每次选择连接当前树和外部顶点的最小权值边

*/

public class PrimMST {

static class Edge {

int source;

int destination;

int weight;

public Edge(int source, int destination, int weight) {

this.source = source;

this.destination = destination;

this.weight = weight;

}

@Override

public String toString() {

return source + " - " + destination + " : " + weight;

}

}

static class Graph {

int vertices;

List<List<Edge>> adjacencyList;

public Graph(int vertices) {

this.vertices = vertices;

this.adjacencyList = new ArrayList<>(vertices);

for (int i = 0; i < vertices; i++) {

adjacencyList.add(new ArrayList<>());

}

}

public void addEdge(int source, int destination, int weight) {

Edge edge1 = new Edge(source, destination, weight);

Edge edge2 = new Edge(destination, source, weight);

adjacencyList.get(source).add(edge1);

adjacencyList.get(destination).add(edge2);

}

}

/**

* Prim算法实现

* @param graph 图对象

* @return 最小生成树的边集合

*/

public static List<Edge> primMST(Graph graph) {

if (graph.vertices == 0) {

return new ArrayList<>();

}

int vertices = graph.vertices;

boolean[] inMST = new boolean[vertices];

int[] key = new int[vertices];

int[] parent = new int[vertices];

// 初始化

Arrays.fill(key, Integer.MAX_VALUE);

Arrays.fill(parent, -1);

// 使用优先队列优化

PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));

// 从顶点0开始

key[0] = 0;

pq.offer(new Edge(-1, 0, 0));

List<Edge> mst = new ArrayList<>();

while (!pq.isEmpty() && mst.size() < vertices - 1) {

Edge minEdge = pq.poll();

int u = minEdge.destination;

if (inMST[u]) continue;

inMST[u] = true;

// 添加边到MST(排除起始边)

if (minEdge.source != -1) {

mst.add(minEdge);

}

// 更新相邻顶点的key值

for (Edge edge : graph.adjacencyList.get(u)) {

int v = edge.destination;

int weight = edge.weight;

if (!inMST[v] && weight < key[v]) {

key[v] = weight;

parent[v] = u;

pq.offer(new Edge(u, v, weight));

}

}

}

return mst;

}

/**

* 计算最小生成树的总权值

*/

public static int calculateTotalWeight(List<Edge> mst) {

return mst.stream().mapToInt(edge -> edge.weight).sum();

}

public static void main(String[] args) {

// 创建图

Graph graph = new Graph(5);

graph.addEdge(0, 1, 2);

graph.addEdge(0, 3, 6);

graph.addEdge(1, 2, 3);

graph.addEdge(1, 3, 8);

graph.addEdge(1, 4, 5);

graph.addEdge(2, 4, 7);

graph.addEdge(3, 4, 9);

System.out.println("图的顶点数: " + graph.vertices);

System.out.println("图的边:");

for (int i = 0; i < graph.vertices; i++) {

for (Edge edge : graph.adjacencyList.get(i)) {

if (edge.source < edge.destination) { // 避免重复输出

System.out.println(edge);

}

}

}

// 计算最小生成树

List<Edge> mst = primMST(graph);

System.out.println("\n最小生成树的边:");

mst.forEach(System.out::println);

int totalWeight = calculateTotalWeight(mst);

System.out.println("最小生成树总权值: " + totalWeight);

// 验证MST性质

System.out.println("\n验证:");

System.out.println("MST边数: " + mst.size() + " (期望: " + (graph.vertices - 1) + ")");

Set<Integer> verticesInMST = new HashSet<>();

for (Edge edge : mst) {

verticesInMST.add(edge.source);

verticesInMST.add(edge.destination);

}

System.out.println("MST包含顶点数: " + verticesInMST.size() + " (期望: " + graph.vertices + ")");

// 性能测试

System.out.println("\n性能测试:");

long startTime = System.nanoTime();

List<Edge> result = primMST(graph);

long endTime = System.nanoTime();

System.out.println("执行时间: " + (endTime - startTime) + " 纳秒");

}

}
算法分析

  • 时间复杂度:O(E log V),使用优先队列优化
  • 空间复杂度:O(V + E),存储图结构和辅助数组
  • 最优性:保证得到最小生成树

5. 贪心算法的进阶应用

5.1 多机调度问题

import java.util.*;

/**

* 多机调度问题的贪心算法实现

* 问题描述:有n个作业和m台机器,每个作业需要一定的处理时间,

* 如何安排作业使得所有作业完成时间最短

*/

public class MultiMachineScheduling {

/**

* 多机调度贪心算法

* 策略:总是将当前最长的作业分配给当前负载最小的机器

* @param jobs 作业处理时间数组

* @param m 机器数量

* @return 每台机器的作业分配

*/

public static List<List<Integer>> scheduleJobs(int[] jobs, int m) {

if (jobs == null || jobs.length == 0 || m <= 0) {

throw new IllegalArgumentException("参数不合法");

}

// 将作业按处理时间降序排序

int[] sortedJobs = Arrays.copyOf(jobs, jobs.length);

Arrays.sort(sortedJobs);

for (int i = 0; i < sortedJobs.length / 2; i++) {

int temp = sortedJobs[i];

sortedJobs[i] = sortedJobs[sortedJobs.length - 1 - i];

sortedJobs[sortedJobs.length - 1 - i] = temp;

}

// 使用优先队列(最小堆)来维护机器负载

PriorityQueue<Machine> pq = new PriorityQueue<>(

Comparator.comparingInt(Machine::getTotalTime)

);

// 初始化机器

for (int i = 0; i < m; i++) {

pq.offer(new Machine(i));

}

// 分配作业

for (int job : sortedJobs) {

Machine leastLoaded = pq.poll();

leastLoaded.addJob(job);

pq.offer(leastLoaded);

}

// 收集结果

List<List<Integer>> result = new ArrayList<>();

while (!pq.isEmpty()) {

Machine machine = pq.poll();

result.add(machine.getJobs());

}

return result;

}

static class Machine {

int id;

List<Integer> jobs;

int totalTime;

public Machine(int id) {

this.id = id;

this.jobs = new ArrayList<>();

this.totalTime = 0;

}

public void addJob(int jobTime) {

jobs.add(jobTime);

totalTime += jobTime;

}

public int getTotalTime() {

return totalTime;

}

public List<Integer> getJobs() {

return jobs;

}

@Override

public String toString() {

return "Machine " + id + ": " + jobs + " (总时间: " + totalTime + ")";

}

}

public static void main(String[] args) {

int[] jobs = {3, 5, 2, 7, 4, 6, 1, 8, 9, 2};

int m = 3;

System.out.println("作业处理时间: " + Arrays.toString(jobs));

System.out.println("机器数量: " + m);

List<List<Integer>> schedule = scheduleJobs(jobs, m);

System.out.println("\n作业分配结果:");

int maxTime = 0;

for (int i = 0; i < schedule.size(); i++) {

List<Integer> machineJobs = schedule.get(i);

int totalTime = machineJobs.stream().mapToInt(Integer::intValue).sum();

maxTime = Math.max(maxTime, totalTime);

System.out.println("机器 " + i + ": " + machineJobs + " (总时间: " + totalTime + ")");

}

System.out.println("\n所有作业完成时间: " + maxTime);

// 性能分析

System.out.println("\n性能分析:");

double sumJobs = Arrays.stream(jobs).sum();

double lowerBound = Math.ceil(sumJobs / m);

System.out.printf("理论下界: %.2f\n", lowerBound);

System.out.printf("实际完成时间: %d\n", maxTime);

System.out.printf("近似比: %.2f\n", maxTime / lowerBound);

}

}

6. 贪心算法的正确性证明

6.1 证明方法详解

交换论证法示例(区间调度问题):
对于区间调度问题,我们可以用交换论证证明贪心选择性质:

  1. 假设存在一个最优解O,其中第一个选择的区间不是结束时间最早的
  2. 我们可以用结束时间最早的区间替换O中的第一个区间
  3. 替换后的解仍然是可行的,且区间数量不变
  4. 因此,总是选择结束时间最早的区间是安全的
    数学归纳法示例(霍夫曼编码):
    基础情况:当只有两个字符时,霍夫曼编码显然最优
    归纳假设:对于n-1个字符,霍夫曼编码最优
    归纳步骤:对于n个字符,合并频率最小的两个字符后,问题转化为n-1个字符的情况,由归纳假设知霍夫曼编码最优

7. 贪心算法的局限性及改进

7.1 常见局限性

  1. 局部最优不等于全局最优
  2. 对问题结构要求严格
  3. 难以证明正确性
  4. 对输入数据敏感

7.2 改进策略

  1. 与动态规划结合:在局部使用贪心策略
  2. 随机化贪心:引入随机性避免陷入局部最优
  3. 多起点贪心:从多个起点开始执行贪心算法
  4. 贪心+局部搜索:在贪心解的基础上进行局部优化

8. 实际工程应用

8.1 网络路由算法

  • Dijkstra算法(最短路径)
  • OSPF协议中的路由计算

8.2 资源分配系统

  • 云计算中的虚拟机调度
  • 分布式系统中的负载均衡

8.3 数据处理系统

  • 数据压缩(gzip, PNG等)
  • 数据库查询优化

9. 性能分析与优化

9.1 时间复杂度对比

|-------|------------|-------|---------|
| 问题 | 贪心算法 | 动态规划 | 暴力搜索 |
| 区间调度 | O(n log n) | - | O(2^n) |
| 霍夫曼编码 | O(n log n) | - | O(n!) |
| 最小生成树 | O(E log V) | - | O(E^V) |
| 分数背包 | O(n log n) | O(nW) | O(2^n) |

9.2 空间复杂度分析

贪心算法通常具有较低的空间复杂度,一般在O(1)到O(n)之间,远低于动态规划的O(nW)或O(n²)。

10. 学习建议与最佳实践

10.1 学习路径

  1. 掌握基本贪心策略
  2. 学习正确性证明方法
  3. 练习经典问题变种
  4. 理解算法局限性
  5. 学习与其他算法的结合

10.2 代码实现最佳实践

  1. 充分的参数校验
  2. 清晰的代码注释
  3. 完整的测试用例
  4. 性能监控和优化
  5. 错误处理机制

11. 总结

贪心算法是一种强大而高效的算法设计范式,在适合的问题上能够提供近乎最优的解决方案。通过本文的深入分析,我们可以看到:

  1. 理论基础坚实:贪心选择性质和最优子结构是算法正确性的保证
  2. 应用范围广泛:从数据压缩到网络路由都有重要应用
  3. 实现相对简单:代码清晰易懂,易于维护
  4. 性能优异:在时间复杂度上往往优于其他算法
    然而,贪心算法并非万能钥匙,在使用时需要仔细分析问题特性,严格证明算法的正确性。通过理论与实践的结合,我们能够更好地掌握这一重要的算法设计技术。
相关推荐
paopaokaka_luck4 小时前
基于SpringBoot+Vue的DIY手工社预约管理系统(Echarts图形化、腾讯地图API)
java·vue.js·人工智能·spring boot·后端·echarts
wydaicls4 小时前
C语言对单链表的操作
c语言·数据结构·算法
傻童:CPU5 小时前
C语言需要掌握的基础知识点之排序
c语言·算法·排序算法
计算机学姐8 小时前
基于微信小程序的高校班务管理系统【2026最新】
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
一路向北⁢8 小时前
基于 Apache POI 5.2.5 构建高效 Excel 工具类:从零到生产级实践
java·apache·excel·apache poi·easy-excel·fast-excel
游戏开发爱好者88 小时前
HTTPS 内容抓取实战 能抓到什么、怎么抓、不可解密时如何定位(面向开发与 iOS 真机排查)
android·网络协议·ios·小程序·https·uni-app·iphone
大数据张老师9 小时前
数据结构——邻接矩阵
数据结构·算法
低音钢琴10 小时前
【人工智能系列:机器学习学习和进阶01】机器学习初学者指南:理解核心算法与应用
人工智能·算法·机器学习
毕设源码-赖学姐10 小时前
【开题答辩全过程】以 基于Android的校园快递互助APP为例,包含答辩的问题和答案
java·eclipse