假设需要登记每个班级的学生姓名,有的班级总人数为 35,有的班级总人数为 90,则用数组存放数据时,需要将数组的大小设为 90。若班级的人数无法确定,则需要将数组的大小设的足够大,但这样会浪费内存空间。
相比数组,链表就没有这种缺点,链表可以根据需要开辟内存空间。与数组不同的是,数组在内存中按顺序依次存储(线性),而链表在内存中是非线性存储的。
链表的创建
链表是一种数据结构,一般使用结构体变量作为链表的元素,也叫节点(Node)。因此,链表的基本构成包含指针变量和结构体变量。
在 int main() 函数外,先创建结构体类型,代码如下:
struct int_node {
int val;
struct int_node *next; //next是结构体指针变量,指向下一个结构体变量
};
这样创建了 int_node 结构体类型,这个结构体类型与之前的结构类型体不同,这个结构体类型有一个指向结构体变量的成员,这个成员是指针变量,用于存储下一个结构体变量的位置,该成员是创建链表最重要的部分。
在 int main() 函数里,创建头指针变量 head,指针变量 p 和结构体变量 a、b、c 代码如下:
struct int_node *head,*p,a,b,c;
对结构体变量 a、b、c 的 val 成员分别赋值 1、2、3,代码如下:
a.val = 1;
b.val = 2;
c.val = 3;
代码运行后,形成的数据初始结构如下图所示。
图 1 数据初始结构
接下来做以下几步操作:
将结构体变量 a 的起始地址赋给头指针变量 head;
结构体变量 b 的起始地址赋给结构体变量 a 的 next 成员;
将结构体变量 c 的起始地址赋给结构体变量 b 的 next 成员;
为结构体变量 c 的 next 成员赋值为 NULL。
具体代码如下:
head = &a;
a.next = &b;
b.next = &c;
c.next = NULL; //指向空
到这一步,已经创建好了链表。指针变量 head 指向结构体变量 a 的地址,然后通过 a.next 指向结构体变量 b 的地址,再通过 b.next 指向结构体变量 c 的地址,最后 c.next 指向空。这样就将 3 个结构体变量串连起来,形成了链表其数据结构如下图所示。
图 2 形成链表的数据结构
下面进行遍历链表操作,先将指针变量 p 指向链表的首地址,代码如下:
p = head;
从头到尾,对链表进行访问,依次输出链表中 val 的值,代码如下:
do
{
printf("%d\n",p->val);
p = p->next;
}while(p! = NULL);
创建链表的完整代码如下:
#include
struct int_node {
int val;
struct int_node *next;
};
int main()
{
struct int_node *head,*p,a,b,c;
a.val = 1;
b.val = 2;
c.val = 3;
head = &a;
a.next = &b;
b.next = &c;
c.next = NULL;
p = head;
while(p != NULL)
{
printf("%d\n",p->val);
p = p->next;
}
return 0;
}
编译运行,结果如下:
1 2 3
注意,上面创建的是静态链表,即所有节点(也就是结构体变量)都已在程序中事先定义好的,而不是根据需要动态定义的。
链表的基本操作
接下来我们创建的链表,是在程序运行过程中从无到有地建立链表,也就是根据需要申请新节点,并将新节点从头到尾链接起来。
1) 创建链表
创建动态单向链表,能够输入学生的学号、成绩。代码如下:
#include
#include
#define LEN sizeof(struct stu) //获取字节数
//构造节点
struct stu
{
long id; //学号
float score; //成绩
struct stu *next;
};
int n; //全局变量n,保存节点总数
//创建链表
struct stu *create(void)
{
struct stu *head = NULL, *p1, *p2;
n = 0;
p1 = p2 = (struct stu*)malloc(LEN); //申请新节点
scanf("%ld %f",&p1->id,&p1->score); //输入学生的学号和成绩
while(p1->id != 0) //学号不等于0继续创建
{
n = n+1;
if(n == 1) head = p1; //head指向第一个节点
else p2->next = p1;
p2 = p1;
p1 = (struct stu*)malloc(LEN); //继续申请新节点
scanf("%ld %f",&p1->id,&p1->score);
}
p2->next = NULL; //指向空
return(head); //返回头节点
}
int main()
{
struct stu *pt;
pt = create();
return 0;
}
编译运行,结果如下:
1 80
2 90
3 70
4 86
5 78
0 0
可以看出,程序的第 3 行定义 LEN 存储 struct stu 类型数据的字节数,sizeof 是“字节数运算符”。
下面对 create() 函数进行解析:
1) 先定义指向 struct stu 类型数据的指针变量 head、p1、p2。n 赋值为 0,表示没有节点。
2) create() 函数中的第 4 行,malloc(LEN) 的作用是开辟一个长度为 LEN 字节的内存空间。malloc() 函数返回的是不指向任何类型数据的指针(void 型),而 p1 和 p2 是指向 struct stu 类型数据的指针变量,因此在 malloc(LEN) 之前添加“(struct stu *)”,对其类型进行强制转换。
3) malloc() 函数开辟第一个节点后,用 p1 和 p2 指向它,如下图所示。
图 3 动态链表创建(只有一个节点)
4) 从键盘输入一个学生的学号、成绩给 p1 所指的节点。如果输入的学号不为 0,则执行 n = n+1,n 的值为 1。如果判断出 n==1,即判断是第一个节点,则 head = p1,即将 head 指向第一个节点如图 3 所示(程序中始终将 head 指向第一个节点)。执行 p2 = p1,让 p2 也指向第一个节点。用 malloc() 函数开辟第二个节点,并将 p1 指向它(图 4 所示的第一步)。然后从键盘输入一个学生的学号、成绩给 p1 所指向的新节点。
图 4 动态单向链表(包含5个节点)
5) 若学号为 0,则退出循环,执行 p2−>next=NULL(让链表最后的节点指向空),执行 return 语句返回链表的头指针 head。这样 p1 开辟的新节点不会链接到链表中。至此,完成了动态单向链表的创建。
创建的链表如图 3(只有一个节点)和图 4 所示(包含 5 个节点)。
与 malloc() 函数不同的是,calloc() 函数进行了初始化,calloc() 函数分配的空间全部初始化为 0。
char* p;
p=(char*)calloc(40,sizeof(char));
realloc() 函数用法为 realloc(void *p,unsigned size),将指针变量 p 指向的已分配的内存空间的大小改为 size。
p=(char*)realloc(p,20);
2) 遍历链表
在前面程序的基础上添加自定义的 print() 函数,完成链表的遍历,具体代码如下:
//链表的输出
void print(struct stu *head)
{
struct stu *p; //声明指针变量p
printf("id \t score\n"); //\t 表示跳格,即留8个空格
p = head;
while(p != NULL)
{
printf("%ld\t%f\n",p->id,p->score);
p = p->next;
}
}
分析print()函数的功能:
先将链表头指针作为参数,在函数中声明了指针变量 p,将链表的头指针参数赋值给指针变量 p;
然后执行 while 循环,若指针变量 p 非为空,则输出指针变量 p 所指向节点的数据。再执行 p = p−>next,让指针变量 p 指向链表的下一个节点。
若指针变量 p 为空,则表明指针变量 p 已经指向链表的尾部,链表的遍历已完成。
链表的遍历如下图所示。
图 5 链表的遍历
3) 查找节点
下面的 find() 函数是从链表中查找与 id 值相同的节点,代码如下:
struct stu *find(struct stu *head,long a)
{
struct stu *temp;
temp = head;
if(temp == NULL) //如果原来链表是空表
return NULL;
else
{
//查找id值不等于链表中节点值且没有到达链表末端,则后移
while (( temp->id != a) && (temp->next != NULL))
temp = temp->next; //p1后移一个节点
}
return temp;
}
查找节点比较简单,就是匹配对应的 id 值,如果不相等且没有到达链表末端,则将链表指针不断后移,直到找到相等的值或到达链表末端为止。
4) 删除节点
下面的 del() 函数是从链表中删除与 id 值相同的节点,代码如下:
struct stu *del(struct stu *head,long id)
{
struct stu *p1,*p2;
if(head == NULL) //判断是否为空链表
{
printf("是空链表");
return head;
}
p1 = head; //p1指向第一个节点
//p1指向的不是要找的节点,且后面还有节点
while(id != p1->id && p1->next != NULL)
{
p2 = p1;
p1 = p1->next; //p1后移节点
}
if(id == p1->id) //找到了对应的节点
{
if(p1 == head) //如果p1是首节点,把第二个节点给head
head = p1->next;
else
p2->next = p1->next; //下一个节点地址赋给前一个节点
printf("已删除:%ld\n",id);
free(p1); //释放p1节点空间
//n为全局变量,含义为节点数,不在本函数内定义
n = n-1; //节点数n-1
}
else
printf("链表中找不到对应节点");
return head;
}
在 del() 函数中,包含 head 和 id 两个参数,参数值通过主程序中头节点和待删除学生的学号传递过来。
代码先判断该链表是否为空,如果为空,则无法删除并返回主函数。然后执行 while 循环找匹配的节点,先判断查找学号是否与链表当前节点的学生学号相等,或者链表是否到达链表末端,如果查找到匹配学号或者到达链表末端,则停止循环,否则链表指针不断移向下一个节点。
此时,p2 指针跟着 p1 指针移动,且在 p1 指针后面。如果找到匹配节点,则判断匹配节点是否恰好是头节点,即判断 p1 是否是首节点,如果是则将第二个节点地址赋值给 head,否则将前一个节点 p1 的下一个节点地址赋值给前一个节点 p2 指向的下一个节点地址,即将 p1 节点从链表中移除,并将 p1 节点空间释放,然后执行 n=n−1,将总节点数减 1。
动态删除链表节点过程如下图所示。
图 6 动态删除链表节点
5) 插入节点
向链表中插入节点,对应的 insert() 函数代码如下,其中参数 stud 为待插入节点的地址或指针:
struct stu *insert(struct stu *head, struct stu *stud)
{
struct stu *p0, *p1, *p2;
p1 = head; //p1指向头节点
p0 = stud; //p0指向要插入的新节点
if(head == NULL) //如果原来链表是空表
{
head = p0; //头节点指向新节点p0,只有1个节点
p0->next = NULL; //p0指向空
}
else
{
//新添加节点p0的id值如果大于链表中节点的id值,则后移
while ((p0->id > p1->id) && (p1->next != NULL))
{
p2 = p1; //p2指向p1所指向的节点,跟着p1移动
p1 = p1->next; //p1后移一个节点
}
if(p0->id < p1->id) //p0->id的值比当前的p1->id的值小,插入节点
{
if(head == p1)
head = p0; //在头节点前插入新节点
else
p2->next = p0; //p2接入新节点
p0->next = p1; //新节点接向p1,原来p2的下一个节点
}
else //新节点p0的id值大于链表中任一个节点的id值,在链表最后插入节点
{
p1->next = p0;
p0->next = NULL;
}
}
//n为节点数,全局变量
n = n+1; //节点数加1
return head;
}
下面通过重新插入 id=2,score=88 的节点来描述节点的插入过程。插入节点与删除节点的代码前面略相同。先判断链表是否是空表,如果非空,则通过 while 循环找到插入节点的位置 p1。在找到插入位置 p1 后,判断 head 是否等于 p1,如果是则把 p0 赋值给 p1,即将 p0 作为新的头节点,否则将 p0 赋值给 p2 的下一个节点,即将 p2 的下一个节点指向 p0,如下图中的第一步所示。
图 7 动态单向链表节点的插入
然后将 p0 的下一个节点指向 p1,即将新节点 p0 指向原来的 p1,完成节点的插入,如图 7 中的第二步所示。
如果新节点 p0 的 id 值大于链表中任一个节点的 id 值,则在链表最后插入节点,先将 p1 的下一个节点指向 p0,再将 p0 的下一个节点指向空。
调用 del() 函数和 insert() 函数的主函数代码如下:
int main()
{
struct stu *head,*temp;
head = create(); //创建
print(head); //遍历
printf("删除链表id为2的节点\n");
head = del(head,2);
print(head); //遍历
printf("重新输入新节点学号2和成绩88\n");
//重新开辟一个新节点
temp = (struct stu*)malloc(LEN);
scanf("%ld %f",&temp->id,&temp->score);
head = insert(head,temp);
print(head); //遍历
return 0;
}