文章目录
任务:学习链表
- 看视频、看书
- 敲例题
- 做练习
1. 存储空间的分配和释放
C语言标准库函数中提供了4个函数malloc()、calloc()、realloc()、free(),用来实现内存的动态分配与释放。
malloc()
函数:动态分配一段内存空间
malloc()函数的原型为:void *malloc(unsigned int size);
这个函数的功能是在内存的动态存储区申请一个长度为 size 字节的连续存储空间。malloc()函数会返回一个指针,并指向所分配空间的起始地址。如果没有足够的内存空间可分配,则函数的返回值为空指针 NULL。
#include<stdio.h>
#include<malloc.h> //也可以用<stdlib.h>
int main() {
int *p = (int *)malloc(sizeof(int)); //分配空间
*p = 10; //使用该空间保存数据
printf("%d",*p); //输出数据,输出结果为:10
return 0;
}
calloc()
函数:动态分配连续内存空间
calloc() 函数的原型为:void *calloc(unsigned int n,unsigned int size);
这个函数的功能是在内存申请 n 个长度为 size 字节的存储空间,并返回该存储空间的起始地址。如果没有足够的内存空间可分配,则函数的返回值为空指针NULL。
例:int *p = (int *)calloc(3,sizeof(int));
表示申请3个 int 类型长度的存储空间,并将分配到的存储空间地址转换为 int 类型地址,将其首地址赋给所定义的指针变量 p。然后 p 就可以作为3个元素的整型数组来使用。
这个语句的功能也可以用malloc() 函数来实现:int *p = (int *)malloc(3*sizeof(int));
例:
#include<stdio.h>
#include<stdlib.h> //<stdlib.h>中包含malloc()和calloc()函数.
int main() {
int i;
char *ch1 = (char *)calloc(26,sizeof(char)); //使用malloc动态分配一个长度为26字符的字符数组
char *ch2 = (char *)malloc(26*sizeof(char)); //使用calloc动态分配一个长度为26字符的字符数组
for(i = 0;i < 26;i++) { //为两个字符数组赋值
ch1[i] = 65 + i; //ch1是大写字符数组
ch2[i] = 97 + i; //ch2是小写字符数组
}
printf("26个大写字母:\n");
for(i = 0;i < 26;i++) {
printf("%c ",ch1[i]);
}
printf("\n26个小写字母:\n");
for(i = 0;i < 26;i++) {
printf("%c ",ch2[i]);
}
return 0;
}
-
realloc()
函数:改变指针指向空间的大小
realloc() 函数的原型为:void *realloc(void *ptr,size _t size);
它的功能是改变ptr 指针指向大小为 size 的空间。设定 size 的大小可以是任意的,可以比原来的数值大或者小。
函数返回值是一个指向新地址的指针,如果出现错误,则返回NUL。 -
free()
函数:释放存储空间
free() 函数原型为void free(void *p);
它的功能是将指针变量 p 指向的存储空间释放,交还给系统。free 函数无返回值。
注意:p 只能是程序中此前最后一次调用 malloc 或 calloc 函数所返回的地址。
2. 概述
定义:链表是物理存储单元上非连续、非顺序的存储结构。
作用:可以动态的进行存储分配。
特征:可以在节点中定义多种数据类型,还可以根据需要添加、删除、插入节点。
构成:
- 链表都有一个头指针,一般用head表示,存放的是一个地址。
- 链表中每个结点都分为两部分:指针域和数据域;
- 节点分为两类:头结点和一般结点。头结点是没有数据的
- 在链表中,第一个结点前虚加一个头结点,头指针指向头结点,头结点的指针域指向第一个实际有效结点(也称为首元结点)。头结点的数据域可以不使用。
- 对于带头结点的链表,空表也会保留头结点。带头结点的链表比不带头结点的链表在创建、插入和删除等操作时代码更简洁。
分类:
- 从内存角度出发: 链表可分为 静态链表、动态链表。
- 从链表存储方式的角度出发:链表可分为 单链表、双链表、以及循环链表。
3. 静态链表和动态链表
静态链表是把线性表的元素存放在数组中,且每个元素除了存放数据以外,还要存放指向下一个元素的位置,即下一个元素所在数组单元的下标。
静态链表大小固定,插入元素是固定的。一般会设置一个元素为用户自定义类型的数组,大小是固定的。如:
#define MAX 1000
typedef struct StaticList
{
int data; //存放数据
int cur; //用下标来代替指针
}SL;
SL Test[MAX];
动态链表是用申请内存函数(C是malloc,C++是new)动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个结点的物理地址不连续,要通过指针来顺序访问。
4. 单链表
若在链表中,每个结点只有一个指针,所有结点都是单线联系,除了末尾结点指针以空外,每个结点的指针都指向下一个结点,一环扣一环形成一条线性链,则称此链表为单向线性链表,简称单链表。
特点:
- 有一个 head 指针变量,它存放在头结点的地址,称之为头指针。
- 头结点的指针域
head->next
存放首元结点的地址。 - 从头指针 head 开始,head 指向头结点,头节点指向首元结点,首元结点指向第二个结点,直到最后一个结点。所有结点都是单线联系。
- 最后一个结点不再指向其他结点,称为“表尾节点”,它的指针域为空指针NULL,表示链表到此结束。指向表尾结点的指针称为尾指针。
- 链表各结点之间的顺序关系由指针域 next 来确定,并不要求逻辑上相邻的结点物理位置上也相邻。即链表依靠指针相连不需要占用一片连续的内存空间。
4.1 单链表的初始化
由于链表的每个结点都包含数据域和指针域,即每个节点都要包含不同类型的数据,所以结点的数据类型必须选用结构体类型。且结构体中必须有一个成员的类型是指向本结构体类型的指针类型。
单链表的初始化就是创建一个头结点,头结点的数据域可以不使用,头结点的指针域为空,表示空单链表。
- 先定义一个需要用的结构体类型:
typedef struct node {
int number; //数据域
char name[20]; //数据域
struct node *next; //递归定义指向struct node类型结构体的指针变量next。
}NODE,*LinkList;
- 然后才开始链表的初始化。单链表的初始化就是创建一个头结点,头结点的数据域可以不使用,头结点指针域为空,表示空单链表。
LinkList List() {
LinkList head; //定义头指针变量
head = (NODE*)malloc(sizeof(NODE)); //头指针指向分配的头结点内存空间
head->next = NULL; //头结点的指针域为空
return head; //返回头结点的地址,即头指针
}
如下图:
4.2 单链表的建立
单链表的建立就是在程序的运行过程中,从无到有的建立一个链表,即一个一个的分配结点的内存空间,然后输入结点中的数据,并建立结点间的相连关系。
单链表的建立可以分为两种方法:尾插法和头插法。
尾插法
:在单链表的尾部插入新结点。
从一个空表开始,重复读入数据,生成新结点,将读入数据存放到新结点的数据域中,然后将新结点插入到当前链表的表尾上,直至读入结束标志为止。
void(CreatByRear(LinkList)) {
NODE *r,*s; //s用于创建新结点
int number;
char name[20];
r = head; //head指向头结点,故r指向头结点
printf("请输入学生的学号和姓名:\n");
while(1) {
scanf("%d",&number);
scanf("%s",name);
if(number == 0) break;
s = (node *)malloc(sizeof(NODE)); //分配结点的内存空间
s->number = number;
strcpy(s->name,name);
r->next = s; //原来的结点指向新结点
r = s; //r指向新结点
}
r->next = NULL; //链表的尾结点指针为空
}
如下图:
- 头插法:在单链表的头部插入新结点
从一个空表开始,重复读入数据,生成新结点,将读入的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头节点之后,直至读入结束标志为止。
void CreatByHead(LinkList head) {
NODE *S;
int number;
char name[20];
printf("请输入学生学号和姓名;\n");
while(1) {
scanf("%d",&number);
scanf("%s",name);
if(number == 0) break;
s = (NODE *)malloc(sizeof(NODE)); //分配节点的内存空间
s->number = number;
strcpy(s->name,name);
s->next = head->next; //让新结点指向首元结点
head->next = s; //让头结点指向新结点
}
}
如下图:
4.3 单链表的遍历
直接上代码:
void OutPut(LinkList head) {
NODE *p; //循环所用的临时指针
p = head->next; //p指向链表的首元结点
while(p) {
printf("学号:%d\n",p->number);
printf("姓名:%s\n",p->name);
p = p->next; //移动临时指针到下一个结点
}
}
- 函数定义了一个临时指针p用来进行循环操作,使其指向要输出链表的首元结点。
- 在while循环中,每输出一个结点的内容后,就移动临时指针p到下一个节点的位置。如果是最后一个结点,指针指向NULL,表示链表中的节点都已经输出,循环结束。
4.4 单链表的插入
链表的插入操作可以在链表的头指针位置进行插入,也可以在链表中某个结点的位置进行插入,或者在链表的最后面添加结点。在头指针位置和在最后面插入的思路与头插法和尾插法的思路相同。
以下写一段在链表中某个结点的位置插入新结点的代码示例:
void Insert(LinkList head,int i) {
NODE *P = head,*s;
int j = 0;
while(j < i-1 && p) { //从头结点开始,故开始j=0,头结点是第0个结点,遍历找到第i-1个结点的地址
p = p->next;
j++;
}
if(p) {
printf("请输入待添加学生的学号和姓名:\n");
s = (NODE *)malloc(sizeof(NODE)); //定义s指向新分配的空间
scanf("%d",&s->number);
scanf("%d",s->name);
s->next = p->next; //新结点指向原来的第i个结点
p->next = s; //新结点成为原来的第i个结点
}
}
4.5 单链表的删除
在创建单链表删除某个结点的函数时需要两个参数,一个表示链表的头指针head,另一个表示要删除的节点在链表中的位置。
代码示例:
void Delete(LinkList head,int pos) {
NODE *p = head,*q;
int j = 0;
printf("删除第%d个学生的信息",pos);
while(j < pos-1 && p) { //通过循环,找到第pos-1个结点的地址
p = p->next;
j++;
}
if(p == NULL || p->next == NULL) printf("the pos is error"); //第pos个结点不存在
else {
q = p->next; //q 指向第pos个结点
p->next = q->next; //连接所要删除结点两边的结点
free(q); //释放所要删除结点的内存空间
}
}
4.6 单链表的查询
在创建单链表查询某个结点的函数时需要两个参数,一个表示链表的头指针head,另一个表示要查找的值。
代码示例:
NODE *Search(LinkList head,char name[]) { //在单链表head中找到值为name的结点
NODE *p = head->next;
while(p) {
if(strcmp(s->name,name)!=0) p = p->next; //判断结点的值是不是name的值,若不是则移动p指向下一个结点
else break; //查找成功!
}
if(p == NULL)
printf("未找到值为%d的结点",name);
return p;
}
4.7 单链表的长度
单链表的长度是隐形表示的,当从首元结点开始,依次遍历链表的所有结点,并同时统计结点个数,最后返回结点个数值。
代码示例:
int Length(LinkList head) {
int count;
NODE *p;
p = head->next; //指针变量p指向链表的首元结点
while(p) { //结点存在,表示链表没有遍历结束
count++; //结点个数累加器加1
p = p->next; //指向当前结点的下一结点
}
return count; //返回链表结点的个数
}
4.8 不带头结点的单链表
不带头结点的单链表,在操作过程中必须针对第一个结点和其余结点分别进行操作。
- 插入
分为在链表的首位置插入和不在链表的首位置插入两种情况。
在链表首位置:
插入时,首先为插入的新结点分配内存,然后将新结点的指针指向原来的首结点,最后将头指针指向新结点。需要注意的是,在这种情况下,头指针发生了改变,所以需要返回新的头指针。
不在链表首位置: 例如要在第 i 个结点插入新结点
需要先通过循环找到链表的第 i-1个结点的地址p。如果该结点存在,则可以在第 i-1 个结点后面插入第i个结点。为插入的新结点分配内存,然后向新结点输入数据。插入时,首先将新结点的指针指向原来第 i 个结点,然后将第 i-1 个结点指向新结点。完成。
- 删除
分为删除首结点和删除其他结点两种情况。
删除首结点:
定义指针变量去指向待删除的结点,再让头指针指向原来的第二个结点,成为新的首结点。最后释放原来的首结点的内存空间。在这种情况下,头指针也发生了改变,所以需要返回新的头指针。
删除不是头结点的结点:
定义整型变量 j 来控制循环次数,然后定义指针变量p表示该结点之前的结点。接着利用循环找到要删除的结点之前的结点p;如果该结点存在并且待删除结点存在,则将指针变量q指向待删除的结点,再连接要删除结点两边的结点。,并使用free函数将q指向的内存空间进行释放。
5. 循环链表
基于单链表的学习后,很容易知道,循环链表,就是在链表最后一个结点的指针域处存放头结点的地址,头尾相连,形成一个环形的数据链。故对于循环链表表尾的判断,只需判断该节点的指针域是否指向链表头结点。
typedef struct node { //定义循环链表的结点类型
int age;
char name[20];
struct node *next;
}NODE,*PNODE;
PNODE List() { //初始化
PNODE head;
head = (NODE *)malloc(sizeof(NODE));
head->next = NULL;
return head;
}
void Creat(PNODE head) {
NODE *r,*s;
int age;
char name[20];
r = head;
while(1) {
scanf("%d",&age);
scanf("%s",name);
if(age == 0) break;
s = (NODE *)malloc(sizeof(NODE));
s->age = age;
strcpy(s->name,name);
r->next = s;
r = s;
}
r->next = head; //区别于单链表(r->next = NULL;),此处尾结点的指针域存放头指针的地址。
}
6. 双向链表
单链表每个结点有一个指针域和一个数据域,要访问任何结点都需知道头结点,不能逆着进行。双向链表则添加了一个指针域,通过两个指针域分别指向结点的前一个结点和后一个结点。这样的话,可以通过双链表的任何结点访问到它的前一个结点和后一个结点。
两个指针域一个存储直接后继结点的地址,一般称为右链域,另一个存储直接前驱结点,一般称为左链域。
typedef struct node { //定义双向链表的结点类型
int age;
char name[20];
struct node *prior;
struct node *next;
}NODE,*PNODE;
PNODE List() { //初始化
PNODE head;
head = (NODE *)malloc(sizeof(NODE));
head->prior = NULL;
head->next = NULL;
return head;
}
void Creat(PNODE head) {
NODE *r,*s;
int age;
char name[20];
r = head;
printf("请输入学生信息:年龄和姓名\n");
while(1) {
scanf("%d",&age);
scanf("%s",name);
if(age == 0) break;
s = (NODE *)malloc(sizeof(NODE));
s->age = age;
strcpy(s->name,name);
r->next = s;
s->prior = r; //结点左链域存储直接前驱结点的地址
r = s;
}
r->next = NULL;
}