news 2026/8/28 13:55:39

C语言通讯录项目实战:从数组到链表的数据结构设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言通讯录项目实战:从数组到链表的数据结构设计与实现

1. 项目概述:为什么通讯录是C语言学习的“毕业设计”?

如果你已经学完了C语言的基础语法,比如变量、数组、指针、结构体,甚至文件操作,但总觉得知识点是散的,不知道如何把它们串起来,做成一个真正能用的东西。那么,这个“C语言实现通讯录”项目,就是你一直在等的那个“粘合剂”。它不是什么高深莫测的算法,但麻雀虽小,五脏俱全,几乎涵盖了C语言从入门到实践的所有核心知识点。我当年学C语言,就是靠写了一个又一个版本的通讯录,才真正把指针和内存管理给“盘”明白的。

这个项目要做什么?很简单,就是做一个能在命令行里运行的通讯录管理程序。你可以用它来添加新联系人、删除旧联系人、查找特定的人、修改信息,并且最好能把所有联系人的数据保存到硬盘上的文件里,下次打开程序还能读出来。听起来是不是很像我们手机里的通讯录?没错,它的核心逻辑就是如此。但正是这个简单的需求,背后藏着结构体定义、动态内存管理、链表或数组操作、文件读写、用户交互界面(虽然是命令行)等一系列关键技术点。通过亲手实现它,你会对“程序=数据结构+算法”这句名言有最直观的感受——通讯录的数据结构就是联系人信息的集合,而增删改查就是操作这些数据的算法。

2. 核心数据结构设计:如何为联系人“建模”?

动手写代码之前,最重要的一步是设计数据结构。这就像盖房子先画图纸,数据模型设计得好,后面的代码写起来就顺畅,维护也容易。

2.1 定义联系人结构体:信息的容器

首先,我们需要定义一个结构体(struct)来代表一个联系人。这是整个项目的基石。一个联系人有哪些基本信息?通常包括姓名、电话,可能还有性别、年龄、住址、邮箱等。

#define MAX_NAME_LEN 50 #define MAX_PHONE_LEN 20 #define MAX_ADDR_LEN 100 typedef struct Person { char name[MAX_NAME_LEN]; char phone[MAX_PHONE_LEN]; char address[MAX_ADDR_LEN]; int age; } Person;

这里有几个设计要点:

  1. 使用typedeftypedef struct Person {...} Person;这行代码为结构体创建了一个别名Person。这样以后声明变量时,直接写Person p1;即可,而不需要写struct Person p1;,代码更简洁。
  2. 数组 vs. 指针:对于字符串字段(name,phone,address),我选择了固定长度的字符数组。为什么不用char*动态分配呢?对于初学者项目,固定数组更简单,避免了动态内存分配的复杂性和内存泄漏的风险。我们通过#define定义了最大长度,便于统一修改。缺点是可能会浪费一些空间,但对于学习项目,清晰稳定更重要。
  3. 信息扩展性:这个结构体很容易扩展。如果你想增加“邮箱”字段,只需要加一行char email[MAX_EMAIL_LEN];即可。这就是结构体的优势,把相关的数据打包在一起。

注意:在实际更复杂的项目中,可能会使用动态字符串(char*配合malloc)来节省内存,但管理起来更复杂。作为第一个版本,我强烈建议从固定数组开始,把核心逻辑跑通。优化是后续的事情。

2.2 管理通讯录:数组还是链表?

定义了单个联系人,我们还需要一个容器来管理所有的联系人。这里有两个主流选择:静态数组动态链表。它们的选择直接决定了整个程序的内存管理方式和核心操作逻辑。

方案一:静态数组

#define MAX_CONTACTS 1000 Person contacts[MAX_CONTACTS]; int contact_count = 0; // 记录当前已存储的联系人数量
  • 优点:实现极其简单,访问速度快(通过下标直接访问),内存连续,对CPU缓存友好。
  • 缺点:容量固定,有上限(MAX_CONTACTS)。如果定义小了不够用,定义大了又浪费内存。插入和删除操作效率低(需要移动后续所有元素)。

