数组的局限性
使用数组时必须事先定义好数组长度(即元素个数),这个长度一经定义就是固定不变的。如果事先难以确定元素个数,则必须把数组长度定义得足够大,这将占用许多内存。另一方面,在数组中若要插入或删除某个元素,需要移动插入点或删除点后面所有的数组元素,这将运行大量时间。
链表的概念
链表是一种存储空间能动态进行增长或缩小的数据结构。
链表主要用于两个目的:
一是建立不定长度的数组。
二是链表可以在不重新安排整个存储结构的情况下,方便且迅速地插入和删除数据元素。
由于这些原因,链表广泛地运用于数据管理中。
链表原理
链表实际上是一些以指针为链接的结构体。每一个结点都是一个结构体,其中包含着所要储存的数据(数据域),和一个指针,指向下一个结点。最后一个结点的指针指向NULL。
以一个储存学号和姓名的结构体为例:
typedef struct student
{
int num;
char name[20];
struct student* next;//next是结构体指针
}STU;
链表的分类
单链表和双链表
链表分为单链表和双链表两种。上图所示的就是单链表的结构。
双链表与单链表的区别是:双链表中不仅存放了一个指向下一个节点的指针,还存放了一个指向前一个节点的指针。
typedef struct student
{
int num;
char name[20];
struct student* next;//next是结构体指针
struct stuent* prev;
}STU;
头结点的prev为NULL,尾结点的next为NULL。
双链表的优点:与单链表相比,双链表可以从前向链和后向链遍历整个链表,这样简化了链表排序方法及运算。同时,当一个链数据受损(如数据库设备故障)时可以根据另一个链来恢复它。
循环链表
若单链表的尾结点指向的不是NULL,而是头结点,则为循环单链表。
若双链表的头结点的prev指向尾结点,尾结点的next指向头结点,则为循环双链表。
链表的创建
因为所有链表的创建和使用大同小异,下面以单链表为例。
利用动态内存分配可以产生新结点的内存单元。
例如:
STU* p;//结构体指针,在这里可以称为是链表指针
p=(STU*)malloc(sizeof(STU));
调用malloc realloc 内存分配函数和free释放函数时,需要包含命令:
#include <stdlib.h>
int main()
{
int n;
scanf("%d",&n);
STU* head=NULL;//开始的时候头结点为空
STU* new1=NULL;//用来指向新的结点
for(;n>0;n--)
{
new=(STU*)malloc(sizeof(STU));//为新节点申请一块空间
printf("请输入数据:");
scanf("%d %s",&new1->num,&new1->name);
Link(&head,new1);//调用连接函数,把新的结点连接到链表。
//连接函数的实现见下文
}
return 0;
}
链表的连接分为头插法和尾插法。两者只是在将新的结点链接到链表时的连接方式不同。
尾插法创建链表
尾插法即把新的结点链接到链表的末尾。如下图:
代码如下:
void Link(STU** head,STU* new1);//连接函数,把新的结点连接到已有的链表 (尾插法)
int main()
{
int n;
scanf("%d",&n);
STU* head=NULL;//开始的时候头结点为空
STU* new1=NULL;//用来指向新的结点
for(;n>0;n--)
{
new1=(STU*)malloc(sizeof(STU));//为新节点申请一块空间
printf("请输入数据:");
scanf("%d %s",&new1->num,&new1->name);
Link(&head,new1);//调用连接函数,把新的结点连接到链表。
}
return 0;
}
//new是特殊字,故new1代替
void Link_tail(STU** head,STU* new1)//连接函数,把新的结点连接到已有的链表 (尾插法)
{
STU* move=*head;
if(*head==NULL)//当第一个结点加入链表时
{
*head=new1;
new1->next=NULL;
}
else
{
while(move->next!=NULL)//move的下一个结点不是NULL,说明还没到尾结点
{
move=move->next;
}
//经过移动后move指向尾结点
move->next=new1;
new1->next=NULL;
}
}
注意这里用了指向指针的指针,即二级指针。因为在传参的时候,如果使用一级指针,函数里的head只是head的一个副本,从而不能对head进行修改。因此使用二级指针,传递的时head的地址,从而达到更新head的目的。
头插法创建链表
头插法是将新的结点加在头结点之后,第二个结点之前。
原来:
插入后:
新结点作为链表的第一个元素。因此,头结点不储存数据。
头插法的连接代码如下:
void Link_head(STU** head,STU* new1)//连接函数,把新的结点连接到已有的链表 (头插法)
{
new1->next=*head->next;
*head->next=new1;
}
链表的遍历
链表遍历算法的实现步骤为:
①令指针move指向L的开始结点。
②若move为NULL,表示已到链尾,遍历结束。
③令move指向直接后继结点,即move=move->next。重复②~③步骤直至遍历结束。
在尾插法时已经用过了数组的遍历。
代码如下:
STU* move=head;//move开始等于头结点
while(move!=NULL)//到达尾结点的时候退出循环
{
//根据需要进行操作(省略)
move=move->next;//到达尾结点,move=NULL
}
应用遍历输出链表
void print_Link(STU* head)
{
STU* move=head;
while(move!=NULL)
{
printf("%d %s",move->num,move->name);
move=move->next;
}
}
此时不需要修改head,用一级指针即可。
销毁链表
void Link_free(STU** head)
{
STU* move=head;
while(*head!=NULL)
{
move=*head;
*head=move->next;//让head保存下一个结点
free(move);//释放move指向的动态空间
}
}
查找结点
//返回第i个结点
int search_elm(STU* head,int i,STU** ans)
{
STU* move=head;
while(move!=NULL && i>0)
{
move=move->next;
i--;
}
if(move==head || move==NULL) return 0;//第i个结点不存在
*ans=move;
return 1;//操作成功返回1
}