C语言链表入门教程(非常详细,新手必看)
链表是一种常见的数据结构,读者已经掌握了用数组存放数据,使用数组时要先指定数组中包含元素的个数,即数组的长度。如果向这个数组中加入的元素个数超过数组的长度,便不能将内容完全保存。
例如,在定义一个班级的人数时,如果小班是 30 人,普通班级是 50 人,且定义班级人数时使用的是数组,那么定义数组的大小应为最大人数,也就是最少为 50 个元素,否则不满足最大时的情况,这种方式非常浪费空间。
这时就希望有一种存储方式,其存储元素的个数是不受限制的,当添加元素时存储元素的个数就会随之改变,这种存储方式就是链表。
链表结构的示意图如下图所示:
图 1 链表结构的示意图
从图 1 可以看到,head(头)节点指向第 1 个元素,第 1 个元素中的指针又指向第 2 个元素的地址,第 2 个元素的指针又指向第 3 个元素的地址,第 3 个元素的指针指向空。
注意,链表这种数据结构必须利用指针才能实现,因此链表中的节点应该包含一个指针变量来保存下一个节点的地址。
例如,设计一个链表表示一个班级,其中链表中的节点表示学生,代码如下:
struct Student
{
char cName[20]; /*姓名*/
int iNumber; /*学号*/
struct Student* pNext; /*指向下一个节点的指针*/
};
可以看到学生的姓名和学号属于数据部分,而 pNext 就是指针部分,用来保存下一个节点的地址。
链表的创建
从前面的讲解不难看出,链表并不是一开始就设定好大小的,而是根据节点的多少确定的,因此链表的创建过程是一个动态的过程。
定义一个 Create() 函数,用来创建链表。该函数会返回链表的头指针,代码如下:
int iCount; /*全局变量表示链表长度*/
struct Student* Create()
{
struct Student* pHead = NULL; /*初始化链表头指针,使其为空*/
struct Student* pEnd, *pNew;
iCount = 0; /*初始化链表长度*/
pEnd = pNew = (struct Student*)malloc(sizeof(struct Student));
printf("please first enter Name ,then Number\n");
scanf("%s", &pNew->cName);
scanf("%d", &pNew->iNumber);
while (pNew->iNumber != 0)
{
iCount++;
if (iCount == 1)
{
pNew->pNext = pHead; /*使得指向为空*/
pEnd = pNew; /*跟踪新加入的节点*/
pHead = pNew; /*头指针指向首节点*/
}
else
{
pNew->pNext = NULL; /*新节点的指针为空*/
pEnd->pNext = pNew; /*原来的尾节点指向新节点*/
pEnd = pNew; /*pEnd指向新节点*/
}
pNew = (struct Student*)malloc(sizeof(struct Student));
/*再次分配节点内存空间*/
scanf("%s", &pNew->cName);
scanf("%d", &pNew->iNumber);
}
free(pNew); /*释放没有用到的空间*/
return pHead;
}
从上述代码中可以看出以下内容:
1) Create() 函数的功能是创建链表,在 Create() 的外部有一个整型的全局变量 iCount,这个变量的作用是表示链表中节点的数量。在 Create() 函数中,首先定义需要用到的指针变量,pHead 表示头节点,pEn指 向原来的尾节点,pNew 指向新创建的节点。
2) 使用 malloc() 函数分配内存,先使 pEnd 和 pNew 两个指针都指向第一个分配的内存,然后输出提示信息,先输入一个学生的姓名,再输入学生的学号。使用 while 语句进行判断,如果学号为 0,则不执行循环语句。
3) 在 while 循环语句中,iCount++ 自加操作表示链表中节点的增加。然后要判断新加入的节点是否为第一次加入的节点,如果为第一次加入的则运行 if 语句块中的代码,否则运行 else 语句块中的代码。
4) 在 if 语句块中,因为第一次加入节点时其中没有节点,所以新节点即首节点,也为最后一个节点,并且要将新加入的节点的指针指向空,即 pHead 的指向。else 语句块实现的是链表中已经有节点存在时的操作。
5) 首先将新节点 pNew 的指针指向空,然后将原来最后一个节点的指针指向新节点,最后将 pEnd 的指针指向最后一个节点。这样节点创建完之后,要再次分配内存,然后向其中输入数据,通过 while 语句再次判断输入的数据是否符合节点的要求。当不符合要求时,调用 free() 函数将不符合要求的节点空间进行释放。
这样,链表就通过动态分配内存空间的方式创建完成了。
链表的输出
链表已经被创建出来,构建数据结构就是为了使用它,以将保存的信息进行输出。接下来介绍如何将链表中的数据输出。代码如下:
void print(struct Student* pHead)
{
struct Student *pTemp; /*循环所用的临时指针*/
int iIndex = 1; /*表示链表中节点的序号*/
printf("----the List has %d members:----\n", iCount); /*提示信息*/
printf("\n"); /*换行*/
pTemp = pHead; /*指针得到首节点的地址*/
while (pTemp != NULL)
{
printf("the NO%d member is:\n", iIndex);
printf("the name is: %s\n", pTemp->cName); /*输出姓名*/
printf("the number is: %d\n", pTemp->iNumber); /*输出学号*/
printf("\n"); /*换行*/
pTemp = pTemp->pNext; /*移动临时指针到下一个节点*/
iIndex++; /*进行自加运算*/
}
}
结合创建链表和输出链表的过程,运行程序,结果为:
please first enter Name ,then Number
dahua 1
xiaoning 2
daxhuang 3
lucy 4
exit 0
----the List has 4 members:----
the N01 member is:
the name is: dahua
the number is: 1
the N02 member is:
the name is: xiaoning
the number is: 2
the N03 member is:
the name is: daxhuang
the number is: 3
the N04 member is:
the name is: lucy
the number is: 4
printf() 函数用来将链表中的数据进行输出。在函数的参数中,pHead 表示一个链表的头节点。在函数中,定义一个临时的指针 pTemp 用来进行循环操作。定义一个整型变量表示链表中的节点序号。然后使用临时指针变量 pTemp 保存首节点的地址。
使用 while 语句将所有节点中保存的数据都输出。每输出一个节点的数据后,就移动 pTemp 指针指向下一个节点的地址。当为最后一个节点时,其所有的指针指向空,此时循环结束。
链表的插入和删除
1) 链表的插入操作
链表的插入操作可以在链表的头节点位置进行,也可以在某个节点的位置进行,或者可以像创建结构一样在链表的后面添加节点。这 3 种插入操作的思路都是一样的。
下面主要介绍第一种插入操作,即在链表的头节点位置插入节点,如下图所示:
图 2 插入节点
插入节点的过程就如手拉手的小朋友连成一条线,这时又来了一个小朋友,他要站在第一位小朋友的前面,那么他只需要牵原来第一位小朋友的手就可以了。这样,这条连成的线还是连在一起的。
设计一个函数用来向链表中添加节点,代码如下:
struct Student* Insert(struct Student* pHead)
{
struct Student* pNew; /*指向新分配的空间*/
printf("----Insert member at first----\n"); /*提示信息*/
pNew = (struct Student*)malloc(sizeof(struct Student));
/*分配内存空间,并返回指向该内存空间的指针*/
scanf("%s", &pNew->cName);
scanf("%d", &pNew->iNumber);
pNew->pNext = pHead; /*新节点指针指向原来的首节点*/
pHead = pNew; /*头指针指向新节点*/
iCount++; /*增加链表节点数量*/
return pHead; /*返回头指针*/
}
上述代码插入节点的步骤如下:
为要插入的新节点分配内存,然后向新节点中输入数据,这样一个节点就创建完成了。
将新节点的指针指向原来的首节点,保存首节点的地址,然后将头指针指向新节点,这样就完成了节点的连接操作。
主函数 main() 的代码如下:
int main()
{
struct Student* pHead; /*定义头节点*/
pHead = Create(); /*创建节点*/
pHead = Insert(pHead); /*插入节点*/
Print(pHead); /*输出链表*/
return 0; /*程序结束*/
}
运行程序,结果为:
please first enter Name ,then Number
liuwen 2
suyunjing 3
exit 0
----Insert member at first----
wangjiashen 1
----the List has 3 members:----
the N01 member is:
the name is: wangjiashen
the number is: 1
the N02 member is:
the name is: liuwen
the number is: 2
the N03 member is:
the name is: suyunjing
the number is: 3
2) 链表的删除操作
之前介绍的操作是向链表中添加节点,当希望删除链表中的节点时,应该怎么办呢?还是通过前文中小朋友手拉手的例子进行介绍,队伍中的一个小朋友想离开,使这个队伍不会断开的方法是他两边的小朋友将手拉起来。
例如,在一个链表中删除其中的一个节点,如下图所示:
图 3 删除节点
通过图 3 可以发现,要删除一个节点,先要找到这个节点的位置。例如要删除 NO2 节点,首先要找到 NO2 节点,然后删除该节点,将 NO1 节点的指针指向 NO3 节点,最后释放 NO2 节点的内存空间,这样就完成了节点的删除。
根据这种思想编写删除链表节点的函数,代码如下:
void Delete(struct Student* pHead, int iIndex)
/*pHead表示头节点,iIndex表示要删除的节点索引*/
{
int i; /*控制循环的变量*/
struct Student* pTemp; /*临时指针*/
struct Student* pPre; /*表示要删除的节点前的节点*/
pTemp = pHead; /*得到头节点*/
pPre = pTemp;
printf("----delete NO%d member----\n", iIndex); /*提示信息*/
/*for循环使得pTemp指向要删除的节点*/
for (i = 1; i { pPre = pTemp; pTemp = pTemp->pNext; } pPre->pNext = pTemp->pNext; /*连接要删除的节点两边的节点*/ free(pTemp); /*释放要删除的节点的内存空间*/ iCount--; /*减少链表中的元素个数*/ } 为 Delete() 函数传递两个参数,pHead 表示链表的头节点,iIndex 表示要删除的节点的索引。定义整型变量 i 用来控制循环的次数,然后定义两个指针,分别用来表示要删除的节点和这个节点之前的节点。 输出一行提示信息表示要进行删除操作,之后利用 for 语句进行循环操作,找到要删除的节点,使用 pTemp 保存要删除节点的地址,pPre 保存前一个节点的地址。找到要删除的节点后,连接要删除的节点两边的节点,并使用 free() 函数将 pTemp 指向的内存空间释放。 接下来在 main() 函数中添加代码执行删除操作,将链表中的第二个节点删除。代码如下: int main() { struct Student* pHead; /*定义头节点*/ pHead = Create(); /*创建节点*/ pHead = Insert(pHead); /*插入节点*/ Delete(pHead, 2); /*删除第二个节点的操作*/ Print(pHead); /*输出链表*/ return 0; /*程序结束*/ } 运行程序,通过运行结果可以看到第二个节点中的数据被删除,结果为: please first enter Name ,then Number WangJun 2 LiXin 3 exit 0 ----Insert member at first---- XuMing 1 ----delete N02 member---- ----the List has 2 members:---- the N01 member is: the name is: XuMing the number is: 1 the N02 member is: the name is: LiXin the number is: 3