方案二:动态链表

typedef struct ContactNode { Person data; struct ContactNode *next; } ContactNode; ContactNode *head = NULL; // 链表头指针
  • 优点:动态内存分配,理论上可以无限添加(直到内存耗尽)。插入和删除操作效率高,只需要修改指针,不需要大规模移动数据。
  • 缺点:实现复杂,需要熟练掌握指针和动态内存管理(malloc,free)。访问某个特定节点需要遍历,速度比数组慢。内存不连续。

如何选择?对于初学者,我建议分两步走

  1. 第一版用静态数组。你的首要目标是实现所有增删改查和文件存读的功能逻辑。用数组可以让你专注于业务逻辑,而不被指针和内存泄漏搞得焦头烂额。先把功能做出来,获得成就感。
  2. 第二版升级为动态链表。当你用数组版本彻底弄明白流程后,再挑战链表版本。这时,你的重点就变成了学习如何用指针串联数据、如何安全地进行动态内存分配与释放。这是从“会用C语言”到“理解C语言精髓”的关键一步。

在下面的解析中,我会以静态数组版本作为主线进行讲解,因为它更直观。但在关键处,我会指出如果换成链表,思路该如何转换。

3. 核心功能模块实现与解析

有了数据结构,我们就可以像搭积木一样,一个个实现功能模块了。每个模块都是一个独立的函数,这样代码结构清晰,也便于调试。

3.1 功能菜单与主循环:程序的指挥中心

任何交互式程序都需要一个菜单来引导用户。我们用一个无限循环(while(1))来持续显示菜单并等待用户输入。

