简介:这份数据结构课程设计报告面向通信工程、物联网及计算机相关专业学生,围绕图书管理信息系统的设计与实现展开,可作为课程设计、期末大作业或数据结构综合练习的完整参考方案。报告以图书采编、编目、查询及借还流通为主线,系统讲解数组、链表、树形结构、Hash表、队列与栈在图书信息存储、快速检索和借还流程中的具体应用,并给出图书信息数据库、索引文件、借书人与图书结构体的设计思路,以及buy()、borrow()、return()等函数的模块化实现方案。资源包共1个doc文件,约119KB,内容涵盖设计题目、问题描述、基本要求、概要设计、结构体定义与折半查找等核心代码片段,目录结构清晰,便于按模块查阅与二次修改。目前已有1381人学习下载,适合需要快速理清设计思路、补齐代码实现与撰写报告的学生参考借鉴。
1. 从一份 2012 年的课程设计报告说起:它到底能帮你解决什么
如果你正在搜「数据结构课程设计 图书管理信息系统」,大概率是两种情况:要么课设选题卡在「图书管理」上,不知道从哪下手;要么手里已经有一份报告,但代码跑不起来、链表指针一改就崩。这份 2012 年湖北师范学院的课程设计报告,题目是《图书管理信息系统的设计与实现》,技术栈是纯 C 语言加链表,核心数据结构覆盖了结构体数组、单链表、折半查找和索引链头文件。它不花哨,但恰好是数据结构课设最典型的形态——用最基础的结构把「采编入库、按书号查、按书名查、借书、还书」五件事串起来。适合通信工程、物联网、计算机相关专业需要交课设报告的人,也适合想拿一个完整 C 语言链表项目练手的人。下面我按「这份资源是什么、怎么跑起来、坑在哪」的顺序拆一遍。
2. 结构体与链表怎么搭:图书、借书人、借阅记录三张表的关系
这份报告最值得先看懂的不是函数,而是数据模型。它定义了三个核心结构体:book(图书)、lend(借书人数组)、Bor(借阅记录链表)。三者通过指针互相挂接,构成了一个「图书—借阅者—借阅记录」的三角关系。理解了这个关系,后面所有函数都只是在这个骨架上做增删改查。
2.1 图书结构体 book 与借阅者链表 LinkList
图书结构体book里存了书号、书名、作者、出版社、总库存、现库存,还有一个LinkList *next指针。这个指针指向的是「借了这本书的人」组成的链表,每个节点只存一个图书证号CardNum。也就是说,同一本书被多个人借走时,book.next后面会挂一串借阅者证号。
typedef struct LNode { char CardNum[20]; // 借这本书的读者证号 struct LNode *next; // 下一个借阅者 } LinkList; typedef struct book { char num[20]; // 书号,作为主关键字 char name[20]; // 书名 char auth[20]; // 作者 char pub[20]; // 出版社 int TotNum; // 总库存 int NowNum; // 现库存 LinkList *next; // 借了该书的人组成的链表 } ook;这里有个容易忽略的点:book数组本身是按书号有序排列的,因为后面BinarySearch依赖有序性。而book.next挂的借阅者链表是无序的,只做遍历查找。两种结构混用,是这份报告的核心设计思路。
2.2 借书人数组 lend 与借阅记录链表 Bor
借书人这边用的是数组lend[LIST_INIT_SIZE],每个元素代表一个读者,里面存图书证号CNum、借书数量Total,以及一个指向Bor链表的指针。Bor节点记录所借书的书号、借书日期、归还日期。
typedef struct Boro { char BNum[20]; // 所借书的书号 char BorDate[8]; // 借书日期 char RetDate[8]; // 归还日期 struct Boro *next; // 下一本借阅记录 } Bor; typedef struct LinkBook { Bor *next; // 该读者的借阅记录链表 char CNum[20]; // 图书证号 int Total; // 借书数量 } lend[LIST_INIT_SIZE];为什么图书用有序数组、借书人用数组加链表?因为图书查询频繁,折半查找要求有序;而借书人数量相对少,数组遍历够用,每个读者的借阅记录用链表动态增长,避免数组扩容。这是典型的「按操作频率选结构」,课设答辩时能讲清这一点,比背代码更加分。
2.3 索引链头文件的设计意图
报告里还提到「书名索引链头文件、作者索引链头文件、出版社索引链头文件」,表格里给了链头地址和长度。这部分在实际代码里没有完整实现,但设计意图很清楚:为书名、作者、出版社各建一张索引表,表项存链头地址和该关键字下的记录数,查询时先查索引表再顺着链走。这是数据库索引的雏形,也是课设报告里「理论分」最高的部分。如果你要补全,可以用一个简单的哈希或有序数组实现,不必真写 B 树。
3. 五个核心函数怎么落地:采编、查找、借书、还书的代码骨架
报告的概要设计里明确了五个函数:Buy()、SearchByNum()、SearchByName()、Borrow()、Return()。这一章逐个拆开,给出可抄的代码骨架和参数说明。注意原报告代码里有几处笔误(比如ook拼写、Boro与Bor混用),下面按可编译版本整理。
3.1 折半查找 BinarySearch 与采编入库 Buy
折半查找是这份报告的算法核心。它要求book数组按书号升序排列,每次取中间点比较,返回mid作为外部变量传出位置。
int mid = 0; // 外部变量,用于返回查找到的位置 int total = 0; // 图书种类数 int BinarySearch(ook boo[], char SearchNum[]) { int low = 0, high = total - 1; while (low <= high) { mid = (low + high) / 2; int cmp = strcmp(boo[mid].num, SearchNum); if (cmp == 0) return 1; // 找到 else if (cmp > 0) high = mid - 1; else low = mid + 1; } return 0; // 未找到 }参数说明:boo[]是图书数组,SearchNum是待查书号。返回值 1 表示找到,0 表示未找到,找到时mid就是下标。注意原报告里if(strcmp(...)!=0) high=mid-1; else low=mid+1;是错的,那样会把「大于」和「小于」混在一起,正确写法必须用cmp的正负分别处理。
采编入库Buy()的逻辑是:先折半查找,如果书已存在,总库存和现库存各加 1;如果不存在,在mid位置插入新书,保持数组有序。
void Buy(ook boo[], char BuyNum[]) { if (BinarySearch(boo, BuyNum)) { boo[mid].TotNum++; boo[mid].NowNum++; printf("入库成功,现库存 %d\n", boo[mid].NowNum); } else { int i; for (i = total; i > mid; i--) // 后移,空出插入位 boo[i] = boo[i-1]; strcpy(boo[i].num, BuyNum); printf("新书,请输入数量、书名、作者、出版社:\n"); scanf("%d %s %s %s", &boo[i].NowNum, boo[i].name, boo[i].auth, boo[i].pub); boo[i].TotNum = boo[i].NowNum; boo[i].next = NULL; total++; } }这里有个细节:BinarySearch未找到时mid的值是最后一次比较的位置,插入点应该在这个位置或其后。原报告直接for(i=total; i>mid; i--)在mid处插入,当mid指向的元素比新书号大时是对的,但边界情况(新书号比所有都大)需要验证mid是否等于total-1。稳妥做法是查找失败后单独判断插入点,或者用low作为插入位置。
3.2 按书号查找 SearchByNum 与按书名查找 SearchByName
按书号查找直接复用BinarySearch,找到后打印图书信息,并遍历book.next链表显示借阅者证号。
void SearchByNum(ook boo[], char SeaNum[]) { if (!BinarySearch(boo, SeaNum)) { printf("未找到该书。\n"); return; } printf("书号:%s 书名:%s 作者:%s 出版社:%s 现库存:%d 总库存:%d\n", boo[mid].num, boo[mid].name, boo[mid].auth, boo[mid].pub, boo[mid].NowNum, boo[mid].TotNum); LinkList *p = boo[mid].next; while (p) { printf("借阅者证号:%s\n", p->CardNum); p = p->next; } }按书名查找没有索引,只能线性遍历整个数组,把所有同名书都打印出来。
void SearchByName(ook boo[]) { char SeaName[20]; printf("输入书名:"); scanf("%s", SeaName); for (int i = 0; i < total; i++) { if (strcmp(SeaName, boo[i].name) == 0) { printf("书号:%s 作者:%s 出版社:%s 现库存:%d\n", boo[i].num, boo[i].auth, boo[i].pub, boo[i].NowNum); } } }对比一下:按书号是 O(log n),按书名是 O(n)。如果课设要求「建立书名索引」,这里就是可以扩展的点——用一张书名到书号列表的映射表,把 O(n) 降到接近 O(1)。
3.3 借书 Borrow 与还书 Return 的指针操作
借书Borrow()做三件事:检查现库存是否大于 0、现库存减 1、在book.next和读者借阅记录里各加一个节点。
void Borrow(ook boo[], lend Lin, char BorrowNum[], char CaNum[]) { if (!BinarySearch(boo, BorrowNum)) { printf("书库无此书。\n"); return; } if (boo[mid].NowNum <= 0) { printf("库存为 0,借阅失败。\n"); return; } boo[mid].NowNum--; // 在图书的借阅者链表中追加证号 LinkList *m = (LinkList *)malloc(sizeof(LinkList)); strcpy(m->CardNum, CaNum); m->next = boo[mid].next; boo[mid].next = m; // 在读者的借阅记录中追加书号 for (int i = 0; i < Retotal; i++) { if (strcmp(Lin[i].CNum, CaNum) == 0) { Bor *q = (Bor *)malloc(sizeof(Bor)); strcpy(q->BNum, BorrowNum); printf("输入归还日期:"); scanf("%s", q->RetDate); q->next = Lin[i].next; Lin[i].next = q; printf("借阅成功。\n"); return; } } // 新读者 strcpy(Lin[Retotal].CNum, CaNum); Bor *q = (Bor *)malloc(sizeof(Bor)); strcpy(q->BNum, BorrowNum); printf("输入归还日期:"); scanf("%s", q->RetDate); q->next = NULL; Lin[Retotal].next = q; Retotal++; printf("借阅成功。\n"); }还书Return()是借书的逆操作:在book.next里找到对应证号节点删除,现库存加 1,在读者借阅记录里删除对应书号节点。原报告代码里用了flag标记是否找到,还做了「删除空借阅记录读者」的清理。这里最容易翻车的是指针删除时的顺序——先保存next再free,否则就是野指针。
void Return(ook boo[], lend Lin, char ReturnNum[], char BorrowerNum[]) { if (!BinarySearch(boo, ReturnNum)) { printf("书库无此书。\n"); return; } // 从图书的借阅者链表中删除 LinkList *m = boo[mid].next, *prev = NULL; while (m) { if (strcmp(m->CardNum, BorrowerNum) == 0) { if (prev) prev->next = m->next; else boo[mid].next = m->next; free(m); boo[mid].NowNum++; break; } prev = m; m = m->next; } // 从读者的借阅记录中删除(略,逻辑类似) printf("归还成功。\n"); }参数说明:ReturnNum是书号,BorrowerNum是证号。两个链表都要删,漏一个就会导致「书还了但读者记录还在」或者「读者记录清了但图书借阅者链表还挂着」的脏数据。
4. 避坑与排查:这份 2012 年代码最容易翻车的五个地方
原报告代码是手写风格,有不少笔误和边界问题。我按「现象 → 原因 → 解决」整理五条,都是实际调试时会遇到的。
4.1 折半查找死循环或查不到
现象:输入存在的书号,BinarySearch返回未找到,或者程序卡住。原因:原报告里if(strcmp(boo[mid].num,SearchNum)!=0) high=mid-1; else low=mid+1;把「不等于」当成「大于」,导致查找区间收缩方向错误。解决:用int cmp = strcmp(...),cmp > 0时high = mid - 1,cmp < 0时low = mid + 1,cmp == 0返回。
4.2 插入新书后数组顺序乱了
现象:采编入库后,再按书号查找查不到,或者查到错的书。原因:Buy()里插入位置用的是mid,但折半查找失败时mid不一定是正确的插入点。解决:查找失败后,用low作为插入位置,或者单独写一个FindInsertPos()返回第一个大于新书号的下标。
4.3 借书后还书,库存对不上
现象:借了一本,还了之后现库存变成 2 或者还是 0。原因:Borrow()里现库存减 1 和链表追加节点没有原子性,如果中间malloc失败或读者已存在但没找到,库存已经减了但记录没加。解决:先完成所有链表操作,最后再改NowNum;或者用事务思路,失败时回滚。
4.4 删除节点后程序崩溃
现象:还书时free()之后程序异常退出。原因:free之后还继续访问该节点的next,或者prev指针没更新。解决:删除前先保存next,free后立即把指针置NULL,并且确保prev->next或头指针已经指向下一个节点。
4.5 字符串输入带空格就截断
现象:书名或作者名里有空格,scanf("%s")只读到空格前。原因:%s以空白字符为分隔。解决:改用scanf("%[^\n]", buf)或fgets,并处理换行符。课设演示时书名带空格很常见,这个坑不修,答辩现场容易尴尬。
5. 从能跑到能讲:把这份课设变成你自己的东西
代码跑通只是及格线,课设真正拉开差距的是「你能不能讲清为什么这么设计」。这份报告里最有讲头的三个点:一是折半查找依赖有序数组,所以采编入库必须维护有序性,这是「查找效率换插入成本」的典型权衡;二是图书用数组、借阅者用链表,是因为图书查询频繁而借阅记录动态增长,结构选择跟着操作频率走;三是索引链头文件的设计,虽然没完全实现,但它是从「线性查找」到「索引查找」的过渡,答辩时能画出索引表示意图,比只贴代码高一个层次。
如果你要补全索引部分,我一般会这样做:为书名建一张哈希表,键是书名,值是一个链表头,链表里存所有同名书的数组下标。查询时先哈希定位,再遍历短链表。代码量不大,但能把「按书名查找 O(n)」降到平均 O(1),报告里多一个对比表格,分数就上去了。
| 查找方式 | 数据结构 | 平均时间复杂度 | 适用场景 |
|---|---|---|---|
| 按书号 | 有序数组 + 折半查找 | O(log n) | 书号唯一,查询频繁 |
| 按书名 | 线性遍历 | O(n) | 书名可能重复,查询较少 |
| 按书名(索引优化) | 哈希表 + 链表 | O(1) 平均 | 需要频繁按书名查 |
验证方法也简单:造 1000 条图书记录,分别用线性查找和折半查找各查 1000 次,用clock()打时间戳,把耗时打印出来。数据一摆,报告里的「性能分析」部分就有了。
最后说个血泪经验:课设代码一定要在答辩前三天完整跑一遍「采编 → 借书 → 还书 → 再查库存」的闭环,我见过太多人只测了单个函数,一串联就崩。从那以后我每次交课设前都强制走一遍全流程,并且把malloc和free的配对检查一遍。希望帮到你。
本文还有配套的精品资源,点击获取