C语言链表 秒懂
讲解之前弄清两个概念:
顺序表:是在计算机内存中以数组的形式保存的线性表
特点:
1、存储单元的地址连续
2、逻辑结构上相邻的数据元素存储在相邻的物理存储单元
链表 :
特点:
1、物理存储单元上非连续、非顺序
2、数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
3、链表由一系列结点(链表中每一个元素称为结点)组成
4、结点可以在运行时动态生成。
每个结点包括两个部分:
1:存储数据的数据域
2:存储下一个结点地址的指针域。
由以上引出:顺序表和链表统称为线性表
链表和顺序表分别对应链式存储结构与顺序存储结构
顺序存储结构:所有的元素地址都相邻。如图:
此前我们学习的数组叫顺序表,何为顺序,数组的元素的地址 在内存中是连续的、顺序的。
如:a[0]的地址和a[1]的地址是挨在一块的。
链表和顺序表的区别
进入正题之前,介绍一下链表的种类
本次讲解包括单向链表、双向链表和循环链表,前两种链表的区别在于能否双向移动指针。循环链表是单向链表或双向链表通过将头结点和表尾结点连接起来的链表。
看一下对比图吧:
其中单向链表的创建分头插法和尾插法,区别在于:
1、尾插法第一个创建的结点就是头结点,而头插法相反,第一个创建的结点是表尾结点,最后一个创建的结点是头结点。
进入正题:
单向链表
尾插法
创建
:新创建的结点永远是链表的尾巴,尾插法含有头结点。 1、定义如下框架:#include<stdio.h>
#include<stdlib.h>
struct Node{
int data;//存放数据
struct Node *next;//指向下一个结点的指针,见名知意
};
struct Node *creat(void)//创建链表,返回链表头结点
{
}
int main(void)
{
struct Node *head;
return 0;
}
2、creat函数内定义三个指针
struct Node *head,*left,*right;
head保存头结点指针;right指向新创建的结点,即表尾;left指向新结点的前一个结点
3、动态内存内配语句
(struct Node *)malloc(sizeof(struct Node));
//malloc函数返回值为 void *,通过(struct Node *)使指针变量能正确接收;
//sizeof(struct Node)目的使分配一个内存块,其大小为结构体Node的大小
则新结点赋值语句为:
right=(struct Node *)malloc(sizeof(struct Node));//使right指向新结点
假如现在right和left都正确指向不同的结点,那么请问怎么将两结点连接起来呢?
正确语句:
left->next=right;//链接最后一个结点和它的前一个结点
图示:
那么问题又来了,我再创建一个新结点,由要怎么连接呢,用left->next=right; 还行吗?
如图:
答案是否定的,因为left不再是新结点的前一个结点了,如果执行:left->next=right;,那么中间的结点就会丢失,而将倒数第三个和最新的结点连接起来了,那么要怎么保持left一直是倒数第二个结点呢?
那就要在链接了最新结点和前一结点后,立即让left指向最后一个结点,那么新分配结点时,left就保持了指向新结点的前一结点。
联合上一步得代码:
left->next=right;//连接
left=right;//left回到这一次的最后结点,下一次的前一结点
如图:
综上几个步骤,完整的创建新结点,链接结点,更新left指针已经完成,则完整过程如图所示:
代码展示:
right=(struct Node *)malloc(sizeof(struct Node));//使right指向新结点
left->next=right;//链接最后一个结点和它的前一个结点
left=right;//left回到这一次的最后结点,下一次的前一结点
以上代码基本上可以实现链表的建立,但是,还有一个问题要处理,怎样确定头结点呢?
可以用一个if语句来确定头结点
第一次判断,如果head指针为空,那么就将第一个创建的结点赋给head否则就执行left->next=right;进行连接,然后更新就可以了。
以上步骤只能通过增加语句来添加新结点,那么怎么用相同的一组语句建立拥有若干个结点的链表呢?
是的,用循环来完成
看代码:
while(judge==1)//确定是否进入循环,创建链表
{
right=(struct Node *)malloc(sizeof(struct Node));//使right指向新结点
scanf("%d",&right->data);//输入节点数据
if(head==NULL)//head为空,那么是第一次进循环,则将创建的节点作为头结点
head=right;//将第一个结点赋给头结点指针
else
left->next=right;//链接最后一个结点和它的前一个结点
left=right;//left回到这一次的最后结点,下一次的前一结点
puts("\n\ndo you want to continue 1 or 0");
scanf("%d",&judge);//询问用户是否继续创建,1继续,0停止
}
做事有始有终,链表需要有结束标志,这个标志就是:NULL(空指针),通过上面的循环语句可知,结束循环后right指针永远是指向最后一个节点的,所以收尾就由它来完成,收尾语句:
right->next=NULL;
至此,所有步骤讲解完成,接下来,我们用完整代码展示创建步骤:
#include<stdio.h>
#include<stdlib.h>
struct Node{
int data;//存放数据
struct Node *next;//指向下一个结点的指针,见名知意
};
struct Node *creat(void);
struct Node *creat(void)//创建链表,返回链表头结点
{
struct Node *head,*left,*right;//定义3结构体指针
head=left=right=NULL;//全赋空值
int judge;//循环决定性变量
puts("do you want to continue 1 or 0");
scanf("%d",&judge);
while(judge==1)//确定是否进入循环,创建链表
{
right=(struct Node *)malloc(sizeof(struct Node));//使right指向新结点
scanf("%d",&right->data);//输入节点数据
if(head==NULL)//head为空,那么是第一次进循环,则将创建的节点作为头结点
head=right;//将第一个结点赋给头结点指针
else
left->next=right;//链接最后一个结点和它的前一个结点
left=right;//left回到这一次的最后结点,下一次的前一结点
puts("\n\ndo you want to continue 1 or 0");
scanf("%d",&judge);//询问用户是否继续创建,1继续,0停止
}
right->next=NULL;//链表收尾
return head;//返回头指针
}
int main(void)
{
struct Node *head;
head=creat();//接收头指针
return 0;
}
运行结果:
插入结点
明白链表的创建了,我们进一步学习在已创建的链表中插入一个结点。
先分析一张图:
通过这张图解,我相信大家已经明白怎样插入的了
1、先让s的next指向left的next,也就是s的next指向right
2、left的next指向s
这样就能够插入在left和right之间了,看代码:
s->next=left->next;
left->next=s;
还要解决一个问题:怎样保存left和right的值?
答:1、首先left和right应指向同一节点
2、再让right指向下一节点
如果你明白了,那么我们来写一个插入节点的函数:
struct Node *insert(struct Node *head)
{
struct Node *left,*right,*s;//s为新结点指针
int ist;
right=head;
s=(struct Node *)malloc(sizeof(struct Node));
if(s==NULL)//如果内存分配失败则退出程序
{
puts("插入失败\n");
return NULL;
}
puts("input the data of new node");
scanf("%d",&s->data);//新节点的数据
puts("input the data of searching");
scanf("%d",&ist);//要寻找的节点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if(head==NULL)//本身是空表的情况
return s;//s作为头结点,也是表中唯一结点
else if(head==right)
{
s->next=head;//s作为头结点
return s;//返回s作为新的头指针
}
else if(right==NULL)//没找到数据为ist的结点
{
left->next=s;//新结点作为表尾结点
s->next=NULL;//添加链表结束标志
}
else//找到ist的情况
{
s->next=left->next;//连接s和right
left->next=s;//连接left和s
}
return head;//返回插入后的头指针
}
运行结果:
删除结点
删除和插入类似,比插入简单, 确定left和right位置后,仅仅需要 一条连接语句和 一条释放内存语句即可完成删除.left->next=right->next;//left的next跨越right,连接right->next;
free(right);//释放right,完成删除。
接下来我们来写一个删除函数夯实学习成果;
struct Node *delet(struct Node *head)
{
struct Node *left,*right;
int ist;
right=left=head;
puts("input the data of deleting");
scanf("%d",&ist);//要删除的结点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if((head==NULL))//空表或头结点就是待删除的结点的情况
{
return NULL;//返回空表
}
else if(right==head)//表头为要删除的结点
{
struct Node *T=right->next;
free(right);
return T;//要是表头为要删除的结点,则后一个结点作为新表头
}
else if(right==NULL)//没找到数据为ist的结点
{
return head;//没有要删除的,退出;
}
else//找到ist的情况
{
left->next=right->next;//left的next跨越right,连接right->next;
free(right);//释放right,完成删除。
}
return head;//返回删除后的头结点
}
运行结果:
最后,我们整合创建,插入,删除于一体,将完整版代码粘贴出来
#include<stdio.h>
#include<stdlib.h>
struct Node{
int data;//存放数据
struct Node *next;//指向下一个结点的指针,见名知意
};
struct Node *creat(void);
struct Node *insert(struct Node * head);
struct Node *delet(struct Node *head);
void print(struct Node *head);
struct Node *creat(void)//创建链表,返回链表头结点
{
struct Node *head,*left,*right;//定义3结构体指针
head=left=right=NULL;//全赋空值
int judge;//循环决定性变量
puts("do you want to continue 1 or 0");
scanf("%d",&judge);
while(judge==1)//确定是否进入循环,创建链表
{
right=(struct Node *)malloc(sizeof(struct Node));//使right指向新结点
scanf("%d",&right->data);//输入节点数据
if(head==NULL)//head为空,那么是第一次进循环,则将创建的节点作为头结点
head=right;//将第一个结点赋给头结点指针
else
left->next=right;//链接最后一个结点和它的前一个结点
left=right;//left回到这一次的最后结点,下一次的前一结点
puts("\n\ndo you want to continue 1 or 0");
scanf("%d",&judge);//询问用户是否继续创建,1继续,0停止
}
right->next=NULL;//链表收尾
return head;//返回头指针
}
struct Node *insert(struct Node *head)
{
struct Node *left,*right,*s;//s为新结点指针
int ist;
right=head;
s=(struct Node *)malloc(sizeof(struct Node));
if(s==NULL)//如果内存分配失败则退出程序
{
puts("插入失败\n");
return NULL;
}
puts("input the data of new node");
scanf("%d",&s->data);//新节点的数据
puts("input the data of searching");
scanf("%d",&ist);//要寻找的节点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if(head==NULL)//本身是空表的情况
return s;//s作为头结点
else if(head==right)
{
s->next=head;//s作为头结点
return s;//返回s作为新的头指针
}
else if(right==NULL)//没找到数据为ist的结点
{
left->next=s;//新结点作为表尾结点
s->next=NULL;//添加链表结束标志
}
else//找到ist的情况
{
s->next=left->next;//连接s和right
left->next=s;//连接left和s
}
return head;//返回插入后的头指针
}
struct Node *delet(struct Node *head)
{
struct Node *left,*right;
int ist;
right=left=head;
puts("input the data of deleting");
scanf("%d",&ist);//要删除的结点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if((head==NULL))//空表或头结点就是待删除的结点的情况
{
return NULL;//返回空表
}
else if(right==head)//表头为要删除的结点
{
struct Node *T=right->next;
free(right);
return T;//要是表头为要删除的结点,则后一个结点作为新表头
}
else if(right==NULL)//没找到数据为ist的结点
{
return head;//没有要删除的,退出;
}
else//找到ist的情况
{
left->next=right->next;//left的next跨越right,连接right->next;
free(right);//释放right,完成删除。
}
return head;//返回删除后的头结点
}
void print(struct Node *head)
{
while(head)
{
printf("%d ",head->data);
head=head->next;
}
}
int main(void)
{
struct Node *head;
puts("创建节点:");
head=creat();//接收头指针
puts("\n\n插入节点:");
head=insert(head);
puts("\n\n删除节点:");
head=delet(head);
if(head) //非空表则打印
{ puts("\n\n最后链表:");
print(head);
}
return 0;
}
运行结果如图:
一口气写这么多,挺辛苦的,先休息会
头插法
头插法是倒序链表,最早添加的结点,在创建完成链表后,变成表尾,最后创建的结点称为表头
创建
对比尾插法和头插法代码left->next=right;//尾插法
right->next=left;//头插法
有了尾插法的基础我们直接来看代码部分吧!
struct Node *creat(void)//创建链表,返回链表头结点
{
struct Node *left,*right;左指针,右指针(最新结点)
left=right=NULL;//全赋空值
int judge;//循环决定性变量
puts("do you want to continue 1 or 0");
scanf("%d",&judge);
while(judge==1)//确定是否进入循环,创建链表
{
right=(struct Node *)malloc(sizeof(struct Node));//使right指向新结点
scanf("%d",&right->data);//输入节点数据
right->next=left;//链接最后一个结点和它的前一个结点
left=right;//left回到这一次的最后结点,下一次的前一结点
puts("\n\ndo you want to continue 1 or 0");
scanf("%d",&judge);//询问用户是否继续创建,1继续,0停止
}
return right;//返回头指针
}
运行结果:
插入结点
头插法与尾插法插入相同,这里要注意,之所以插入语句相同,是因为,left和right始终保持一前一后状态这里看图可能有些别扭,因为是在头插创建的链表的基础上从后往前看的,left一端为表头端。
s->next=left->next;//头插法,插入语句
left->next=s;//完成插入
还可以是:
s->next=right;//right和left->next是指向同一结点,所以可以这样替换
left->next=s;
代码:
struct Node *insert(struct Node *head)
{
struct Node *left,*right,*s;//s为新结点指针
int ist;
right=head;
s=(struct Node *)malloc(sizeof(struct Node));
if(s==NULL)//如果内存分配失败则退出程序
{
puts("插入失败\n");
return NULL;
}
puts("input the data of new node");
scanf("%d",&s->data);//新节点的数据
puts("input the data of searching");
scanf("%d",&ist);//要寻找的节点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if(head==NULL)//本身是空表的情况
return s;//s作为头结点
else if(head==right)
{
s->next=head;//s作为头结点
return s;//返回s作为新的头指针
}
else if(right==NULL)//没找到数据为ist的结点
{
left->next=s;//新结点作为表尾结点
s->next=NULL;//添加链表结束标志
}
else//找到ist的情况
{
s->next=left->next;//连接s和right
left->next=s;//连接left和s
}
return head;
}
运行结果:
删除结点
与尾插法同理,什么都不变,仅仅看图有点别扭。
struct Node *delet(struct Node *head)
{
struct Node *left,*right;
int ist;
right=left=head;
puts("input the data of deleting");
scanf("%d",&ist);//要删除的结点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if((head==NULL))//空表的情况
{
return NULL;//返回空表
}
else if(right==head)//头结点为要删除结点
{
struct Node *p=right->next;
free(right);
return p;
}
else if(right==NULL)//没找到数据为ist的结点
{
return head;//没有要删除的,退出;
}
else//找到ist的情况
{
left->next=right->next;//left的next跨越right,连接right->next;
free(right);//释放right,完成删除。
}
return head;//返回删除后的头结点
}
运行结果:
接下来,结三部分于一体,看一下头插法的完整部分代码:
#include<stdio.h>
#include<stdlib.h>
struct Node{
int data;//存放数据
struct Node *next;//指向下一个结点的指针,见名知意
};
struct Node *creat(void);
struct Node *insert(struct Node * head);
struct Node *delet(struct Node *head);
void print(struct Node *head);
struct Node *creat(void)//创建链表,返回链表头结点
{
struct Node *left,*right;左指针,右指针(最新结点)
left=right=NULL;//全赋空值
int judge;//循环决定性变量
puts("do you want to continue 1 or 0");
scanf("%d",&judge);
while(judge==1)//确定是否进入循环,创建链表
{
right=(struct Node *)malloc(sizeof(struct Node));//使right指向新结点
scanf("%d",&right->data);//输入节点数据
right->next=left;//链接最后一个结点和它的前一个结点
left=right;//left回到这一次的最后结点,下一次的前一结点
puts("\n\ndo you want to continue 1 or 0");
scanf("%d",&judge);//询问用户是否继续创建,1继续,0停止
}
return right;//返回头指针
}
struct Node *insert(struct Node *head)
{
struct Node *left,*right,*s;//s为新结点指针
int ist;
right=head;
s=(struct Node *)malloc(sizeof(struct Node));
if(s==NULL)//如果内存分配失败则退出程序
{
puts("插入失败\n");
return NULL;
}
puts("input the data of new node");
scanf("%d",&s->data);//新节点的数据
puts("input the data of searching");
scanf("%d",&ist);//要寻找的节点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if(head==NULL)//本身是空表的情况
return s;//s作为头结点
else if(head==right)
{
s->next=head;//s作为头结点
return s;//返回s作为新的头指针
}
else if(right==NULL)//没找到数据为ist的结点
{
left->next=s;//新结点作为表尾结点
s->next=NULL;//添加链表结束标志
}
else//找到ist的情况
{
s->next=left->next;//连接s和right
left->next=s;//连接left和s
}
return head;
}
struct Node *delet(struct Node *head)
{
struct Node *left,*right;
int ist;
right=left=head;
puts("input the data of deleting");
scanf("%d",&ist);//要删除的结点的数据
while((right!=NULL)&&(right->data!=ist))//找到节点,或没找到退出循环
{
left=right;//指向同一结点
right=right->next;//right移动至下一结点,则left和right就一前一后了
}
if((head==NULL))//空表或头结点就是待删除的结点的情况
{
struct Node *p=right->next;
free(right);
return p;//返回空表
}
else if(right==head)//头结点为要删除结点
{
struct Node *p=right->next;
free(right);
return p;
}
else if(right==NULL)//没找到数据为ist的结点
{
return head;//没有要删除的,退出;
}
else//找到ist的情况
{
left->next=right->next;//left的next跨越right,连接right->next;
free(right);//释放right,完成删除。
}
return head;//返回删除后的头结点
}
void print(struct Node *head)
{
while(head)
{
printf("%d ",head->data);
head=head->next;
}
}
int main(void)
{
struct Node *head;
puts("创建节点:");
head=creat();//接收头指针
puts("\n\n插入节点:");
head=insert(head);
puts("\n\n删除节点:");
head=delet(head);
if(head!=NULL)
{
puts("\n\n最后链表:");
print(head);
}
return 0;
}
运行结果:
郑重声明:
1、学者水平有限,若有不足之处,请指出。
2、未经允许,转载请贴上出处
3、本人系中职生,时间有限(对口高考),所以接下来的循环链表和双向链表可能会停更,若更新也至少得7月份往后。