void show_menu() { printf("\n====== 通讯录管理系统 ======\n"); printf("1. 添加联系人\n"); printf("2. 删除联系人\n"); printf("3. 查找联系人\n"); printf("4. 修改联系人信息\n"); printf("5. 显示所有联系人\n"); printf("6. 清空通讯录\n"); printf("7. 将通讯录保存到文件\n"); printf("8. 从文件加载通讯录\n"); printf("0. 退出程序\n"); printf("请选择操作: "); } int main() { Person contacts[MAX_CONTACTS]; int count = 0; int choice; // 程序启动时,可以尝试从文件自动加载数据 // count = load_from_file(contacts, "contacts.dat"); while (1) { show_menu(); if (scanf("%d", &choice) != 1) { // 处理非数字输入 while (getchar() != '\n'); // 清空输入缓冲区 printf("输入错误,请输入数字!\n"); continue; } getchar(); // 吸收回车键,防止影响后续的gets/fgets switch (choice) { case 1: add_contact(contacts, &count); break; case 2: delete_contact(contacts, &count); break; case 3: search_contact(contacts, count); break; case 4: modify_contact(contacts, count); break; case 5: display_all(contacts, count); break; case 6: clear_all(contacts, &count); break; case 7: save_to_file(contacts, count, "contacts.dat"); break; case 8: count = load_from_file(contacts, "contacts.dat"); break; // 注意返回值覆盖count case 0: printf("是否保存修改后退出?(y/n): "); if (getchar() == 'y') { save_to_file(contacts, count, "contacts.dat"); } printf("感谢使用,再见!\n"); return 0; default: printf("无效选择,请重新输入!\n"); } } return 0; }

关键点解析:

  • 输入验证if (scanf(“%d”, &choice) != 1)用于检查用户输入的是否是一个整数。如果不是,scanf会失败,返回0。我们随后用while (getchar() != ‘\n’);来清空输入缓冲区中残留的错误字符,防止它们影响下一次输入。这是一个非常实用的防崩溃技巧。
  • 吸收回车getchar();这行代码至关重要。当用户输入数字后按回车,这个回车符\n会留在输入缓冲区。如果下一个操作是使用fgets读取字符串(比如添加联系人时输入姓名),fgets会直接读到这个\n,导致看起来“跳过了”输入。用getchar()把它吸收掉就解决了。
  • 退出前的保存提示:在用户选择退出时,主动询问是否保存,这是一个友好的设计。数据无价,防止用户忘记保存而丢失劳动成果。

3.2 添加联系人:数组的尾部插入

这是最基本的功能,逻辑也相对简单。

void add_contact(Person contacts[], int *count) { if (*count >= MAX_CONTACTS) { printf("通讯录已满,无法添加!\n"); return; } Person *p = &contacts[*count]; // 指向当前空闲位置 printf("请输入姓名: "); fgets(p->name, MAX_NAME_LEN, stdin); // 去除fgets读入的换行符 p->name[strcspn(p->name, “\n”)] = ‘\0’; printf(“请输入电话: “); fgets(p->phone, MAX_PHONE_LEN, stdin); p->phone[strcspn(p->phone, “\n”)] = ‘\0’; printf(“请输入地址: “); fgets(p->address, MAX_ADDR_LEN, stdin); p->address[strcspn(p->address, “\n”)] = ‘\0’; printf(“请输入年龄: “); while (scanf(“%d”, &(p->age)) != 1 || p->age <= 0) { while (getchar() != ‘\n’); // 清空错误输入 printf(“年龄输入无效,请输入一个正整数: “); } getchar(); // 再次吸收回车! (*count)++; // 联系人数量加1 printf(“联系人添加成功!\n”); }

关键点与避坑指南:

  1. 数组越界检查:第一件事就是检查*count是否已经达到MAX_CONTACTS,这是安全编程的好习惯。
  2. 使用fgets而非getsscanf(“%s”)gets函数极其危险,已被废弃,因为它不检查缓冲区长度,容易导致缓冲区溢出(一种严重的安全漏洞)。scanf(“%s”)读取字符串会在遇到空格时停止,不适合读取带空格的名字或地址。fgets可以指定读取的最大字符数,是安全的选择。
  3. 处理fgets的换行符fgets会把用户输入的回车键(\n)也读进来。我们需要手动找到这个换行符并将其替换为字符串结束符\0strcspn(p->name, “\n”)这个函数返回字符串中第一个\n出现的位置,我们在这个位置写入\0,就巧妙地删除了换行符。
  4. 数字输入的验证与清理:读取年龄时,我们用一个while循环来确保输入的是一个有效的正整数。scanf(“%d”, &var)的返回值表示成功匹配并赋值的参数个数。如果输入的是字母,scanf会失败并返回0。循环内的while (getchar() != ‘\n’);用于清空缓冲区中所有错误字符,直到遇到回车。
  5. 再次吸收回车:在scanf读取完年龄后,缓冲区里还有一个回车。必须再用一个getchar()把它吃掉,否则会影响下一次菜单显示或后续的fgets调用。这是新手最容易忽略的地方之一,会导致诡异的程序行为。

3.3 查找联系人:线性搜索与字符串比较

查找是许多操作(删除、修改)的基础。我们通常按姓名查找。

int find_contact_by_name(const Person contacts[], int count, const char *name) { for (int i = 0; i < count; i++) { if (strcmp(contacts[i].name, name) == 0) { return i; // 找到,返回下标 } } return -1; // 未找到 } void search_contact(const Person contacts[], int count) { char name[MAX_NAME_LEN]; printf(“请输入要查找的姓名: “); fgets(name, MAX_NAME_LEN, stdin); name[strcspn(name, “\n”)] = ‘\0’; int index = find_contact_by_name(contacts, count, name); if (index != -1) { printf(“\n找到联系人:\n”); printf(“姓名: %s\n”, contacts[index].name); printf(“电话: %s\n”, contacts[index].phone); printf(“地址: %s\n”, contacts[index].address); printf(“年龄: %d\n”, contacts[index].age); } else { printf(“未找到姓名为 %s 的联系人。\n”, name); } }

解析与技巧:

  • 封装查找函数:将查找逻辑单独写成find_contact_by_name函数是个好习惯。它只负责查找并返回结果(下标或-1),不负责输入输出。这样,删除和修改功能也可以复用这个函数,代码更清晰、可复用。
  • 使用strcmp进行字符串比较:不能直接用==比较两个字符串(那比较的是地址)。必须使用string.h中的strcmp函数。strcmp(a, b) == 0表示字符串a和b完全相同。
  • 模糊查找的扩展:当前是精确查找。你可以尝试扩展为模糊查找,比如使用strstr函数判断查找的关键词是否包含在姓名中,这样用户体验会更好。

3.4 删除联系人:数组元素的“移除”

删除操作是数组版本中相对复杂的一步,因为数组在内存中是连续的,删除中间的元素需要移动后面的所有元素来“填补空缺”。

void delete_contact(Person contacts[], int *count) { char name[MAX_NAME_LEN]; printf(“请输入要删除联系人的姓名: “); fgets(name, MAX_NAME_LEN, stdin); name[strcspn(name, “\n”)] = ‘\0’; int index = find_contact_by_name(contacts, *count, name); if (index == -1) { printf(“联系人不存在,删除失败。\n”); return; } // 确认删除 printf(“确定要删除联系人 [%s] 吗?(y/n): “, name); if (getchar() != ‘y’) { while (getchar() != ‘\n’); // 清空缓冲区,包括回车 printf(“取消删除。\n”); return; } getchar(); // 吸收确认时的回车 // 核心:从index+1开始,每个元素向前移动一位 for (int i = index; i < *count - 1; i++) { contacts[i] = contacts[i + 1]; // 结构体可以直接赋值(浅拷贝) } (*count)--; // 总数减1 printf(“联系人删除成功!\n”); }

关键操作与思考:

  • 确认机制:删除操作不可逆,提供一个确认环节是必要的,防止误操作。
  • 数组元素移动for (int i = index; i < *count - 1; i++)这个循环是删除的核心。它将待删除位置之后的所有元素,都向前移动一个位置。注意循环的终止条件是i < *count - 1,因为我们要把i+1的元素挪到i,当i是倒数第二个元素时,它把最后一个元素挪过来,任务就完成了。
  • 结构体赋值contacts[i] = contacts[i + 1];这行代码进行了结构体的整体拷贝。对于我们的Person结构体(成员都是基本类型或数组),这是安全且高效的。如果结构体内部包含指针(比如char* name),这种直接赋值就是“浅拷贝”,只拷贝了指针值而不是指向的内容,可能会出问题。这就是为什么初学者版本建议用固定数组。
  • 链表版本的差异:如果是链表,删除就简单多了:找到要删除的节点,让它的前一个节点的next指针,指向它的后一个节点,然后free掉这个节点即可。不需要移动大量数据。

3.5 修改联系人信息:查找到后的直接赋值

修改功能是查找和添加的结合体。先查找,如果找到了,就重新输入信息覆盖旧的数据。

void modify_contact(Person contacts[], int count) { char name[MAX_NAME_LEN]; printf(“请输入要修改联系人的姓名: “); fgets(name, MAX_NAME_LEN, stdin); name[strcspn(name, “\n”)] = ‘\0’; int index = find_contact_by_name(contacts, count, name); if (index == -1) { printf(“联系人不存在,修改失败。\n”); return; } Person *p = &contacts[index]; // 指向要修改的联系人 printf(“\n正在修改联系人 [%s] 的信息(直接回车将保留原值):\n”, name); printf(“原姓名[%s] -> 新姓名: “, p->name); char input[MAX_NAME_LEN]; fgets(input, MAX_NAME_LEN, stdin); if (input[0] != ‘\n’) { // 用户输入了新内容 input[strcspn(input, “\n”)] = ‘\0’; strcpy(p->name, input); } // 电话、地址、年龄的修改逻辑类似,此处省略... printf(“联系人信息修改成功!\n”); }

用户体验优化:

  • 保留原值功能:代码中演示了一个小技巧:如果用户直接按回车(input[0] == ‘\n’),就跳过赋值,保留原来的值。这是一个非常人性化的设计,用户不需要为了修改一个字段而重新输入所有信息。

3.6 文件存储与加载:数据的持久化

这是让程序变得“有用”的关键。没有文件操作,数据只在程序运行时存在,关闭就没了。我们需要把内存中的contacts数组写入文件,以及从文件中读回来。

保存到文件:

int save_to_file(const Person contacts[], int count, const char *filename) { FILE *fp = fopen(filename, “wb”); // 以二进制写入模式打开 if (fp == NULL) { perror(“打开文件失败”); return 0; // 失败 } // 先写入联系人数量,方便读取时预知 fwrite(&count, sizeof(int), 1, fp); // 将整个联系人数组写入文件 // 注意:这里写入的是二进制数据,不是文本 size_t written = fwrite(contacts, sizeof(Person), count, fp); fclose(fp); if (written == count) { printf(“成功保存 %d 个联系人到文件 %s\n”, count, filename); return 1; } else { printf(“保存文件时发生错误。\n”); return 0; } }

从文件加载:

int load_from_file(Person contacts[], const char *filename) { FILE *fp = fopen(filename, “rb”); // 以二进制读取模式打开 if (fp == NULL) { // 文件可能不存在,第一次运行,这不是错误 printf(“未找到保存文件,将创建新的通讯录。\n”); return 0; } int count = 0; // 先读取保存的联系人数量 if (fread(&count, sizeof(int), 1, fp) != 1) { printf(“读取文件头信息失败,文件可能已损坏。\n”); fclose(fp); return 0; } if (count > MAX_CONTACTS) { printf(“警告:文件中的联系人数量(%d)超过最大容量(%d),将只加载前%d个。\n”, count, MAX_CONTACTS, MAX_CONTACTS); count = MAX_CONTACTS; } // 读取联系人数据到数组 size_t read = fread(contacts, sizeof(Person), count, fp); fclose(fp); if (read == count) { printf(“成功从文件加载 %d 个联系人。\n”, count); return count; } else { printf(“读取联系人数据时发生错误,文件可能已损坏。\n”); return 0; } }

二进制 vs. 文本格式的抉择:

  • 二进制模式(”wb”/”rb”:我们使用了fwrite/fread直接读写内存块。优点是速度快、效率高,一次调用就能读写整个结构体数组。缺点是生成的文件是二进制的,用文本编辑器打开是乱码,且可移植性可能有问题。如果程序在不同平台(如Windows和Linux)间运行,或者编译器对结构体的内存对齐方式不同,读取可能会出错。
  • 文本模式(”w”/”r”:另一种常见方法是使用fprintf将每个字段以特定格式(如CSV)写入文件。优点是文件是人类可读的文本,可以用Excel打开,跨平台性好。缺点是读写速度慢,需要解析格式,代码更复杂。

为什么先保存count在文件开头写入联系人数量,是一个非常重要的设计。这样在读取时,我们可以先知道有多少条记录,便于控制读取循环,也便于进行容量检查(if (count > MAX_CONTACTS))。

重要警告:如果你在Person结构体中使用了指针(比如char* name),绝对不能用上面的二进制方式直接fwrite整个结构体。因为你保存的只是一个内存地址(指针的值),下次程序运行时,这个地址是无效的,而且它指向的字符串内容并没有被保存。这种情况下,必须使用文本模式,逐个字段地保存字符串的实际内容。

4. 从数组到链表的升级思路

当你用数组版本稳定运行后,可以尝试挑战链表版本。这不仅仅是数据结构的更换,更是对C语言指针和动态内存管理的深度实践。

4.1 链表版本的核心改动

  1. 数据结构重定义:如前所述,联系人节点需要包含数据域和指针域。
  2. 内存动态分配add_contact函数中,你需要使用ContactNode *new_node = (ContactNode*)malloc(sizeof(ContactNode));来创建新节点,并在delete_contact中对应地使用free(node_to_delete)来释放内存。
  3. 插入与删除:链表插入可以在头部进行(最简单),new_node->next = head; head = new_node;。删除则需要找到待删除节点的前驱节点,修改其next指针。
  4. 文件存储:链表不能像数组那样一次性fwrite。你需要遍历链表,将每个节点的dataPerson结构体)依次写入文件。同样,读取时需要循环读取,并为每个读取的数据动态创建节点并链接起来。
  5. 遍历操作:所有需要遍历联系人的功能(显示、查找、保存),都需要一个ContactNode *current = head; while (current != NULL) { … current = current->next; }的循环。

4.2 链表版本的挑战与收获

  • 挑战:你需要非常小心地处理指针,确保不会出现“野指针”或“内存泄漏”。每一个malloc都必须有对应的free。删除节点时,指针的修改顺序很重要,否则可能丢失链表。
  • 收获:你会彻底理解“动态”二字的含义。你会对内存的申请和释放有切身体会。你会掌握一种极其重要且灵活的数据组织方式。完成链表版本后,你对C语言的理解会上一个大台阶。

5. 常见问题与调试技巧实录

在实际编写和运行这个通讯录程序时,你几乎一定会遇到下面这些问题。我把它们和解决方法记录下来,希望能帮你节省大量时间。

5.1 输入输出相关“玄学”Bug

  • 问题:选择菜单后,程序好像“跳过”了接下来的输入,直接闪过了。

  • 原因与解决:99%是因为输入缓冲区里残留了换行符\n。记住这个“黄金法则”:scanf之后、fgets之前,用getchar()吸收掉多余的换行符。更稳健的做法是使用while (getchar() != ‘\n’);来清空整个缓冲区。

  • 问题:输入年龄时,如果输入了字母,程序陷入死循环或崩溃。

  • 原因与解决scanf(“%d”, &age)无法处理非数字输入,失败后错误字符会留在缓冲区,导致下一次scanf继续失败。解决方案是检查scanf的返回值,并在失败时清空缓冲区,如3.2节所示。

5.2 文件操作相关错误

  • 问题:文件保存成功,但再次打开程序加载时,数据乱码或程序崩溃。

  • 检查点

    1. 读写模式是否匹配?保存用”wb”,加载就要用”rb”
    2. 结构体是否包含指针?如果包含,二进制方式保存/加载是无效的,必须改用文本格式。
    3. 加载后,count变量是否正确更新了?确保load_from_file的返回值赋值给了主函数里的count
  • 问题:在Windows上写的程序,保存的文件在Linux上读不出来。

  • 原因:可能是文本文件的行结束符问题(Windows是\r\n,Linux是\n),也可能是结构体的内存对齐(#pragma pack)不同。对于学习项目,可以暂时只考虑同一平台。对于追求健壮性,建议使用文本格式(如CSV)存储。

5.3 功能逻辑错误

  • 问题:删除或修改联系人时,总是对最后一个联系人进行操作。

  • 原因:很可能是在查找函数中,字符串比较使用了==而不是strcmp。记住,C语言中比较字符串必须用strcmp

  • 问题:添加了很多联系人后,删除中间的某个,再显示时,最后一条信息出现了重复或乱码。

  • 原因:删除元素后,数组count减1了,但你的显示循环for (int i = 0; i < OLD_COUNT; i++)可能还在用旧的、更大的范围去遍历,访问到了已无效的数据。确保所有用到count的地方,在删除后都使用更新后的值。

5.4 调试技巧:让程序“说话”

  1. 打印大法好:在怀疑出问题的函数开头和关键步骤后,打印相关变量的值。比如在delete_contact里,打印index*count,以及移动元素前后数组的内容。这是最直接有效的调试手段。
  2. 分模块测试:不要写完所有代码再一起测试。写一个函数,就立刻在main里调用测试一下。例如,先只实现add_contactdisplay_all,测试添加和显示是否正常。然后再加入查找、删除等功能。
  3. 使用调试器:如果你用的是Visual Studio、Code::Blocks、CLion或VSCode(配合GDB),学习使用调试器。设置断点,单步执行,查看变量值的变化,能帮你精准定位逻辑错误。

把这个通讯录项目从头到尾实现一遍,你遇到的坑和解决的办法,会比你看十本教科书学到的东西都多。编程是门实践手艺,光说不练假把式。当你看到自己写的程序能稳稳地保存上百个联系人的信息时,那种成就感就是学习路上最好的奖励。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/28 13:53:12

Lapse:用MCP为AI Agent打造跨会话共享记忆空间

Lapse 这个项目最值得关注的一点&#xff0c;是它把“笔记应用”和“AI agent 的共享记忆空间”做成了同一个东西&#xff0c;并且用 MCP&#xff08;Model Context Protocol&#xff09;作为对外连接口。你可以把它理解为&#xff1a;你平时用笔记记录自己的想法、计划、知识&…

作者头像 李华
网站建设 2026/8/28 13:53:02

R语言曲线拟合与模型选择:AIC、BIC、LRT优化方法实战

1. 项目概述&#xff1a;从“画线”到“选线”的思维跃迁在数据分析的日常工作中&#xff0c;我们常常会遇到这样的场景&#xff1a;手头有一组散点数据&#xff0c;我们想用一条光滑的曲线来描述其背后的趋势。无论是预测未来走势、理解变量关系&#xff0c;还是单纯为了可视化…

作者头像 李华
网站建设 2026/8/28 13:51:00

稀疏检索+语义检索+LLM重排序:ADHD症状检测的多路召回实践

在文本分类、信息检索与医疗健康文本挖掘交叉的场景里&#xff0c;“稀疏检索 语义检索 LLM 重排序”这套组合已经被越来越多的竞赛方案和生产项目采用。本文以 DSGT-ARC 在 eRisk 2026 Task 3 上的方案为例&#xff0c;拆解 ADHD 症状句子检测中的稀疏信号、语义信号和 LLM …

作者头像 李华
网站建设 2026/8/28 13:50:28

CM4载板SMT贴片成本解析与实操避坑指南

我去年做了一整年的CM4载板项目&#xff0c;前后打样了将近十版&#xff0c;在PCB组装上花的钱足够买一台中端3D打印机了。所以看到Seeed这波针对树莓派Compute Module 4载板的组装折扣消息&#xff0c;第一反应是“终于等到你”。今天就把这事拆开聊聊&#xff1a;CM4载板到底…

作者头像 李华
网站建设 2026/8/28 13:49:04

基于YOLOv8的番茄目标检测实战:从数据集解析到模型训练部署

简介&#xff1a;目标检测是计算机视觉的核心任务之一&#xff0c;旨在识别图像中特定目标的位置和类别。其原理通常基于深度学习模型&#xff0c;通过卷积神经网络提取特征&#xff0c;并预测目标的边界框和类别概率。这项技术在工业自动化、智能安防、自动驾驶等领域具有重要…

作者头像 李华
网站建设 2026/8/28 13:48:47

数学建模竞赛优化问题实战:Python线性规划求解与可视化

1. 项目背景与问题拆解 最近在整理过去的竞赛资料&#xff0c;翻到了2022年“华中杯”数学建模竞赛A题的相关文件。虽然当时只完整做完了第一小问&#xff0c;但整个解题过程&#xff0c;尤其是用Python实现模型求解与可视化的部分&#xff0c;我觉得对很多刚开始接触数学建模或…

作者头像 李华