实验二单链表的基本操作(必做,设计性实验)
- 实验目的
了解线性表的链式存储结构和顺序存取特性,熟练掌握线性表的链式存储结构的C语言描述方法,熟练掌握动态链表的基本操作查找、插入、定位等,能在实际应用中选择适当的链表结构。掌握用链表表示特定形式的数据的方法,并能编写出有关运算的算法。
- 实验内容
- 已知线性表中的元素以值递增有序排列,并以单链表作存储结构。试写一高效算法,删除表中所有值大于mink且小于maxk的元素(若表中存在这样的元素)同时释放被删结点空间,并分析你的算法的时间复杂度(注意:mink和maxk是给定的两个参变量,它们的值可以和表中的元素相同,也可以不同)。
- 完成单链表上的如下操作:
①逆序建立单链表
②遍历单链表(输出单链表每个元素的值)
③在单链表第5个元素前插入一个值为999的元素.
④删除单链表第5个元素.
- 问题描述
(说明你选做的题目及要求)
题目:(1)删除单链表中介于mink和maxk之间的元素;(2)对单链表进行建立、遍历、插入和删除的操作
题目要求:
(1)线性表中的元素以值递增有序输入,并以单链表作存储结构。写一高效算法,删除表中所有值大于mink且小于maxk的元素(若表中存在这样的元素)同时释放被删结点空间,并分析你的算法的时间复杂度(注意:mink和maxk是给定的两个参变量,它们的值可以和表中的元素相同,也可以不同)。
(2)逆序建立一个单链表,对此链表完成在单链表的第5个元素前插入一个值为999的元素、删除单链表第5个元素和遍历单链表(输出单链表每个元素的值)的操作。
- 数据结构定义
(说明你算法中用到的数据结构、数据类型的定义)
数据结构:用到的数据结构是线性表,选择的存储结构是链式存储结构
数据类型的定义:整型变量data存储链表结点的数据域,next 存储链表结点的指向后继结点的指针域。节点类型用Lnode表示,指向结点类型的指针用LinkList表示。节点类型定义如下:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
- 算法思想及算法设计
(先文字说明算法的思想,然后给出类C语言算法)
题目(1):
算法的思想:①首先输入链表元素个数,正序构造一个单链表,按照递增顺序向单链表中赋值并遍历单链表;②然后输入mink和maxk的值,从链表的首元结点开始比较,直到当前结点的data>mink,若当前结点的data<maxk,则删除结点并释放节点空间,继续向后查找并重复前面的操作,直到当前结点的data>=maxk或表中元素均比较完;③最后遍历链表以确定操作是否成功。
类C语言算法:
Status ListDelete_L(LinkList L,int mink,int maxk){
p=L; q = L->next;//指向第一个结点
while(q->data<=mink&&q)
{
p = q; q = q->next;
if(!q->next) return OK;
}
while(q->data<maxk)
{
p->next = q->next;//p=L避免讨论删除的位置在第一个结点的情况
free(q);
q = p->next;//所以要根据它的前一个指针重新定位指向
if(!q) break;
}
return OK;
}
题目(2):
算法思想:①首先输入表长,用尾插法创建单链表,即从一个空表开始,生成新结点并输入一个数据,将读取的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到结束为止,遍历链表;②然后从链表的首元结点开始查找,直到找到第4个结点,在第4个结点后插入数据域中储存的数为999的新结点并遍历链表;③最后从链表的首元结点开始查找,直到找到第5个结点,删除第5个结点并释放结点空间并遍历链表以验证操作是否成功。
类C语言算法:
①逆序建立链表
Status CreatList_L(LinkList L,int n){
//用尾插法建立逆序单链表
p = L;
for(int i=0;i<n;i++){ //按照递增的顺序向链表中赋值
q = (LinkList)malloc(LEN); //生成新结点
scanf(&q->data);
q->next = L->next;//p永远指向当前链表的最后一个结点 q指向当前新生成的一个结点
L->next = q;
}
return OK;
}
②遍历链表
Status TraveList_L(LinkList L){
p = L->next;//首先指向第一个结点 p = L是初始指向了头结点
if(!L->next) return ERROR; //空链表
while(p){ //输出链表
printf(p->data); p = p->next;
}
return OK;
}
③插入结点
Status ListInsert_L(LinkList L,int i,ElemType e){
p = L; j = 0;
while(p&&j<i-1){
p = p->next; ++j; //寻找第i-1个结点
}
if(!p||j>i-1) return ERROR;
s = (LinkList)malloc(LEN); //生成新结点
s->data = e; s->next = p->next; p->next = s; //将新节点插入链表
return OK;
}
④删除链表结点
Status ListDelete_L(LinkList L,int i){
j = 0; p = L;//指向头结点
while(p->next&&j<i-1){
p = p->next; ++j;
}
if(!(p->next)||j>i-1)
return ERROR;
q = p->next; p->next = q->next; //删除结点后再将链表连接
free(q); //释放结点空间
return OK;
}
- 实验代码
(即C语言程序)
详见电子版
- 算法测试结果
(说明测试数据,粘贴实验结果图)
题目(1):
删除的值介于表中
删除的值介于表头
删除的值介于表尾
maxk不大于表头元素
mink不小于表尾元素
题目(2): 链表元素{1 2 3 4 5 6 7 8}
- 分析与总结
(1)算法复杂度分析及优、缺点分析
(说明你编写算法的复杂度,算法的优点和缺点有哪些)
算法复杂度分析:
①删除单链表中介于mink和maxk之间的元素
CreatList_L(LinkList L,int n), TraveList_L(LinkList L), ListDelete_L(LinkList L,int mink,int maxk)
算法时间复杂度均为O(n)
②对单链表进行建立、遍历、插入和删除的操作
CreatList_L(LinkList L,int n), ListDelete_L(LinkList L,int i),
TraveList_L(LinkList L), ListInsert_L(LinkList L,int i,ElemType e)
算法时间复杂度均为O(n)
优点:
①已知链表中元素插入删除位置的情况下,在单链表中删除一个结点时,仅需要修改指针而不需要移动元素;删除结点后可以把这个节点的空间释放,更加合理的使用了空间;
②在删除结点的功能函数中令p=L可以避免讨论删除的位置在第一个结点的情况,程序可读性更好;
③元素的存储单元是任意的,可连续也可不连续;④线性表不需要限定长度,不会出现内存溢出的情况。
缺点:
①存放元素时需要另外开辟一个指针域的空间;
②访问元素时,不支持随机访问,访问第n个数据元素,必须先得到第n-1个元素的地址,因此访问任何一个结点必须从头结点开始向后迭代寻找,直到找到这个目标结点为止。
(2)实验总结
(说明你怎么解决实验中遇到的问题,有什么收获)
①问题:如何建立逆序单链表
解决办法:最初我通过教材了解到可以用尾插法建立逆序链表,虽然依照教材成功建立,单我始终不理解其中原理,后通过查找资料并结合示意图理解了其中原理。尾插法,就是把新加入的节点插入到上一个节点的尾部(头插法是把新加入的节点插入到上一个节点的头部),next存储下一个节点位置的地址。开始时,初始化定义头节点:
head -> next = NULL; //表示头节点的下一个节点为空,就是该链表只有一个头节点
头插法要把每一个新加入的节点插入到上一个节点的尾部。
r = head; //将头节点赋值给r,r记录每次插入变换后尾部的信息
申请一个节点A1,将A1按照尾插法插入到链表当中:
r -> next = p; //将 r (当前还是代表头节点的地址)的下一个节点指向p
r = p; //将原本表示头部节点地址的指针赋值为新插入的A1,也就是说 r 当前表示为节点A1
当插入节点A2时,依然执行这两行代码,由于r是上一个新插入的节点,所以A2插入到了A1的尾部,插入后同样将r赋值给当前插入节点的地址,不断地执行上述过程,把新来的节点插入到上一个尾部,把最后一个节点的next值赋为空,实现尾插法代码的实现。
②问题:在题目(1)中对于删除表头元素没有特殊处理,使程序无限循环
解决办法:由p=q=L->next换为p=L;q=L->next;这样在下面进行p->next = q->next;操作时对于删除表头元素能够正常连接。
收获:
①首先,在删除单链表中介于mink和maxk之间的元素的实验中,我学会了如何遍历一个单链表,并且根据给定条件删除符合条件的节点。这个实验不仅加深了我对链表的理解,还提高了我处理链表操作的能力。我了解到,在删除链表节点时,需要注意保存好前一个节点的指针,以便正确地重连链表,避免断链。
②其次,通过对单链表进行建立、遍历、插入和删除操作的实验,我进一步巩固了链表相关的基本操作。在建立链表的过程中,我学会了如何正确地连接每个节点,并保持链表的完整性。遍历链表时,我能够逐个访问链表中的节点,并对其进行处理。此外,我也学会了如何在指定位置插入新的节点,并且在删除节点时,需要注意前后节点之间的指针调整,以保持链表的正确性。
③通过这些实验,我深刻认识到链表是一种非常灵活和高效的数据结构,能够方便地进行增删改查等操作。与数组相比,链表的插入和删除操作时间复杂度更低,因为不需要移动其他元素的位置。这对于处理大量数据时,可以提高程序的效率和性能。
④此外,在实验过程中,我也学会了合理地使用指针,并注意内存管理的问题。链表的节点需要手动分配和释放内存,因此,在插入和删除节点时,需要小心地释放被删除节点的内存,避免出现内存泄漏等问题。
⑤链表的创建、插入和删除都涉及到内存的申请和释放,在实验过程中,我意识到了及时释放不再需要的节点内存的重要性,避免出现内存泄漏的问题。
⑥最后,我也加深了对数据结构和算法的理解,我的编程能力和问题解决能力有所提高。
总结起来,完成上述实验让我对单链表的建立、遍历、插入和删除等操作有了更深入的理解和熟练掌握。这将对我的编程能力和算法思维有所帮助,并为我在日后的学习和工作中应用链表等数据结构打下了坚实的基础。