1、需求分析
本实验通过C语言来实现数据结构中的单链表,以及通过函数调用的方式对写好的操作单链表的功能函数进行实验,并完成指定的测试操作。
(1)从键盘输入数字1-12和14来完成对链表的不同操作,如果数字越界,不会执行任何操作,自动请求重新输入。
(2)每个功能执行完成都会给出执行成功的提示或者完成指定功能的数据,执行失败也会给出提示,以及对应的解决办法。
(3)该程序能够完成对链表的初始化,释放,判断是否为空表,测取长度,输出,排序,头插法初始化赋值功能;以及能完成对链表中元素查询、插入、删除和一键测试实验内容、退出功能。
(4) 每一个功能被使用,无论成功失败,均会给用户相应的反馈,使用户可以重新正确的进行操作,但操作本身也具有一定的逻辑性,比如:要使用链表自然是要先初始化链表,这边没有对功能执行的先后顺序做约束,但用户也无需担心,若用户执行顺序出错,程序会给出相应的解决办法以及友情提示,重新按顺序继续执行即可。
2、概要设计
(1)抽象数据类型的定义
typedef struct LNode { //结点类型定义
ElemType data; //数据域
struct LNode *next; //next为指针域,指向直接后继结点
};
(2)主程序流程图
(3)本程序各功能模块之间均不互相调用,单独成块,由主函数一一调用。
3、详细设计
(1)程序开始预编译部分如下:
//头文件
#include <stdio.h>
#include <stdlib.h>
#include "LinklistDef.h"
// 函数结果状态代码
#define OVERFLOW -2
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
//定义类型
typedef char ElemType;
typedef int Status;
typedef struct LNode { //结点类型定义
ElemType data; //数据域
struct LNode *next; //next为指针域,指向直接后继结点
};
typedef struct LNode LNode, *LinkList; // LinkList为指针类型名
(2)程序主要功能函数如下:
Status InitList(LinkList* L);//初始化单链表L
Status DestroyList(LinkList L);//释放单链表L
Status ListEmpty(LNode L);//判断单链表L是否为空表
int ListLength(LNode L);//返回单链表L的元素个数
Status PriorElem(LNode L, ElemType cur_e, ElemType *pre_e);//返回给定数据元素的前驱数据元素的值
Status NextElem(LNode L, ElemType cur_e, ElemType *next_e);//返回给定数据元素的后继数据元素的值
Status DispList(LNode L);//当单链表L不为空时顺序输出L中各数据元素的值
Status GetElem(LNode L, int i, ElemType *e);//获取单链表L的第i个元素
int LocateElem(LNode L, ElemType e);//在单链表L中按元素值查找,返回对应元素第一个匹配到的序号,不存在则返回0
Status ListInsert(LinkList L, int i, ElemType e);//在单链表L的第i个位置(1≤i≤ListLength(L)上插入新的元素e
Status ListDelete(LinkList L, int i, ElemType *e);//删除单链表L的第i(1≤i≤ListLength(L))个元素
Status ClearList(LinkList L);//清空单链表L中的所有数据元素
void CreateList_L2(LinkList L, const ElemType a[],int n);//尾插法
SortList(LinkList L);//将链表L里的元素从小到大排序
void home();//主界面生成函数
void CreateList_L(LinkList L, int n);//头插法
void LinkListTest();//功能测试
(3)函数调用关系图
4、调试分析
(1)Status InitList(LinkList* L)链表初始化函数中使用指向指针的指针作为函数参数传递。
C语言的函数参数默认有两种传递方式:值传递和地址传递。
- 在刚开始使用指针LinkList L作为函数实参时,发现若使用定义好的指针直接做参数,指针没有赋值,指向一个随机的空间,传入函数中初始化,会导致初始化失败。
在指针L未进入函数前:指针是指向随机内存空间的,L.next->next->next=NULL,显然是没有初始化的,因为我们还没有开始初始化,只是定义了一个指针变量L。
在指针L进入函数后,但为出函数:L初始化“成功了”,这个L的地址是0x671628,和之前的L(地址是0x400080)并不是一个空间,看着似乎成功了,但离开初始化函数,主函数里面的L仍然没有初始化。
离开初始化函数:
分析原因:C语言角度:所谓的地址传递本质上仍然是值传递,指针作为函数参数只是将指针变量里面存储的地址传给参数,如果对指针变量存储的地址上的变量数据修改确实可以修改原有的值(虽然实参和形参不是同一个指针,但是他们指向的内存空间是同一块,通过形参指针对该内存空间中的数据进行修改,是可以达到修改效果的),但我们的链表初始化函数是希望对定义在函数外面的指针变量存储的地址进行重新赋值(把开辟好的内存空间的首地址给指针变量L),所以本质上来说函数里面的参数仍然是一个局部指针变量,不是我们在函数外面定义的,因此达不到想要的效果。
2. 改进LinkList L=NULL(同1中理,仍然无法改变)。
3.使用LinkList* L作为实参
进入函数之前:
进入函数之后:通过指向指针的指针作为实参,将LinkList指针变量的内容(随机值)作为(实参或形参)指向的共同内存空间,从而修改L的地址数据。
离开函数:初始化成功!
4.附上函数源码
Status InitList(LinkList *L) {//初始化单链表L
*L = (LinkList) malloc(sizeof(LNode));//创建头结点
if (!*L) exit(OVERFLOW);
(*L)->next = NULL;
return OK;
}
(2)PriorElem(LNode L, ElemType cur_e, ElemType *pre_e)获取给定元素 cur_e前驱数据元素。
- 附上源码
Status PriorElem(LNode L, ElemType cur_e, ElemType *pre_e) {//返回给定数据元素的前驱数据元素的值
LinkList p = L.next;
if (p->data == cur_e) return FALSE;//第一个结点不存在前驱结点,规避第一元素
while (p != NULL) {
*pre_e = p->data;
p = p->next;
if (p != NULL && p->data == cur_e) return OK;//循环开始,直接从第二元素开始判断是不是给定元素
}
return FALSE;
}
代码思路:一开始想着直接从第二个结点判断是不是给定元素,然后外层while的循环中*pre_e的值是之前结点的数据域,如果是的话直接结束循环,拿到给定元素的前驱元素,可以最大程度上节约代码量以及存储空间,不需要存储循环前一个结点的地址。
缺点:一开始代码中是没有这行代码if (p->data == cur_e) return FALSE;首先从第二个元素开始判断是否是给定元素的原因是因为第一个元素就算是给定元素,它也没有前驱元素,然后后面的判断没有找到给定元素,仍然不影响函数的结果,但是如果数据量很大,而第一元素又正好是我们要找的给定元素,本来可以循环一次直接结束,但因为忽略第一元素的存在导致做了大量的无用功的循环,这样是万不可取的,最后我加上了if (p->data == cur_e) return FALSE;在开始循环之前先把第一元素判断掉,再开始循环判断。
5、测试数据与结果
(1)主菜单界面
边界测试:超过指定序号,默认重新输入
(2)初始化链表
(3)释放链表
销毁链表不仅将每个结点free掉,包括头结点也free掉了,所以如果链表没有初始化,不可以销毁链表,否则如图,同样不可以销毁链表两次
(4)判断链表是否为空表
同一判断空表的前提是初始化链表(此后默认链表已完成初始化不一一赘述)
空表情况:
非空表:
(5)获取链表长度
默认空表情况下:
(6)输出单链表
默认空表情况下:不输出任何元素
(7)插入、获取、查找、删除元素(操作失败会提示原因和建议,并重新返回菜单,不会退出程序)
正常插入:
插入边界测试:
正常获取:
获取边界测试:
查找:
正常删除:
删除边界测试:
(8)链表排序(由小到大)
(9)一键测试实验要求的所有结果
实验要求如下:
⑴初始化单链表L;
⑵依次采用尾插法插入a,b,c,d,e元素;
⑶输出链表L;
⑷输出L的长度;
⑸判断L是否为空;
⑹输出链表L的第三个元素;
⑺输出元素d的位置;
⑻在第4个位置上插入元素f;
⑼输出链表L;
⑽删除链表的第3个元素;
⑾输出链表L;
⑿销毁链表L;
⒀头插法建立单链表L,其中L的数据元素值依次为12,56,7,3,89,21,123,20,45,34;
⒁输出单链表L;
⒂将单链表L按数据元素值由小到大排序;
⒃输出排序后新单链表L;
⒄销毁单链表L。
(10)头插法赋值链表
(11)退出主菜单