我们之前使用数组来存放数据,但使用数组时,要先指定数组中包含元素的个数,即数组长度。如果向数组中加入的元素个数超过了数组长度,便不能正确保存其中内容。且这种方式特别浪费空间。这时就希望有一种存储方式,其存储元素的个数是不受限制的,当要添加更多元素时,存储的个数会随之增加,这种方式被称为链表。链表是一种常见的数据结构。
数据的存储结构有两种:
专业术语:
首结点:第一个有效节点
尾节点:最后一个有效结点
头节点:第一个有效结点之前的结点,头结点不存放有效数据,且头结点主要为了方便对链表的操作
头指针:指向头结点的指针变量,且存放头结点的地址
尾指针:指向尾节点的指针变量
链表图解如下:
每一个结点都有两部分组成。左边的部分用来存放具体的数值,右边的部分需要存储下一个结点的地址,可以用指针实现(称为后继指针)
定义一个简单的结构体存储节点:
struct node
{
int data;//数据域
struct node * next;//指针域
};
上面的代码中,我们定义了一个叫做node的结构体类型,这个结构体类型有两个成员,第一个成员是整形data,用来存储具体的数值;第二个成员是指针,用来存储下一个结点的地址,因为下一个结点的类型也是struct node ,所以这个指针的类型也必须是struct node *类型的指针。
注:确定一个数组,需要三个参数:元素个数,首地址,长度
如若通过一个函数对链表进行处理,至少需要几个参数?
只需要一个参数:头指针
通过头指针可以推算出链表的其他所有参数(头指针存放头结点的地址,而头结点和其后结点数据类型相同)
单链表:每个结点只有一个指针,每个结点的指针都指向下一结点,所有结点单线联系
双链表:添加了一个指针域,通过两个指针域,分别指向节点的前结点和后节点
循环链表:最后一个结点的指针指向链表头结点,头尾相连,形成一个环形的数据链;
单链表的建立就是在程序运行的过程中,从无到有的建立一个链表,即一个一个分配结点的内存空间,输入结点的数据,建立结点间的相连关系
头插法:始终让每个新结点放在链表中第一个位置,即对链表进行遍历时先遍历的是最后加入链表的结点
尾插法:每次把新结点放在链表的最后一个位置
如何建立链表呢?
struct node *head;
head=NULL;//头指针为空
struct node *p;
//动态申请一个内存的空间,用来存放一个结点,并用临时指针p指向这个结点
p=(struct node *)malloc(sizeof(struct node));
scanf("%d",&a);
p->data=a;//将数据存储到当前结点的data域中
p->next=NULL;//设置当前结点的后继指针为空,也就是当前结点的下一个结点为空
注:上面的代码中,我们发现了一个符号->,它叫做结构体运算符,用来访问结构体成员。因为此处p是一个指针,所以不能使用.号访问内部成员,而要使用->。
if(head=NULL)
head=p;//如果这是新创建的结点,则将头指针指向这个结点
else
q->next=p;// 如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
q=p;//指针q也指向当前结点
完整的创建链表的代码:
#include
#include
struct node//创建一个结构体来表示链表的节点类型
{
int data;
struct node * next;
};
int main()
{
struct node *head,*p,*q,*t;
int i,n,a;
scanf("%d",&n);
head=NULL;//头指针初始为空
for(i=1;i<=n;i++)//循环读入n个数
{
scanf("%d",&a);
p->data=a;//将数据存储到当前结点的data域中
p->next=NULL;//设置当前结点的后继指针为空,也就是当前结点的下一个结点为空
if(head=NULL)
head=p;//如果这是新创建的结点,则将头指针指向这个结点
else
q->next=p;// 如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
q=p;//指针q也指向当前结点
}
}
将链表创建好后,对链表中的数据进行遍历并进行输出
//输出链表中的所有数
t=head;
while(t!=NULL)
{
printf("%d",t->data);
t=t->next;//继续下一个结点
}
free(p);
在1位置插入结点的具体步骤:
t=head;//从链表头部开始遍历
scanf("%d",&a);//读入待插入的数
while(t!=NULL) //当没有达到链表尾部的时候进行循环
{
if(t->next->data>a)//如果当前结点下一个结点的值大于待插入数,将数插入到中间
{
p=(struct node *)malloc(sizeof(struct node));// 当前结点的后继指针指向新增结点
p->data=a;
p->next=t->next;// 新增结点的后继指针指向当前结点的后继指针所指向的结点
t->next=p;// 当前结点的后继指针指向新增结点
break;
}
t=t->next;//继续下一个结点
}
完整的单链表的插入代码:
注:将上文的创建,插入,遍历合起来即为一个完整的单链表的插入代码
#include
#include
struct node//创建一个结构体来表示链表的节点类型
{
int data;
struct node * next;
};
int main()
{
struct node *head,*p,*q,*t;
int i,n,a;
scanf("%d",&n);
head=NULL;//头指针初始为空
for(i=1;i<=n;i++)//循环读入n个数
{
scanf("%d",&a);
p->data=a;//将数据存储到当前结点的data域中
p->next=NULL;//设置当前结点的后继指针为空,也就是当前结点的下一个结点为空
if(head=NULL)
head=p;//如果这是新创建的结点,则将头指针指向这个结点
else
q->next=p;// 如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
q=p;//指针q也指向当前结点
}
}
scanf("%d",&a);//读入待插入的数
while(t!=NULL) //当没有达到链表尾部的时候进行循环
{
if(t->next->data>a)//如果当前结点下一个结点的值大于待插入数,将数插入到中间
{
p=(struct node *)malloc(sizeof(struct node));// 当前结点的后继指针指向新增结点
p->data=a;
p->next=t->next;// 新增结点的后继指针指向当前结点的后继指针所指向的结点
t->next=p;// 当前结点的后继指针指向新增结点
break;
}
t=t->next;//继续下一个结点
}
//输出链表中的所有数
t=head;
while(t!=NULL)
{
printf("%d",t->data);
t=t->next;//继续下一个结点
}
return 0;
}
正确写法:
r=p->pnext;//指向p后面的结点
p->pnext=r->pnext;//指向p后面的后面的结点
free( r );//释放r的内存
void delete(struct node * phead,int n)//phead为头结点,n为要删除的结点序号
{
int i;
struct node *r;//定义一个临时指针
struct node *p;
r=pHead;
p=r;
printf("删除第%d个学生 \n",n);
for(i=1;i<n;i++)//通过for循环使r指向要删除的结点
{
p=r;
r=r->next;
}
p->next=r->next;//连接删除结点两边的结点
free();//释放要删除结点的内存空间
}
优点:空间没有限制
优点: 插入删除元素很快
缺点:存取速度很慢