链表的空间分配
链表是存在于堆内存,存放在不连续的存储空间,在这些不连续的存储空间中使用指针去作为一个索引。 我们知道数组的空间是连续的,但是空间过大的时候,我们可能开不出这个数组。然而实际上电脑内部是有很多零散的空间没有被使用的,只不过他们不连续罢了,所以我们必须要充分利用电脑内部的储存空间。由此我们引入了一个全新的数据结构----链表。
链表的结构体
- C语言中的一个结点
struct Student{
int num;//学号 数据域
char name[10];//姓名 数据域
struct Student *pnext;//表示这个指针指向下一个节点(即下一个结构体)指针域
};
p就是上述代码中的pnext ,我们用变量储存数据,而不断用这个结构体的指针去指向下一个结构体,这就是一个链表。
- 节点是什么?
链表是由节点组成的,上图中每个结构体就是链表的节点。
接下来的代码详细展示了各个变量的作用
typedef struct Student
{
int num; //学号
char name[10]; //姓名
struct Student *pnext; //表示这个指针指向下一个节点(即下一个结构体)
} STU;
//c语言中,如果加上了第一句的typedef ,STU就是一种数据类型(相当于struct Student)若没有加typedef ,STU只是一个变量
//在c++中 ,Student可以直接作为数据类型。
两种链表
- 第一个节点就储存数据的链表
- 第一个节点不储存数据,我们称之为头结点(接下来我们主要是讲这种链表)
动态内存的使用(链表)
- 链表的空间分配是用动态内存malloc函数来进行分配的
STU *pStu = NULL; //相当于struct Student *pStu
pStu = (STU*)malloc(sizeof(STU)); //就是开辟一个节点
free(pStu);
创建链表头结点
头结点是一个不储存数据的结点,它的作用是为了使得首节点的链表操作与其他结点相同而引入的结点,有的书上将其称为哨兵
STU* CreateList()
{
STU *P=(STU*)malloc(sizeof(STU));
P->pnext = NULL; //头结点指向为空 (此时还不知道头结点的下一个指向谁) 为保证安全性,赋值为空
return P;
//不需要给num和name赋值,因为是头结点。
}
添加节点(按输入顺序添加节点)
添加n个节点(人为控制添加的数量)
这个是初始状态,形参指针P指向头结点。
创建临时变量pNew,赋值后,此时令pNew=P->pNext,完成节点之间的连接
再令P指向新的节点
添加一个节点
#include <stdio.h>
#include <stdlib.h>
typedef struct Student
{
int num; //学号
char name[10]; //姓名
struct Student *pnext; //表示这个指针指向下一个节点(即下一个结构体)
} STU;
//创建链表的头结点
STU *CreateList()
{
STU *P = (STU *)malloc(sizeof(STU));
P->pnext = NULL;
return P;
}
//添加节点
void addNode(STU *P)
{
STU *pNew = NULL; //重新定义一个指针
pNew = (STU *)malloc(sizeof(STU)); //为新的指针开辟一块内存
printf("请输入学号:");
scanf("%d", &pNew->num);
printf("请输入姓名:");
scanf("%s", pNew->name);
pNew->pnext = NULL; //为了安全,把指针指向定为空
P->pnext = pNew; //连接起来
}
int main()
{
STU *pStu = NULL; //定义一个指针,接受链表的首地址
pStu = CreateList(); //头结点
addNode(pStu); //添加一个节点 只添加了一个
printf("%d\t%s\n", pStu->pnext->num, pStu->pnext->name); //打印添加的节点
return 0;
}
这样我们就添加了一个节点,但是在正常初始化链表的时候我们不可能只加入一个节点,所以我们需要一次性可以加入多个节点的办法
再往下举一个例子。
在for循环内创建一个新的pNew
给pNew赋值后,此时P指向第二个节点,令此时的pNew=P->pNext,完成节点之间的链接。
再令P指向新创建的节点。往后以此类推,形成链表。
实现代码如下:
其中,addNode不一定非要输入n个,可以自己添加限制条件,例如num==0时结束输入
#include <stdio.h>
#include <stdlib.h>
typedef struct Student
{
int num; //学号
char name[10]; //姓名
struct Student *pnext; //表示这个指针指向下一个节点(即下一个结构体)
} STU;
//创建链表的头结点
STU *CreateList()
{
STU *P = (STU *)malloc(sizeof(STU));
P->pnext = NULL;
return P;
}
//添加节点 人为控制添加的个数
void addNode(STU *P)
{
STU *pNew = NULL;
int n; //当前要加入的节点的数量
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
pNew = (STU *)malloc(sizeof(STU));
printf("请输入学号:");
scanf("%d", &pNew->num);
printf("请输入姓名:");
scanf("%s", pNew->name);
pNew->pnext = NULL; //尾节点设置为空 这个非常重要
P->pnext = pNew; //连接起来
P = P->pnext;
}
}
int main()
{
STU *pStu = NULL; //定义一个指针,接受链表的首地址
pStu = CreateList(); //头结点
addNode(pStu); //添加n个节点
printf("%d\t%s\n", pStu->pnext->num, pStu->pnext->name); //打印第二个节点的内容
return 0;
}
遍历输出节点
经过以上的步骤,我们已经创建了一个链表,接下来我们遍历打印出所有的信息,检验一下我们的链表是否实现了它的功能
代码如下:
void PrintAll(STU *P)
{
P=P->pnext;//P指向第一个有数据的节点
while(P!=NULL)
{
printf("%d\t%s\n",P->num,P->name);
P=P->pnext;
}
}
插入节点
现在我们已经知道了头结点的创建和节点的增加 接下来我们来尝试插入节点(即从中间插入节点)
创建新节点pNew
- 第一步
-
再进行一步相同的操作。
-
由于head->pnext指向先创建的pNew,那么令第二次创建的pNew->pnext=head->pnext,而head->pnext又指向先创建的pNew,于是先创建的pNew和后创建的pNew就连接起来了。
-
然后令head->pnext=pNew,指向后创建的pNew。
-
那么这三个就形成了如下的链表:
- 代码实现
void addstudent(student * head, int n)
{
student * p = head;
student * pNew = (student *)malloc(sizeof(student));
for (int i = 0; i < n-1; ++i)
p = p->pNext;
pNew->pNext = NULL;//初始化
scanf("名字是:%s", pNew->name);
scanf("学号是:%d", &(pNew->name));
pNew->pNext = p->pNext;//先连后面再连前面
p->pNext = pNew;
return;
}
tips
- 我们在写这段代码的时候需要考虑一个事情,我们在插入一个节点的时候,使用了一个循环取到了之前的那个节点的地址,这是为什么呢?这是因为这是一个单向链表,我们无法通过后面的节点来推出前置节点的信息,所以如果想要在两个节点之间插入结点,我们就必须拿到前置节点的地址。
节点的删除
与插入节点一样,我们也需要先拿到前置节点的地址,但是删除节点的操作比插入要简单的多,下面直接贴出代码
void deletestudent(student * header, int n)
{
student * p = header;
for (int i = 0; i < n - 1; ++i)
p = p->pNext;//找到前置节点的地址
student * temp = p->pNext;
p->pNext = temp->pNext;
free(temp);
return;
}
tips
- 删除节点有个易错点,在删除该节点的时候我们需要考虑到这个节点的空间是动态分配的,所以需要被人为的释放,一定要注意这一点。
总结
本次培训主要是介绍了链表的创建,插入,删除,遍历集中基本操作,链表的代码实现,需要编写者充分理解原理,在搞懂原理之后,便可以自己编写链表了,不同的人链表的实现代码也是不同的,但是只要最后效果一样,那就恭喜你,已经掌握了链表的基本操作。
- 附上培训时敲出的代码
#include <iostream>
#include <stdlib.h>
using namespace std;
typedef struct student
{
char name[10]; //名字
int num; //学号
student *pNext;
} STU;
int createlist(student *, int);
void printlist(student *);
void addstudent(student *, int);
void deletestudent(student *, int);
void endlist(student *);
int main()
{
student *header = (student *)malloc(sizeof(student));
header->pNext = NULL;
int n;
cout << "请输入所需要的链表长度:";
cin >> n;
if (createlist(header, n) == -1)
exit(-1);
printlist(header);
endlist(header);
return 0;
}
int createlist(student *header, int n)
{
student *p = NULL;
p = header;//找到头结点地址
for (int i = 0; i < n; i++)
{
student *temp = (student *)malloc(sizeof(student));
if (NULL == temp)
return -1;
cout << "学生学号:";
cin >> temp->num;
cout << "学生姓名";
cin >> temp->name;
//scanf("%s %d",&temp->name,&temp->num);
temp->pNext = NULL;
p->pNext = temp;
p = temp;
}
return 0;
}
void printlist(student *header)
{
student *p = header;
while (p->pNext != NULL)
{
p = p->pNext;
cout << p->name << endl;
cout << p->num << endl;
}
return;
}
void addstudent(student * header, int n)
{
student * p = header;
student * pNew = (student *)malloc(sizeof(student));
for (int i = 0; i < n-1; ++i)
p = p->pNext;//找到前置节点地址
pNew->pNext = NULL;//初始化
cout << "学号是:";
cin >> pNew->name;
cout << "名字是:";
cin >> pNew->num;
pNew->pNext = p->pNext;//先连后面再连前面
p->pNext = pNew;
return;
}
void deletestudent(student * header, int n)
{
student * p = header;
for (int i = 0; i < n - 1; ++i)
p = p->pNext;//找到前置节点地址
student * temp = p->pNext;
p->pNext = temp->pNext;
free(temp);
return;
}
void endlist(student * header)
{
student * p = header;
student * q = NULL;
while (p != NULL)
{
q = p->pNext;
free(p);
p = q;
}
cout << "链表完蛋了";
}
对于链表的学习还是要多写代码,多推敲,在理解原理的基础上自己导出结点之间的关系。