资讯动态

C语言图书管理信息系统:链表与折半查找实战拆解

发布时间:2026/10/9 10:00:54 来源:尧图企业网站定制
简介图书管理信息系统的设计与实现数据结构课程设计报告是一份面向高校计算机、通信工程等专业学生开展数据结构课程设计或图书管理类实训的完整文档重点解决图书采编、编目、查询及借还书流通环节的系统设计与编码实现问题。文档基于C语言涵盖图书信息数据库、索引文件、链表存储、借书人与图书结构体定义等关键设计并给出buy、SearchByNum、SearchByName、borrow、return等函数的实现思路。资源包为doc格式共1个文件压缩包大小约119KB内容包含设计题目、问题描述、基本要求、概要设计与函数实现等章节可辅助读者快速完成课程设计报告撰写与代码框架搭建。已有1381人浏览学习比较适合需要参考图书管理系统数据结构设计思路、索引与链表应用方案或希望借鉴模块化设计和函数实现细节的学生与开发者。1. 图书管理信息系统一份 C 语言课程设计为什么值得认真拆一遍图书管理信息系统是数据结构课程设计里出现频率最高的题目之一但多数人交完报告就把代码丢进回收站。这份资源的特别之处在于它完整覆盖了数据结构的几个核心考点结构体数组做图书主表、单链表做借阅者关系、折半查找维持书号检索效率、线性遍历处理书名查询。整个系统只有五个函数——入库、按书号查、按书名查、借书、还书——却把 C 语言里最容易翻车的指针操作全过了一遍很适合用来练手或者改造成期末项目。适合的读者有两类一是正在做课程设计、需要一套能跑通还能讲清楚原理的参考实现二是想复习链表增删和二分查找的老手。下文按“数据结构设计 → 核心算法 → 流通模块 → 避坑 → 验证扩展”的顺序拆解中间会直接把关键代码贴出来。2. 数据结构设计两个一维数组加四条链表撑起整个系统2.1 图书主表用结构体数组为什么不用链表大多数课程设计第一时间想到的是双链表存所有图书但这个设计选择了结构体数组ook boo[MAXSIZE]MAXSIZE 固定为 100原因很现实图书采编入库要做折半查找而折半查找要求数据在内存里是连续存储、按下标随机访问的。链表虽然插入删除灵活但做不到 O(log n) 的折半定位只能从头遍历到中间结点那就不叫二分了。所以作者的取舍是图书主表用数组保证查询效率借阅关系用链表应对动态变化。#define MAXSIZE 100 // 藏书种类上限 #define LIST_INIT_SIZE 100 // 可登记的图书证数量上限 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;这个ook类型名有点奇怪其实是原作者敲键盘时把 Book 打成了 Ook如果你要复用改成Book更规范。结构体里最值得注意的字段是LinkList *next它是从图书结点伸出去的借阅者链表头。也就是说一本书被谁借走了不单独建表而是挂在图书结点下面查询书详情时顺带打印出借阅者列表。2.2 借书人表一个被 typedef 成数组的结构体借书人这部分容易看晕因为它把lend直接定义成了一个数组类型而不是先定义结构体再声明数组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]; // 直接把数组类型命名为 lend这种写法的意图是lend类型本身就是一个长度为 100 的结构体数组每个元素对应一个图书证每个证号下面挂着一条借书链表。函数声明里写lend Lin调用时传数组名效果是对数组做整体引用修改——这是 C 语言里少见但合法的用法。不过它有一个隐患后面避坑章节会展开讲。2.3 全局变量承担了传递返回值的角色代码里有两个显式定义的全局变量int mid和int total。mid用来把折半查找命中的下标传出来total记录当前图书种类数。int Retotal; // 当前已登记的读者数量 int total; // 当前图书种类数 int mid 0; // 全局变量供 BinarySearch 返回命中下标 void InitBo(ook boo[]) { for (int i 0; i MAXSIZE; i) { // 初始化图书数组给每个结点的 next 置空 boo[i].next NULL; } }全局变量在课程设计级别是可以接受的毕竟函数返回值只有一个作者又想同时拿“是否找到”和“找到的位置”于是用mid做隐蔽的返回值。但你心里要清楚这是偷懒不是最佳实践。项目里函数多了之后全局mid会被反复改写只要在BinarySearch之后做任何会调用其他函数的操作mid就可能被覆盖。更稳妥的做法是定义一个结构体{ int found; int pos; }作为查找结果返回或者干脆把查找封装成传入指针、在函数内修改*pos。3. 采编入库与折半查找把“有序”当成命根子3.1 BinarySearch二分查找的两个隐藏前提先看这段核心代码注意mid被当作全局变量使用high、low是函数内局部变量int BinarySearch(ook boo[], char SearchNum[]) { int low 0, high total - 1; int found 0; while (low high) { mid (low high) / 2; if (strcmp(boo[mid].num, SearchNum) 0) { found 1; return 1; // 查找成功 } else if (strcmp(boo[mid].num, SearchNum) 0) { low mid 1; // 目标书号比中间书号大去右半区 } else { high mid - 1; // 目标书号比中间书号小去左半区 } } return 0; // 查找失败 }我在这里做了一处修正原代码里if (strcmp(...) ! 0) high mid - 1; else low mid 1;是个明显的逻辑错误——只要两个字符串不相等就一律左移high等于永远在左半区找后半截数据永远查不到。改写成比较 0之后才能正确二分。这是这份代码里最容易踩的第一个坑。另一个隐藏前提是图书数组必须始终按书号有序。折半查找建立在有序数组之上所以每次Buy插入新书时必须先把新书放到正确位置再后移元素。这就解释了为什么Buy里要先BinarySearch——查不到时mid指向的就是新书应该插入的位置附近。3.2 Buy 入库三次循环完成一次有序插入void Buy(ook boo[], char BuyNum[]) { int i; if (BinarySearch(boo, BuyNum)) { // 书已存在总库存和现库存同时 1 boo[mid].TotNum; boo[mid].NowNum; printf(入库成功.\n); printf(书号 %s 书名 %s 作者 %s 出版社 %s总库存 %d现库存 %d\n, boo[mid].num, boo[mid].name, boo[mid].auth, boo[mid].pub, boo[mid].TotNum, boo[mid].NowNum); return; } // 书不存在先腾位置 for (i total; i mid total; i--) { boo[i] boo[i - 1]; // 从后往前逐个后移 } // 此时 i mid新书占住空出来的位置 strcpy(boo[i].num, BuyNum); printf(该书购入的数量是:); scanf(%d, boo[i].NowNum); boo[i].TotNum boo[i].NowNum; printf(该书的名字是:); scanf(%s, boo[i].name); printf(该书的作者是:); scanf(%s, boo[i].auth); printf(该书的出版社是:); scanf(%s, boo[i].pub); boo[i].next NULL; total; // 图书种类数 1 printf(入库成功.\n); }这个函数的核心逻辑是“后移元素留出空位”。假设数组前 5 个元素是排好序的book[0]到book[4]新书查不到时mid指向应该在的位置比如mid 2。循环从i 5开始把book[4]的 struct 内容整体拷贝到book[5]然后book[3]拷贝到book[4]book[2]拷贝到book[3]——最后下标 2 空出来写入新书。这个操作完成后数组依然有序。struct book里有一个LinkList *next指针整体后移时指针也被带过去了拷贝后新位置的next指向的内容和旧位置完全一致。这没问题因为借阅者链表本身不需要跟着数组移动。有个细节需要注意scanf( %d, boo[i].NowNum)之前如果上一次用户输入的是字符串缓冲区里可能残留换行符空格能吃掉残留换行这是老 C 程序员的习惯不是玄学。3.3 SearchByName 和 SearchByNum同函数不同复杂度按书号查询调BinarySearch定位时间复杂度 O(log n)前提是数组有序。按书名查询则是线性遍历因为书名没有作为主键维护排序索引void SearchByName(ook boo[]) { char SeaName[20]; printf(输入想查找的书的书名:\n); scanf(%s, SeaName); printf(找到符合该书名的书的详细信息如下:\n); for (int i 0; i total; i) { if (strcmp(SeaName, boo[i].name) 0) { printf(书号:%s\n书名:%s\n作者:%s\n出版社:%s\n总库存量:%d\n现库存量:%d\n\n, boo[i].num, boo[i].name, boo[i].auth, boo[i].pub, boo[i].TotNum, boo[i].NowNum); } } }按书名查询是最典型的线性查找只能接受 O(n)。如果想优化可以对书名建索引表或哈希表课程设计阶段线性遍历足够交差但如果报告里写了“建立书名次关键字索引文件”那代码就要另说——那是一条按书名组织的链头文件结构工作量翻倍展示效果也翻倍。你可以在答辩时口头提一句“当前实现用遍历完成书名查询若要支持大量重复书名的场景可以升级为书名散列”这比硬编码一个做法的印象好。4. 借书与还书两个链表之间的结点游走4.1 Borrow借书成功需要同步更新两条链借书动作的实质是图书现库存减一同时在两个链表尾部各挂一个新结点。一是该图书的借阅者链表二是该读者的借书记录链表。void Borrow(ook boo[], lend Lin[], char BorrowNum[], char CaNum[]) { Bor *p, *q; LinkList *m, *n; if (!BinarySearch(boo, BorrowNum) || total 0) { printf(书库里没这书.\n); return; } if (boo[mid].NowNum 0) { printf(借阅失败.该书现在库存为0.\n); return; } boo[mid].NowNum--; // 现库存减 1 // 1) 往“这本书的借阅者链表”尾部追加读者证号 if (boo[mid].next NULL) { m (LinkList *)malloc(sizeof(LNode)); boo[mid].next m; strcpy(m-CardNum, CaNum); m-next NULL; } else { m boo[mid].next; while (m-next) m m-next; // 遍历到链表尾 n (LinkList *)malloc(sizeof(LNode)); m-next n; strcpy(n-CardNum, CaNum); n-next NULL; } // 2) 往“该读者的借书记录链表”尾部追加一条书号记录 int i; for (i 0; i Retotal; i) { if (strcmp(Lin[i].CNum, CaNum) 0) { // 该证号已登记过直接在其链表尾插入 p Lin[i].next; while (p-next) p p-next; q (Bor *)malloc(sizeof(Boro)); p-next q; strcpy(q-BNum, BorrowNum); printf(输入归还日期:); scanf(%s, q-RetDate); q-next NULL; printf(借阅成功.\n); return; } } // 3) 该证是第一次借书新增一个读者数组元素 if (i Retotal) { strcpy(Lin[i].CNum, CaNum); p (Bor *)malloc(sizeof(Boro)); Lin[i].next p; strcpy(p-BNum, BorrowNum); printf(输入归还日期:); scanf(%s, p-RetDate); p-next NULL; Retotal; printf(借阅成功.\n); } }逻辑顺序是先操作图书侧链表再操作读者侧链表。两次都要判断“链表是否为空”然后选择头插还是尾插。注意这里写的是尾插法每次while (p-next) p p-next都要从头走到尾在链表长时低效。课程设计数据量小无所谓但如果答辩被问“如何优化”回答改成头插新借阅者直接插在链表头部时间复杂度立刻变 O(1)。参数上有个更隐蔽的问题Borrow修改的是Lin[i]里的CNum但CNum是char[20]数组数组不能整体赋值所以用strcpy。整个过程没有做“借书数量超限”检查也没有校验归还日期是否合法这些属于业务边界问题原始实现为了演示数据结构而简化了。4.2 Return删除借阅者链表结点注意头结点判断void Return(ook boo[], lend Lin[], char ReturnNum[], char BorrowerNum[]) { Bor *p, *q; LinkList *m, *n; int flag 0; if (!BinarySearch(boo, ReturnNum) || !total) { printf(书库中无此书.\n); return; } // 1) 从“这本书的借阅者链表”中删除归还者结点 m boo[mid].next; if (m NULL) { printf(无该证信息.\n); return; } if (strcmp(m-CardNum, BorrowerNum) 0) { // 归还者恰好是链表第一个结点 boo[mid].next m-next; free(m); boo[mid].NowNum; } else { while (m-next) { if (strcmp(m-next-CardNum, BorrowerNum) 0) { n m-next; m-next n-next; free(n); boo[mid].NowNum; break; } m m-next; } } // 2) 从“读者的借书记录链表”中删除该书号的记录 for (int i 0; i Retotal; i) { if (strcmp(Lin[i].CNum, BorrowerNum) 0) { p Lin[i].next; if (p strcmp(p-BNum, ReturnNum) 0) { // 归还的是该证借的第一本书 Lin[i].next p-next; free(p); printf(成功归还该书.\n); flag 1; break; } else { while (p p-next) { if (strcmp(p-next-BNum, ReturnNum) 0) { q p-next; p-next q-next; free(q); printf(成功归还该书.\n); flag 1; break; } p p-next; } } } } // 3) 清理空证数组元素前移覆盖 for (int k 0; k Retotal; k) { if (Lin[k].next NULL) { for (int j k; j Retotal; j) { Lin[j] Lin[j 1]; } strcpy(Lin[Retotal - 1].CNum, ); Retotal--; k--; // 回退一步防止漏掉连续出现的空证 } } if (flag 0) printf(无该证信息.\n); }删除链表结点最怕两种情况删除目标是头结点或者目标根本不在链表里。代码里对头结点单独用strcmp(m-CardNum, BorrowerNum)判断避免了“拿头结点当普通结点删把头结点之后的那段全弄丢”的经典失误。空证清理那段代码用Lin[k] Lin[k 1]做元素覆盖并针对连续空证的情况把k--回退这是原作者做得比较细的一点。不过这里有一次隐藏的数组越界当k Retotal - 1时Lin[k] Lin[k 1]会读取Lin[Retotal]虽然lend类型数组本身有 100 个元素边界读取没写穿内存但逻辑上是悬空的严谨代码应该在循环里先判断j Retotal - 1。5. 常见坑与排查指针游离、排序玄学和那些复制粘贴出来的 bug5.1 二分查找查不到后半段数据现象入库了 20 本书按书号查找时只有前几本搜得到后面的书全部提示“未找到”。原因BinarySearch里缩范围写反了——不相等时一律执行high mid - 1导致搜索永远在左半段进行右半段从未被扫描。原代码中的if(strcmp(...) ! 0) high mid - 1; else low mid 1;是有逻辑缺陷的。解决改成先判相等再判大小关系strcmp(boo[mid].num, SearchNum) 0时low mid 1否则high mid - 1。改完后建议用total个测试书号全部查一遍。5.2 typedef 数组类型当引用参数调用处写法容易迷糊现象把lend类型写在头文件里函数声明void Borrow(ook boo, lend Lin)用了引用编译报“undefined reference”或类型不匹配。原因C 编译器下lend本质上是一个数组类型的别名数组不能按引用传递整个数组类型除非按元素引用或者直接传指针。原作者在 C 语境下用lend Lin属于非标准写法换个编译器直接翻车。解决把lend改回普通结构体名ReaderTable函数声明写成void Borrow(ook boo[], ReaderTable Lin[], char num[], char card[])调用时传数组名即可。如果坚持用引用要写成typedef struct {...} Reader; typedef Reader ReaderTable[LIST_INIT_SIZE];然后函数参数是ReaderTable Lin实际上退化成指针效果相同但更清晰。5.3 scanf 读字符串时数组名前误加取地址符现象scanf( %s, boo[i].name)在部分编译器下报警告运行时偶尔字符串内容错乱。原因name是char[20]数组名本身就是地址boo[i].name取到的是指向数组的指针类型是char (*)[20]与%s期望的char *不匹配。虽然值上碰巧相等但类型系统不一致行为属于未定义。解决去掉写作scanf( %s, boo[i].name)。同理所有char []字段的输入一律直接写字段名。5.4 return 删除图书借阅者结点后没有立刻处理链表头指针现象还书时删除了一个借阅者后续再查这本书程序崩溃或打印出随机地址。原因从链表删除结点时如果目标不是头结点需要借助前驱指针m把前驱的next指到目标的next然后free。代码在 else 分支里漏掉了m-next n-next这一句或者在某些分支里free之后还在用旧指针。解决删除后立刻把boo[mid].next或前驱结点的next置为新值再free。顺序不能颠倒——先断开链接后释放内存。5.5 数组后移插入新书时total位置写错现象入库两本后第三本入库时第一本被覆盖打印信息里出现相同书号的书。原因for (i total; i mid total; i--) boo[i] boo[i - 1];执行完后i已经不等于mid了——循环条件是i mid结束条件是i mid 1也就是循环停止时i比mid大 1。随后程序在boo[i]即boo[mid1]处写入新书覆盖了原本该位置的书而mid位置空着没填。解决用独立变量记录插入位置不要依赖循环结束后i的值。例如int insertPos mid;循环结束后在boo[insertPos]写新书。这是这类数组插入代码里最常见的翻车点。6. 验证方法与扩展一个课程设计怎么跑出工程味写这套系统时我最开始也是把五个函数堆在一起编译过了就交差。后来某导师问了一句“你怎么证明你入库后数组一直有序”我才意识到缺的是构造测试数据和验证逻辑。这个六章的拆解到这里最后给你一个直接能用的建议把主函数里一堆scanf替换成固定测试数据跑一遍断言式验证。常规做法是写一个简单的自检流程先用一组书号乱序的书入库然后遍历数组检查boo[0].num boo[1].num ... boo[total-1].num再对每个书号调用BinarySearch确认返回值与线性查找结果一致。这两个验证过了核心逻辑就打底了。// 自检入库后检查全局数组是否保持按书号升序 int CheckOrder(ook boo[]) { for (int i 1; i total; i) { if (strcmp(boo[i - 1].num, boo[i].num) 0) { printf(有序性被破坏: %s %s\n, boo[i - 1].num, boo[i].num); return 0; } } printf(数组按书号升序排列共 %d 种书\n, total); return 1; }扩展方向上有三个性价比最高的改动。第一把scanf全部换成从文件读入主函数只留一条“从 books.txt 加载执行操作写回 books.txt”的壳数据结构完全不改动立刻能应付“文件存储”类加分项。第二借书时把尾插改成头插把一个 O(n) 操作收敛成 O(1)代码改动只有三行报告里可以写一句“采用头插策略优化高频借阅场景”。第三给BinarySearch增加一个失败返回值重建逻辑——目前查不到书时mid仍然指向某个位置入库插入必须依赖它建议另外定义一个insertPos变量把“查找”和“定位插入点”两个职责分开。从那以后我每次拿到结构体数组排序相关的作业都会强制走一遍 ChaOrder 自检和乱序入库测试确认没破序才往下写业务逻辑。这套习惯让我在答辩环节少挨了不少“你的数据会不会乱掉”的灵魂拷问。希望帮到你——这份代码虽然是 2012 年的老古董但它把 C 语言的指针、数组、链表、全局变量玩了个遍花一晚上复现一遍比期末临时背十个算法模板要更有底气。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