因为最近学习了数据结构的链表操作,然而我还是掌握不太熟练,这个货物信息管理系统的链表操作写了整整快一天(可能是真的不适合敲代码,一个对代码兴趣不大的人的垂死挣扎),不管怎么样,算是完成了。本文把我对这个系统实现的过程和链表使用存在的问题以及遇到的bug分享出来,希望对有需求的人有所帮助(然并没)。
代码所要实现功能如下:
*/*货物信息:名称,货号,数量
功能:
1--------录入所有货物名称、货号、数量
2---------输出所有货物信息
3---------给定货号,查找货物位置
4---------入库
5----------出库
*/
具体代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
char name[20], //货物名称(巧妙的运用了逗号运算符)
no[12]; //货物编号
int num; //货物数量
}good; //定义货物类型的结构体
typedef struct node //节点类型的结构体
{
good data; //货物数据域
struct node *next; //存放下一个货物数据的地址,即指针域
}LNode;
//菜单函数
void menu()
{
printf("%60s","|--------------------------------------|\n");
printf("%60s","|---------------*菜单*-----------------|\n");
printf("%60s","|1--------录入货物的名称、货号和数量---|\n");
printf("%60s","|2-----------输出所有货物信息----------|\n");
printf("%60s","|3---------给定货号,查找货物位置------|\n");
printf("%60s","|4--------------入库-------------------|\n");
printf("%60s","|5--------------出库-------------------|\n");
printf("%60s","|0------------退出系统-----------------|\n");
printf("%60s","|--------------------------------------|\n");
}
//录入基本信息
LNode *input(LNode *head)
//定义一个返回类型为结点类型的函数,因为要返回头指针,方便主函数进行其他操作
{
LNode *s,*r;
printf("输入货物个数:\n");
int i,len;
good x;
scanf("%d",&len);
printf("输入货物名称、货物编号和数量:\n");
r=head;
for(i=1;i<=len;i++){
scanf("%s%s%d",x.name,x.no,&x.num);
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
s->next=NULL;
r->next=s;
r=s;
}
return head;
}
//输出所有货物信息
void print(LNode *head)
{
LNode *p=head->next;
printf("货物名称 货物编号 货物数量\n");
while(p!=NULL)
{
printf("%5s%12s%15d\n",p->data.name,p->data.no,p->data.num);
p=p->next;
}
}
//根据货号查找货物信息
LNode* search(LNode *head,good x)
{
LNode *p=head->next;
while((p!=NULL)&&strcmp(p->data.no,x.no)!=0)
p=p->next;
return p;
}
/*入库函数 过程:
先查找,若存在,修改数量;否则,按货物号有序插入*/
void getin(LNode *head,good x)
{
int In_Num;
LNode *k,
*p=head->next,
*r=head;
printf("输入需要入库的货号:\n");
scanf("%s",x.no);
good y=x;
while((p!=NULL)&&strcmp(p->data.no,x.no)!=0)
p=p->next;
if(p==NULL)
{
printf("该货物不存在,需录入该货物信息,并以编号有序插入\n");
printf("请输入货物名称、货号和数量\n");
scanf("%s%s%d",x.name,x.no,&(x.num));
if(strcmp(x.no,y.no)!=0)
printf("输入货号与查询货号不一致,重来\n");
else
{
k=(LNode *)malloc(sizeof(LNode));
while(r->next!=NULL&&strcmp(r->next->data.no,x.no)<0)
r=r->next;
k->data=x;
k->next=r->next;
r->next=k;
printf("显示入库更新结果:\n");
printf("货物名称 货物编号 货物数量\n");
printf("%5s%12s%15d\n",k->data.name,k->data.no,k->data.num);
printf("输出出库后库存信息:\n");
print(head);
}
}
else
{
printf("请输入入库数量:\n");
scanf("%d",&In_Num);
printf("显示入库结果:\n");
p->data.num+=In_Num;
printf("货物名称 货物编号 货物数量\n");
printf("%5s%12s%15d\n",p->data.name,p->data.no,p->data.num);
printf("输出出库后库存信息\n");
print(head);
}
}
/*出库函数 过程如下:
先查找,若存在,分三种情况:
库存数量有多余:修改数量
库存数量不足:需要用户确认
库存数量刚好:删除
若不存在,给出提示*/
void getout(LNode *head,good x)
{
int Out_Num;
LNode *k;
k=search(head,x);
if(k==NULL)
printf("不存在此编号货物\n");
else
{
printf("请输入出库数量:\n");
scanf("%d",&Out_Num);
if(k->data.num<Out_Num)
printf("输入出库数量错误,请用户确认是否输入正确\n");
else
{
if(k->data.num>Out_Num)
k->data.num-=Out_Num;
else
if(k->data.num==Out_Num)//如果出库数等于原有货物数量,清除该货物
{
LNode *q=head;
while(q->next!=k)
q=q->next;
q->next=k->next;//k元素前面的节点的地址指向k后面节点的地址
free(k); //释放k节点
printf("该货物全部出库,已删除\n");
}
printf("输出出库后剩余货物信息\n");
print(head);
printf("出库成功!\n");
}
}
}
int main()
{
LNode *head;
head=(LNode *)malloc(sizeof(LNode));
head->next=NULL;
good x;
int sel;
LNode *k;
do
{
menu();
printf("请输入实现功能的序号:\n");
scanf("%d",&sel);
switch(sel)
{
case 1: head=input(head);
print(head);
printf("录入完成!\n");
break;
case 2: printf("输出已有货物名称、货号和数量\n");
print(head);break;
case 3: printf("输入需要查询货物的货号\n");
scanf("%s",x.no);
k=search(head,x);
if(k==NULL)
printf("库中该货物不存在,需要检查货号是否输入正确,并重来\n");
else
{
printf("该货号的货物信息如下:\n");
printf("货物名称 货物编号 货物数量\n");
printf("%5s%12s%15d\n",k->data.name,k->data.no,k->data.num);
}
break;
case 4:getin(head,x);
break;
case 5:printf("请输入出库货号:\n");
scanf("%s",x.no);
getout(head,x);
break;
default:break;
}
}while(sel!=0);
return 0;
}
代码已经有了,下面进行各个函数块的详细解释
函数头(有小部分注释)跟menu()和菜单函数就不解释了,肯定都能看懂
menu()函数的打印貌似因为格式不同,跟编译器显示的不一样,不过问题不大,重点都在后面。
主函数中创建了头节点(head),因为head几乎都是作为实参传递给形参,所以就把头节点创建在主函数中,方便其他函数调用。再着就是主函数引用了switch语句,让代码整体有了结构性,给人一看,一目了然。我是把块函数和主函数有机混合,所以不是那么简洁,当然这个可以看自己习惯了,萝卜青菜,各有所爱。
小小提示:因为我为了改漏洞,把自己搞然了,就是因为忽略了这一点,觉得我说的没用的话请自行忽略掉这一个提示,就当是给我自己一个警醒吧,也难怪,脑袋瓜不亮清的人才会搞这种低级错误,没错,说的就是我。
这算是一个链表的基础性知识,在线性表的链式存储中,为了便于单链表的建立并且可以在各种情况下使插入,删除等操作实现能够统一通常在单链表的第一个节点之前加一个头节点(即代码中的head),而在头节点不存储任何数据信息,只是利用他的指针域(指针变量)指向第一个节点,就是通过头指针指向头节点,依次访问所有单链表节点。继续看代码吧。
下面是
case1:input,即把信息输入到链表中
LNode *input(LNode *head)
//定义一个返回类型为结点类型的函数,因为要返回头指针,方便主函数进行其他操作
{
LNode *s,*r; //存储数据的节点
int i,len; //控制循环输入
good x; //定义一个货物类型,以便输入数据
printf("输入货物个数:\n");
scanf("%d",&len);
printf("输入货物名称、货物编号和数量:\n");//以下即为尾插法实现数据信息的录入
r=head; //r始终指向链尾节点,因为head原本指的是尾结点
for(i=1;i<=len;i++){
scanf("%s%s%d",x.name,x.no,&x.num);
s=(LNode*)malloc(sizeof(LNode)); //为新结点申请一份空间
s->data=x; //把数据存入节点的数据域中
s->next=NULL; //置待插入的节点s为链尾节点
r->next=s; //在链尾插入s节点,即r的指针域存放s节点的指针
r=s; //r后移,使r指向新的链尾节点s
}
return head;
}
除了尾接法还有头插法,不过头插法生成的链表顺序与输入顺序相反,不太适合这个系统,因为货物信息是以货号从小到大有序排的,要是使用头插法会造成后面不必要的麻烦,(亲身试验过,刚开始我用头插法写,导致后面的查找和入库函数漏洞大大的,最后放弃了头插法,使用尾接法才修复Bug了),不过,比如一排数据要求你链表实现倒序输出,头插法就很很简单实现,而且不用任何算法就可以,所以不同方法有不同用处,权衡利弊,选择他的最优解。在这我还是把头插法代码写一下,也方便拿去直接两个作比较。
把不同地方写出来了,其他定义之类的都一样
头插法:
//这里就不需要r=head;
for(i=1;i<=len;i++){
scanf("%s%s%d",x.name,x.no,&x.num);
s=(LNode*)malloc(sizeof(LNode)); //为新结点申请一份空间
s->data=x; //把数据存入节点的数据域中
s->next=head->next; //把头结点的指针域赋值给s的指针域先保证不短链(重点)
head->next=s; //再更新head的指针域为下一个s节点的地址,实现在表头head后面插入
}
return head;
如果想写一个既没有返回值又想完成头插法的可以考虑二级指针,即地址的地址。如有兴趣,可以自己尝试一下,在此不再赘述。
尾接法:
r=head; //r始终指向链尾节点,因为head原本指的是尾结点
for(i=1;i<=len;i++){
scanf("%s%s%d",x.name,x.no,&x.num);
s=(LNode*)malloc(sizeof(LNode)); //为新结点申请一份空间
s->data=x; //把数据存入节点的数据域中
s->next=NULL; //置待插入的节点s为链尾节点
r->next=s; //在链尾插入s节点,即r的指针域存放s节点的指针
r=s; //r后移,使r指向新的链尾节点s
}
return head;
由于每次都是将新的节点插入链表尾部,这才需要增加一个指针r来始终指向单链表的尾节点,以便新的节点插入,因为你需要知道你要在谁的后面插入节点,r就像是一个标记点,让你知道r所指的就是尾节点,然后插入后面就OK啦,不过一点不要忘记r用完就得更新,指向刚刚插入的节点,否则r的标记作用就无法体现。
case 2:print() 输出 把库存信息输出
//输出所有货物信息
void print(LNode *head)
{
LNode *p=head->next;
printf("货物名称 货物编号 货物数量\n");
while(p!=NULL)
{
printf("%5s%12s%15d\n",p->data.name,p->data.no,p->data.num);
p=p->next;
}
}
输出就比较简单了,通过实参传递的head,让head头指针指向下一个节点p,当p!=NULL
时,利用while 循环去遍历每个节点,并输出就好了。
不过我刚开始学时候没太懂p=p->next;
的具体含义,想了好久,经过舍友的激情讲解,我懂了,意思就是节点p的指针后移,去访问所有节点,因为每个节点的地址不同,改变p的指针域,即改变了p的数据域信息,从而达到遍历整个链表的目的。
case3:search 查找 按值查找
//根据货号查找货物信息
LNode* search(LNode *head,good x)
{
LNode *p=head->next;
while((p!=NULL)&&strcmp(p->data.no,x.no)!=0)
p=p->next;
return p;
}
运行结果1:
再看一个我写代码当时的一个错误写法:
LNode* search(LNode *head,good x)
{
LNode *p=head;
while((p->next!=NULL)&&strcmp(p->data.no,x.no)!=0)
p=p->next;
return p;
}
运行结果如下:
这样程序就错了,因为明明没有03编号的货物,程序居然没有报错,而且还输出了02编号的货物信息,无疑就是代码的Bug了。
想一想,其实原因就在于第二段代码无法判断while循环是因为匹配到元素而退出循环,还是因为链表节点移动到链尾而退出循环,正因为这样,导致我输入不存在编号03的货物不但不是程序报错说货物不存在,而且还会继续执行代码,输出02货物的信息,是因为链表节点走到链尾而退出循环,导致此时的p节点指的是链尾节点编号为02的货物,所以,最后执行的程序无法实现应有的功能,而第一个代码就不会出现此种情况了。
case4:getin(); 入库
/*入库函数 过程:
先查找,若存在,修改数量;否则,按货物号有序插入*/
void getin(LNode *head,good x)
{
int In_Num; //定义入库数量
LNode *k, //存放重新录入的货物的信息,以便后期插入
*p=head->next, //按照编号遍历查找到数据后,判断货物的存在与否情况
*r=head; //插入操作时,用来遍历完成查找,并做有序插入操作
printf("输入需要入库的货号:\n");
scanf("%s",x.no);
good y=x;
while((p!=NULL)&&strcmp(p->data.no,x.no)!=0)
p=p->next;
if(p==NULL)
{
printf("该货物不存在,需录入该货物信息,并以编号有序插入\n");
printf("请输入货物名称、货号和数量\n");
scanf("%s%s%d",x.name,x.no,&(x.num));
if(strcmp(x.no,y.no)!=0) /*以防输入编号时候和之前不存在要录入的编号不相同,给以提示*/
printf("输入货号与查询货号不一致,重来\n");
else
{
k=(LNode *)malloc(sizeof(LNode));
while(r->next!=NULL&&strcmp(r->next->data.no,x.no)<0)
r=r->next;
k->data=x;
k->next=r->next; //插入操作,注意代码顺序,以防节点自己指向自己,导致断链
r->next=k;
printf("显示入库更新结果:\n");
printf("货物名称 货物编号 货物数量\n");
printf("%5s%12s%15d\n",k->data.name,k->data.no,k->data.num);
printf("输出出库后库存信息\n");
print(head);
}
}
else
{
printf("请输入入库数量:\n");
scanf("%d",&In_Num);
printf("显示入库结果\n");
p->data.num+=In_Num;
printf("货物名称 货物编号 货物数量\n");
printf("%5s%12s%15d\n",p->data.name,p->data.no,p->data.num);
printf("输出出库后库存信息\n");
print(head);
}
}
写这段代码难点在于插入时的逻辑思维,要遍历两次,第一次先判断是否存在该编号货物,若存在则好说,更新货物数量即可,若不存在,需要创建新节点存放新货物,并且还要再遍历一次查找到需要插入的位置,再进行插入操作,插入时还要以防链表节点是否是个指向自己的节点,从而可能导致断链,所以一定要注意代码的前后顺序。
k->next=r->next;
r->next=k;
有心者不妨试试把上面两行代码顺序改变实现一下,看看结果如何,分析分析我在上面解释的意思。自己可以尝试画图解决,不要空想。
case 5:getout(); 出库
/*出库函数 过程如下:
先查找,若存在,分三种情况:
库存数量有多余:修改数量
库存数量不足:需要用户确认
库存数量刚好:删除
若不存在,给出提示*/
void getout(LNode *head,good x)
{
int Out_Num;
LNode *k;
k=search(head,x); //调用search()函数实现按照编号查找,从而进行出库的各项操作
if(k==NULL)
printf("不存在此编号货物\n");
else
{
printf("请输入出库数量:\n");
scanf("%d",&Out_Num);
if(k->data.num<Out_Num) //出库数量大于已有的库存
printf("输入出库数量错误,请用户确认是否输入正确\n");
else
{
if(k->data.num>Out_Num) //正常出库
k->data.num-=Out_Num;
else
if(k->data.num==Out_Num)//如果出库数等于原有货物数量,清除该货物
{
LNode *q=head;
while(q->next!=k)
q=q->next; //查找到k节点的前一个节点
q->next=k->next; //k元素前面的节点的地址指向k后面节点的地址,即可完成删除操作
free(k); //释放k节点内存
printf("该货物全部出库,已删除\n");
}
printf("输出出库后剩余货物信息\n");
print(head);
printf("出库成功!\n");
}
}
}
出库程序就是先按照货号查找到需要出库的货物,然后按照要求分情况讨论,重点在于货物全部出库时需要删除操作,此时就需要先遍历节点,查找到需要删除的k节点的前一个节点,在进行删除,我当时的while循环退出时一直返回的时当前所需删除的节点的指针,导致删除功能又有问题,不过应该从一开始就锁定要遍历查找出所要删除的k节点的前一个节点指针,这下就能进行删除操作了。
因为删除操作必须建立在三个连续的链表节点都知道情况下,才能改变指针的指向,实现删除,最后释放锁删除节点的空间就Ok了。
我是这么写while循环的
while(q->next!=k->next)
q=q->next;
很智障吧,这样的while循环退出时的q,永远是k节点,根本无法实现删除操作,不过再动一下大脑,让程序变成这样
while(q->next!=k)
q=q->next;
此时循环退出的就是k节点的上一个节点指针,这样正如所愿,就可以开心的运行代码,实现删除操作了
以上就是我对这个系统的实现,途中是暴躁过好几次,有些概念不清楚导致看不懂指针的指向各种问题,还去网上找视频看,所以基础知识很重要,要写代码,得先明白代码基本的运作基础,否则就是天方夜谭,写出的代码没有什么意义。整篇文章我写的过于简单(因为我脑子简单),不过按照自己想法来,通俗易懂,希望会对看到这篇文章的小小程序员们有那么一丢丢帮助(虽然我并不觉得会有人看,骗骗自己就行了)。
在此要感谢为我找出Bug并进行解释的朋友,请受小弟一拜。
如果里面有错误的地方或者程序可以精简之处,欢迎大佬评论区指出来留言啊,鄙人才疏学浅,如有表达不当或者程序漏洞之处,望路过的朋友们见谅!