详解结构体与链表

2023-05-16

目录:
1.定义使用结构体变量
2.使用结构体数组
3.结构体指针
4.结构体内存对齐(重点)
5.typedef的使用
6.动态内存的分配与释放
7.链表的创立、增删查改插
        什么是链表?
        静态链表和动态链表
        链表的创立
        链表的插入元素
        链表的删除元素
        链表的查找元素
        链表的更新元素
        链表的添加元素

由于博主水平有限,所以博客中难免会出现错误,如果发现,可以加以指正,博主会在第一时间修改。此外,博客中采用部分C语言中文网上的内容C语言中文网

1.定义和使用结构体变量

1.1结构体类型是做什么的?

思考,我们能不能使用一个变量存储一个学生的学号,姓名,性别,年龄,出生年月等个人的基本信息(也就是一个变量代表一个学生,一个变量能查找一个学生的所有信息),像我们学的int、float等基本数据类型是做不到的,你可能会想到数组,但是数组元素的数据类型是相同的,我们的姓名需要使用字符串来存储,年龄是需要int类型的来存储,所以数组也不行。于是C语言便允许用户建立由不同类型数据组合的组合型数据结构——结构体类型

1.2结构体类型的定义与使用

定义格式:

struct 结构体名{
成员列表
};//一定要注意这个分号

注意两个概念:

1.结构体名:用于区别其他结构体类型
2.结构体类型名:struct + 结构体名=结构体类型名

比如我们要定义一个结构体类型用于来存储一个学生的信息:

struct Date{        //用于存储学生的出生年月
  int month;
  int day;
  int year;
};
struct student{     //用于存储学生的信息
  int num;
  char name[20];
  char sex;
  int age;
  struct Date birthday;//结构体嵌套
  char addr[30];
}

已经定义结构体类型(相当于一个模板),我们有三种定义结构体类型变量(相当于一个学生的实体)的方法:

1.先声明结构体类型,再定义结构体变量

#include<stdio.h>
struct Date{        //用于存储学生的出生年月
  int month;
  int day;
  int year;
};
struct student{     //用于存储学生的信息
  int num;
  char name[20];
  char sex;
  int age;
  struct Date birthday;//结构体嵌套
  char addr[30];
};
int main()
{
	struct student student1;//定义结构体变量student1
}

2.在声明类型的同时定义变量

struct 结构体名{
成员列表
}变量名列表;

例子:

struct student{     //用于存储学生的信息
  int num;
  char name[20];
  char sex;
  int age;
  struct Date birthday;//结构体嵌套
  char addr[30];
}student1,student2;//student1与student2是定义的变量

3.不直接类型名直接定义结构体类型变量

struct{
成员列表
}变量名列表;

注意:当我们使用这种方法定义结构体类型时,变量只能在变量名列表处定义,不能再定义其他的变量,因为这种方式定义的结构体类型没有结构体名

注意:结构体类型在编译时是不分配空间的,只对变量分配空间

1.3结构体变量的使用

结构体变量的使用:

结构体变量名.成员名能引用结构体变量里面的各个属性

#include<stdio.h>
struct Date{        //用于存储学生的出生年月
  int month;
  int day;
  int year;
};
struct student{     //用于存储学生的信息
  int num;
  char name[20];
  char sex;
  int age;
  struct Date birthday;//结构体嵌套
  char addr[30];
};
int main()
{
	struct student student1;//定义结构体变量student1
	printf("请输入学生的学号:");
	scanf("%d",&student1.num);
	printf("请输入学生的姓名:");
	scanf("%s",student1.name);
	printf("请输入学生的性别(M/W):");
	getchar();//吸收回车
	scanf("%c",&student1.sex);
	printf("请输入学生的年龄:");
	scanf("%d",&student1.age);
	printf("请输入学生的地址:");
	scanf("%s",student1.addr);
    printf("请分别输入学生出生年、月、日:");
    scanf("%d%d%d",&student1.birthday.year,&student1.birthday.month,&student1.birthday.day);
    
    printf("该学生的学号为:%d\n",student1.num);
    printf("该学生的姓名为:%s\n",student1.name);
    printf("该学生的性别为:%c\n",student1.sex);
    printf("该学生的年龄为:%d\n",student1.age);
    printf("该学生的地址为:%s\n",student1.addr);
    printf("该学生的出生年月为:%d %d %d",student1.birthday.year,student1.birthday.month,student1.birthday.day);
 
}
//输入输出:
请输入学生的学号:1001
请输入学生的姓名:张三
请输入学生的性别(M/W):M
请输入学生的年龄:18
请输入学生的地址:南阳
请分别输入学生出生年、月、日:2000 10 12
该学生的学号为:1001
该学生的姓名为:张三
该学生的性别为:M
该学生的年龄为:18
该学生的地址为:南阳
该学生的出生年月为:2000 10 12
--------------------------------

在这里插入图片描述

2.结构体数组

2.1结构体数组的定义

1.在定义结构体类型的时候定义结构体数组:
struct 结构体名{
成员列表
}数组名[数组长度];
2.使用结构体类型名定义结构体数组
结构体类型名 数组名[数组长度];

2.2结构体数组的初始化

其实和int等类型的数组初始化差不多,举个栗子:

#include<stdio.h>
struct student{    
  int num;
  char name[20];
};
int main()
{
	struct student stu[2]={1001,"张三",1002,"李四"};
	//或者	struct student stu[2]={{1001,"张三"},{1002,"李四"}};
	for(int i=0;i<2;i++)
	{
		printf("第%d个学生的学号是:%d\n",i+1,stu[i].num);
		printf("第%d个学生的姓名是:%s\n",i+1,stu[i].name);
	}
}
//输出结果:1个学生的学号是:10011个学生的姓名是:张三
第2个学生的学号是:10022个学生的姓名是:李四

--------------------------------

在这里插入图片描述

3.结构体指针

如果对指针操作不熟悉的话,可以参照这一篇博客:万字长文搞定C语言指针

3.1指向结构体变量的指针

定义结构体指针:结构体类型名+结构体指针
使用结构体指针:(*结构体指针).属性或者结构体指针->属性

例子:

#include<stdio.h>
struct student{    
  int num;
  char name[20];
};
int main()
{
	struct student *stu;
	struct student s;
	printf("请输入学生的学号:");
	scanf("%d",&s.num);
	printf("请输入学生的姓名:");
	scanf("%s",s.name);
	stu=&s;
	printf("该学生的学号为:%d\n",stu->num);
	//等同于	printf("该学生的学号为:%d\n",(*stu).num);
	printf("该学生的姓名为:%s",stu->name);	
	//等同于	printf("该学生的学号为:%s\n",(*stu).name);
}

3.2指向结构体数组的指针

例子:

#include<stdio.h>
struct student{    
  int num;
  char name[20];
};
int main()
{
	struct student stu[2]={1001,"张三",1002,"李四"};
    struct student *p;
    int i=1;
	for(p=stu;p<stu+2;p++)
	{
		printf("第%d个学生的学号是:%d\n",i,p->num);
		printf("第%d个学生的姓名是:%s\n",i,p->name);
		i++;
	}
}

3.3结构体指针作为函数参数

结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。

计算全班学生的总成绩、平均成绩和以及 140 分以下的人数。

#include <stdio.h>

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
}stus[] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};

void average(struct stu *ps, int len);

int main(){
    int len = sizeof(stus) / sizeof(struct stu);
    average(stus, len);
    return 0;
}

void average(struct stu *ps, int len){
    int i, num_140 = 0;
    float average, sum = 0;
    for(i=0; i<len; i++){
        sum += (ps + i) -> score;
        if((ps + i)->score < 140) num_140++;
    }
    printf("sum=%.2f\naverage=%.2f\nnum_140=%d\n", sum, sum/5, num_140);
}
//运行结果:
sum=707.50
average=141.50
num_140=2

在这里插入图片描述

4.结构体内存对齐(重点)

我们定义一个结构体变量,查看所占据的内存字节数:

#include<stdio.h>
struct Date{        //用于存储学生的出生年月
  int month;
  int day;
  int year;
};
struct student{     //用于存储学生的信息
  int num;
  char name[20];
  char sex;
  int age;
  struct Date birthday;//结构体嵌套
  char addr[30];
};
int main()
{
	struct student stu1;
	printf("%d",sizeof(stu1));
}
//输出结果:76

我们按照正常的思路,num为int类型占据4个字节,name数组占据20个字节,sex占据1个字节,age占据4个字节,birthday占据12个字节,addr占据30个字节,一共是71个字节,为啥会是76个字节大小呢。这就牵扯到C语言的内存对齐问题。

结构体对齐原则一:
结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)

#include <iostream>
using namespace std;
struct X
{
    char a;
    int b;
    double c;
};
int main()
{
	printf("%d",sizeof(X));
}
//输出16

比如上面例子(占据16个字节):首先系统会将字符型变量a存入第0个字节(相对地址,指内存开辟的首地址);然后在存放整形变量b时,会以4个字节为单位进行存储,由于第一个四字节模块已有数据,因此它会存入第二个四字节模块,也就是存入到4~8字节;同理,存放双精度实型变量c时,由于其宽度为8,其存放时会以8个字节为单位存储,也就是会找到第一个空的且是8的整数倍的位置开始存储,此例中,此例中,由于头一个8字节模块已被占用,所以将c存入第二个8字节模块。整体存储示意图如图1所示。

在这里插入图片描述

原则二:
在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍

#include <iostream>
using namespace std;
struct X
{
	char a;
	double b; 
    int c;
};
int main()
{
	printf("%d",sizeof(X));
}
//输出24

上面这个例子中:我们分析完后的存储长度为20字节,不是最宽元素长度8的整数倍,因此将它补齐到8的整数倍,也就是24。这样就没问题了。其存储示意图如图2所示。
在这里插入图片描述

当成员变量里含有指针、数组或是其它结构体变量

1.包含指针类型的情况。只要记住指针本身所占的存储空间是8个字节就行了,而不必看它是指向什么类型的指针
2.包含结构体类型时,检查计算出的存储单元是否为所有元素(也包括所含结构体变量的元素)中最宽的元素的长度的整数倍
3.包含数组类型,内存对齐时只看基类型,如果数组基类型宽度最大,那么内存补齐时要是这个基类型的整数倍

此时开头的问题就能迎刃而解了:

#include<stdio.h>
struct Date{        //用于存储学生的出生年月
  int month;
  int day;
  int year;
};
struct student{     //用于存储学生的信息
  int num;
  char name[20];
  char sex;
  int age;
  struct Date birthday;//结构体嵌套
  char addr[30];
};
int main()
{
	struct student stu1;
	printf("stu1首地址:%d\n",&stu1);
	printf("num首地址:%d\n",&stu1.num);
	printf("name首地址:%d\n",stu1.name);
	printf("sex首地址:%d\n",&stu1.sex);
	printf("age首地址:%d\n",&stu1.age);
	printf("birthday首地址:%d\n",&stu1.birthday);
	printf("addr首地址:%d\n",stu1.addr);
	printf("stu1占据总的字节数是:%d\n",sizeof(stu1));
}
stu1首地址:6684112
num首地址:6684112
name首地址:6684116
sex首地址:6684136
age首地址:6684140
birthday首地址:6684144
addr首地址:6684156
stu1占据总的字节数是:76

--------------------------------

在这里插入图片描述

5.typedef的使用

C语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。

起别名的目的不是为了提高程序运行效率,而是为了编码方便。例如有一个结构体的名字是 stu,要想定义一个结构体变量就得这样写:

struct stu stu1;

struct 看起来就是多余的,但不写又会报错。如果为 struct stu 起了一个别名 STU,书写起来就简单了:

STU stu1;

这种写法更加简练,意义也非常明确,不管是在标准头文件中还是以后的编程实践中,都会大量使用这种别名。

使用关键字 typedef 可以为类型起一个新的别名。typedef 的用法一般为:

typedef  oldName  newName;

oldName 是类型原来的名字,newName 是类型新的名字。例如:

typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;

INTEGER a, b;等效于int a, b;。

typedef 还可以给数组、指针、结构体等类型定义别名。先来看一个给数组类型定义别名的例子:

typedef char ARRAY20[20];

表示 ARRAY20 是类型char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:

ARRAY20 a1, a2, s1, s2;

它等价于:

char a1[20], a2[20], s1[20], s2[20];

又如,为结构体类型定义别名:

typedef struct stu{
    char name[20];
    int age;
    char sex;
} STU;

STU 是 struct stu 的别名,可以用 STU 定义结构体变量:

STU body1,body2;

它等价于:

struct stu body1, body2;

再如,为指针类型定义别名:

typedef int (*PTR_TO_ARR)[4];

表示 PTR_TO_ARR 是类型int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:

PTR_TO_ARR p1, p2;

按照类似的写法,还可以为函数指针类型定义别名:

typedef int (*PTR_TO_FUNC)(int, int);
PTR_TO_FUNC pfunc;

对于给函数指针类型定义别名:

我们知道,单独的int (*p)(int,int),p代表定义了一个函数指针(其返回值为int,两个int形参数),

#include<stdio.h>
int max(int a,int b)
{
	return a>b?a:b;
}
int main()
{
	int (*p)(int,int);
	p=max;
	int a=(*p)(20,30);
	printf("%d",a);
	
}
//输出30

如果变成typedef int (*p)(int,int)。p就变成定义函数指针变量的一个类型,就像int一样,int a代表定义一个整形变量,p a,代表定义一个函数指针变量,p a还等同于int (*a)(int,int)

#include<stdio.h>
int max(int a,int b)
{
	return a>b?a:b;
}
int main()
{
	typedef int (*p)(int,int);
	p a;
	a=max;
	int c=(*a)(20,30);
	printf("%d",c);		
	
}
//输出30

同样的对于typedef char (*PTR_TO_ARR)[30];

在这里插入图片描述

在这里插入图片描述

#include <stdio.h>

typedef char (*PTR_TO_ARR)[30];
typedef int (*PTR_TO_FUNC)(int, int);

int max(int a, int b){
    return a>b ? a : b;
}

char str[3][30] = {
    "http://c.biancheng.net",
    "C语言中文网",
    "C-Language"
};

int main(){
    PTR_TO_ARR parr = str;
    PTR_TO_FUNC pfunc = max;
    int i;
   
    printf("max: %d\n", (*pfunc)(10, 20));
    for(i=0; i<3; i++){
        printf("str[%d]: %s\n", i, *(parr+i));
    }

    return 0;
}

typedef 和 #define 的区别

typedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。#define是在预编译时进行简单的字符串替换,而typedef是在编译阶段完成的

  1. 可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。因为使用typedef相当于定义一个新的类型如下所示:
#define INTERGE int
unsigned INTERGE n;  //没问题

typedef int INTERGE;
unsigned INTERGE n;  //错误,不能在 INTERGE 前面添加 unsigned
  1. 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
#define PTR_INT int *
PTR_INT p1, p2;

经过宏替换以后,第二行变为:

int *p1, p2;

这使得 p1、p2 成为不同的类型:p1 是指向 int 类型的指针,p2 是 int 类型。

相反,在下面的代码中:

typedef int * PTR_INT
PTR_INT p1, p2;

p1、p2 类型相同,它们都是指向 int 类型的指针。

在这里插入图片描述

6.动态内存分配与释放

C语言内存简介

内存区域内容
栈区存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈
堆区就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。
静态区全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。
常量区常量存储在这里,不允许修改。
代码区顾名思义,存放代码

1.void *malloc(unsigned int size):作用是在动态存储区中分配一个长度为size的连续空间,unsigned代表没有符号位的整形数据(非负整数),返回所分配内存区域第一个字节的地址.分配失败返回NULL指针
2.void *calloc(unsigned n,unsigned size):作用是在动态内存空间中分配n个长度为size的连续空间,分配失败返回NULL指针
3.void free(void *p):释放指针变量p所指向的动态空间
4.void *realloc(void *p,unsigned int size):对已经通过malloc函数calloc函数获得了动态空间,想改变其大小,用此函数重新分配

注意:void*类型的指针表示指向空类型或者不指向确定的类型的数据

以上函数得使用#include<stdlib.h>

使用举例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int i=0;
	int *p=(int*)malloc(4);//(函数前的int*代表把分配的内存转换成int*类型)
	*p=3;
    printf("%d\n",*p);
    free(p);
}

7.链表的创立、增删查改插

7.1什么是链表?

当一个班级有50个人,那么我们需要定义数组长度为50的结构体数组来存储这些学生的信息,如果新来了几个学生,那么我们则需要再重新定义一个更大的数组,比如60个大小(你不能申请一个足够大的数组,这样做太浪费内存空间)。这时候就可以用我们的链表存储。

链表是一种动态的进行存储分配的一种结构

看下面一段代码:

struct student{
  int num;
  char name[20];
  struct student *next;
}

上面一段代码中,结构体类型是struct student,存储着学生的基本信息,里边还有一个struct student *next成员变量,他是一个指向struct student类型数据的指针,也就是说,他的存储结构可以如下所示:

在这里插入图片描述
这种能够把数据之间进行连接的数据结构称为链表,链表中的每一个元素称为结点,每个结点中存放指针的空间称为指针域,存放其他信息的空间称为数据域

一般的链表有头指针头结点,尾结点指向NULL,如下:
在这里插入图片描述

我们可以把链表想象成一个火车,头结点相当于火车头,火车头负责连接第一节车厢,火车头里边不放乘客只是与第一个存放乘客的车厢建立联系(相当于数据域不赋值,指针域指向第一个结点)

头结点:头结点不是链表必须的部分,有了头结点,在对第一个结点前插入或者删除第一个结点就和其余结点统一了
头指针:头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。头指针是链表的必要元素

实例:

#include<stdio.h>
struct student{     //用于存储学生的信息
  int num;
  char name[20];
  struct student *next;
};
int main()
{
	struct student stu2={1002,"李四",NULL};
	struct student stu1={1001,"张三",&stu2};
	struct student stu0;
	stu0.next=&stu1;
	struct student *head=&stu0;
	struct student *p=head->next;//此时p指向第一个结点stu1
	int i=1;
	while(p!=NULL)//遍历学生信息
	{
		printf("第%d个学生的学号为:%d",i,p->num);
		printf("第%d个学生的姓名为:%s\n",i,p->name);
		i++;
		p=p->next;
	}
}
//输出:1个学生的学号为:10011个学生的姓名为:张三
第2个学生的学号为:10022个学生的姓名为:李四

--------------------------------

上面程序对应下图:
在这里插入图片描述

7.2静态链表,动态链表

所有结点都是在程序中定义的,不是我们自己申请的内存(由系统自动分配内存空间),用完后系统自动释放,这种链表称为静态链表。如7.1的例子就是如此,所谓动态链表就是我们手动开辟内存存放结点,需要回收时我们手动释放的链表

7.3链表的创建

代码实现:

//声明节点结构
typedef struct Link{
    int  elem;//存储整形元素
    struct Link *next;//指向直接后继元素的指针
}link;
//创建链表的函数
link * initLink(){
    link * p=(link*)malloc(sizeof(link));//创建一个头结点
    link * temp=p;//声明一个指针指向头结点,用于遍历链表
    //生成链表
    for (int i=1; i<5; i++) {
     //创建节点并初始化
        link *a=(link*)malloc(sizeof(link));
        a->elem=i;
        a->next=NULL;
        //建立新节点与直接前驱节点的逻辑关系
        temp->next=a;
        temp=temp->next;
    }
    return p;
}


从实现代码中可以看到,该链表是一个具有头节点的链表。由于头节点本身不用于存储数据,因此在实现对链表中数据的"增删查改"时要引起注意。

在这里插入图片描述

7.4链表的插入元素

根据添加位置不同,可分为以下 3 种情况:

  1. 插入到链表的头部(头节点之后),作为首元节点;
  2. 插入到链表中间的某个位置;
  3. 插入到链表的最末端,作为链表中最后一个数据元素;

虽然新元素的插入位置不固定,但是链表插入元素的思想是固定的,只需做以下两步操作,即可将新元素插入到指定的位置:

  1. 将新结点的 next 指针指向插入位置后的结点;
  2. 将插入位置前结点的 next 指针指向插入结点;

例如,我们在链表 {1,2,3,4} 的基础上分别实现在头部、中间部位、尾部插入新元素 5,其实现过程如图 :
在这里插入图片描述

从图中可以看出,虽然新元素的插入位置不同,但实现插入操作的方法是一致的,都是先执行步骤 1 ,再执行步骤 2。

注意:链表插入元素的操作必须是先步骤 1,再步骤 2;反之,若先执行步骤 2,除非再添加一个指针,作为插入位置后续链表的头指针,否则会导致插入位置后的这部分链表丢失,无法再实现步骤 1。

代码实现:

//p为原链表,elem表示新数据元素,add表示新元素要插入的位置
link * insertElem(link * p, int elem, int add) {
    link * temp = p;//创建临时结点temp
    //首先找到要插入位置的上一个结点
    for (int i = 1; i < add; i++) {
        temp = temp->next;
        if (temp == NULL) {
            printf("插入位置无效\n");
            return p;
        }
    }
    //创建插入结点c
    link * c = (link*)malloc(sizeof(link));
    c->elem = elem;
    //向链表中插入结点
    c->next = temp->next;
    temp->next = c;
    return p;
}

回想这一句话:

头结点:头结点不是链表必须的部分,有了头结点,在对第一个结点前插入或者删除第一个结点就和其余结点统一了

由于有了头结点,所以插入的时候,在第一个结点前插入就和所有结点进行了统一。否则????(你懂的)

7.5链表的删除元素

从链表中删除指定数据元素时,实则就是将存有该数据元素的节点从链表中摘除,但作为一名合格的程序员,要对存储空间负责,对不再利用的存储空间要及时释放。因此,从链表中删除数据元素需要进行以下 2 步操作:

  1. 将结点从链表中摘下来;
  2. 手动释放掉结点,回收被结点占用的存储空间;

其中,从链表上摘除某节点的实现非常简单,只需找到该节点的直接前驱节点 temp,执行一行程序:

temp->next=temp->next->next;

例如,从存有 {1,2,3,4} 的链表中删除元素 3,则此代码的执行效果如图 2 所示:

在这里插入图片描述
代码实现:

//p为原链表,add为要删除元素的值
link * delElem(link * p, int add) {
    link * temp = p;
    //遍历到被删除结点的上一个结点
    for (int i = 1; i < add; i++) {
        temp = temp->next;
        if (temp->next == NULL) {
            printf("没有该结点\n");
            return p;
        }
    }
    link * del = temp->next;//单独设置一个指针指向被删除结点,以防丢失
    temp->next = temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
    free(del);//手动释放该结点,防止内存泄漏
    return p;
}

我们可以看到,从链表上摘下的节点 del 最终通过 free 函数进行了手动释放。

同样的因为有了头结点,才会有对所有结点的删除实现统一

7.6链表的查找元素

在链表中查找指定数据元素,最常用的方法是:从表头依次遍历表中节点,用被查找元素与各节点数据域中存储的数据元素进行比对,直至比对成功或遍历至链表最末端的 NULL(比对失败的标志)。

//p为原链表,elem表示被查找元素、
int selectElem(link * p,int elem){
//新建一个指针t,初始化为头指针 p
    link * t=p;
    int i=1;
    //由于头节点的存在,因此while中的判断为t->next
    while (t->next) {
        t=t->next;
        if (t->elem==elem) {
            return i;
        }
        i++;
    }
    //程序执行至此处,表示查找失败
    return -1;
}


注意,遍历有头节点的链表时,需避免头节点对测试数据的影响,因此在遍历链表时,建立使用上面代码中的遍历方法,直接越过头节点对链表进行有效遍历。

7.7链表更新元素

更新链表中的元素,只需通过遍历找到存储此元素的节点,对节点中的数据域做更改操作即可。

//更新函数,其中,add 表示更改结点在链表中的位置,newElem 为新的数据域的值
link *amendElem(link * p,int add,int newElem){
    link * temp=p;
    temp=temp->next;//在遍历之前,temp指向首元结点
    //遍历到待更新结点
    for (int i=1; i<add; i++) {
        temp=temp->next;
    }
    temp->elem=newElem;
    return p;
}

7.8链表的添加元素

头插法:

link *(link * p){
    link * temp=p;//temp初始化为头指针
    int count;
    printf("请输入要插入的元素数:\n");
    scanf("%d",&count);
    for(int i=0;i<count;i++){
       link *a=(link*)malloc(sizeof(link));
       a->elem=i;//假设添加的元素等于本轮的i
       a->next=temp->next;
       temp->next=a;
    }
    return p;
}

尾插法:

link *(link * p){
    link * temp=p;//temp初始化为头指针
    int count;
    printf("请输入要插入的元素数:\n");
    scanf("%d",&count);
     while(temp->next!=NULL){
          temp=temp->next;
      }
    for(int i=0;i<count;i++){
      link *a=(link*)malloc(sizeof(link));
      a->elem=i;//假设添加的元素等于本轮的i 
      temp->next=a;
      a->next=NULL;
      temp=a;
    }
    return p;
}

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

详解结构体与链表 的相关文章

  • 哈夫曼压缩算法-Python实现

    前面上传了A 算法的实现 xff0c 就顺便把一起写的哈夫曼压缩也上传了吧 本文主要提供了Python版本的哈夫曼压缩算法实现 xff0c 并在此基础上提供了命令行和基于Qt的GUI用户界面 xff08 User Interface xff
  • 内存分段与内存分页:逻辑地址、物理地址、线性地址、虚拟地址

    这篇文章也是我自己的博客网站的里的文章 xff0c 我觉得这篇文章还是我觉得知识含量比较高的文章 xff0c 所以特地把它发出来看看 这篇文章写于我在写自己的操作系统JackOS的时候系统梳理了一下CPU访问内存的各种方式 xff0c 写完
  • VSCode调试C/C++项目

    最近写完了自己的操作系统 xff0c 深感有一个方便的调试环境是有多么重要 xff0c 能够提升不少开发效率 恰好最近在的技术交流群里群友在问如何搭建VSCode调试操作系统的环境 xff0c 刚考完试 xff0c 就先把这篇VSCode调
  • 误差与精度

    整理自 误差理论与数据处理 合肥工业大学 机械专业用于教授学生误差与精度概念的课程叫做 公差与测量 或者叫做 机械精度设计 xff0c 而公差或者精度的本质含义就是误差的大小 xff0c 公差越小 xff0c 误差越小 xff0c 精度越高
  • 两个类的头文件互相包含

    两个类的头文件互相包含 我做任务的时候遇到了两个类都互相包含对方的对象的问题 xff0c 本来是有错误的 xff0c 但经过我一番改动 xff0c 两个头文件互相包含同时 xff0c 每个头文件都含有令一个类的前置声明 虽然最后运行正确 x
  • 【C++ STL 容器】——vector

    概述 vector容器也被称作向量 xff0c 实现了动态的数组 xff0c 用于元素数量变化的对象数组 xff0c 算是比较常用的容器 常用函数 构造函数 vector 创建一个空vectorvector int size 创建一个vec
  • 2021-07-22

    MSP432在keil中通过CMSIS DAP下载程序出现cannot enter debug mode的解决办法 xff1a MSP432下载程序出现cannot enter debug mode 可以通过修改如下设置 Debug里面的两
  • 通信协议基础以及常用的串口通信协议

    通信协议 xff1a 串行通信和并行通信 在数据的通信方式中根据数据传输方式的不同可以分为 xff1a 串行通信和并行通信 串行通信 xff1a 串行通信是指使用一条数据线 xff0c 将数据一位一位地依次传输 xff0c 每一位数据占据一
  • Ubuntu安装ROS melodic,管理环境,创建工作空间

    一 安装ROS 1 设置源 xff1a sudo sh c 39 etc lsb release amp amp echo 34 deb http mirrors tuna tsinghua edu cn ros ubuntu 96 lsb
  • HTTP请求报文的结构组成及URL的结构组成

    HTTP请求报文 HTTP 超文本传输协议 Hypertext Transfer Protocol xff0c 简称HTTP 是应用层协议 HTTP 是一种请求 响应式的协议 xff0c 即一个客户端与服务器建立连接后 xff0c 向服务器
  • Qt之旅_001>>Qt常用窗口类之间的关系

    QApplication xff0c QGuiApplication QCoreApplication三者之间的关系 QCoreApplication处于core模块中 xff0c 为应用程序提供了一个非gui的时间循环 xff1b QGu
  • GPIO相关介绍

    文章目录 GPIO概念TXD与RXD GPIO的使用注意STM32IO口哪些兼容5V一定不要接超过5V的电压默认不能做输出的GPIO GPIO硬件原理图GPIO地址 GPIO的八种工作模式浮空输入带上拉输入带下拉输入模拟输入开漏输出推挽输出
  • STM32的常用C语言

    文章目录 一些被坑了的注意点 int16 define结构体与共用体指针 C语言发展史C语言概述C90 标准C99标准C11标准 C编译o代替c 条件语句else ifdo while 变量定义一个字符串字符串结尾 定义一个字符串数组sta
  • STM32应用霍尔转速传感器基于输入捕获

    这里我用通用定时器3的通道1来测量转速 霍尔转速传感器基本介绍霍尔传感器分类和原理关于为什么选用开关型常开PNP型霍尔传感器 STM32程序实现程序介绍程序源码TIM3 CAP HTIM3 CAP H解读TIM3 CAP CTIM3 CAP
  • Android so库开发——使用Studio生成自己的so库(一)

    一 创建Native项目 1 新建 Native 项目 1 xff09 新建项目 选择最下面的 Native C 43 43 下一步即可 2 xff09 填写项目信息 3 xff09 选择C 43 43 版本可以直接选择默认 2 下载并配置
  • C语言实现线性回归求斜率

    2020 11 22 修改 span class token comment 线性回归求斜率 注意数据类型 参数 count 数据个数 数组行 列 的个数 数组的行列数目相等 参数 dataCol X 数据的列数据 参数 dataRow Y
  • 【C语言】详解位域定义与使用

    位域的定义 span class token keyword struct span span class token class name bit span span class token punctuation span span c
  • C语言实现MQTT协议(一)协议讲解

    MQTT介绍 MQTT是一个客户端服务端架构的发布 订阅模式的消息传输协议 它的设计思想是轻巧 开放 简单 规范 xff0c 易于实现 这些特点使得它对很多场景来说都是很好的选择 xff0c 特别是对于受限的环境如机器与机器的通信 xff0
  • 【STM32】HAL库-外部中断

    外部中断框图 产生中断 硬件触发外部中断 配置中断屏蔽寄存器中的屏蔽位 xff0c 允许该外部中断请求 通过AFIO EXTICRx配置GPIO线上的外部中断 事件 xff0c 必须先使能AFIO时钟 选择外部中断的触发边沿 xff0c 上

随机推荐

  • 【STM32】HAL库-系统滴答定时器SysTick

    SysTick定时器被捆绑在NVIC中 xff0c 是一个简单的定时器 xff0c 对于CM3 CM4内核芯片 xff0c 都有Systick定时器 Systick定时器常用来做延时 xff0c 或者实时系统的心跳时钟 这样可以节省MCU资
  • 【STM32】HAL库-串口USART

    USART简介 通用同步异步收发器 USART 提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换 USART利用分数波特率发生器提供宽范围的波特率选择 一个波特率寄存器 USART BRR xff0c
  • 【STM32】HAL库-通用定时器

    简介 通用定时器是一个通过可编程预分频器驱动的16位自动装载计数器构成 它适用于多种场合 xff0c 包括测量输入信号的脉冲长度 输入捕获 或者产生输出波形 输出比较和PWM 使用定时器预分频器和RCC时钟控制器预分频器 xff0c 脉冲长
  • 【STM32】HAL库-SPI

    3线全双工同步传输 带或不带第三根双向数据线的双线单工同步传输 8或16位传输帧格式选择 主或从操作 支持多主模式 8个主模式波特率预分频系数 最大为fPCLK 2 从模式频率 最大为fPCLK 2 主模式和从模式的快速通信 主模式和从模式
  • 【STM32】标准库-以太网外设-LAN8720A-LWIP-无操作系统

    TCP IP模型 TCP IP 只有四个分层 xff0c 分别为应用层 传输层 网络层以及网络访问层 xff08 物理层 xff09 实际上 xff0c 还有一个 TCP IP 混合模型 xff0c 分为五个层 它实际与 TCP IP四层模
  • 【STM32】标准库-LTDC-DMA2D

    LTDC STM32F429 系列芯片内部自带一个 LTDC 液晶控制器 xff0c 使用 SDRAM 的部分空间作为显存 xff0c 可直 接控制液晶面板 xff0c 无需额外增加液晶控制器芯片 STM32 的 LTDC 液晶控制器最高支
  • 【STM32】HAL库-以太网外设-LAN8720A-LWIP-无操作系统

    开发环境 KEIL MDK ARM 5 27MCU STM32F429IGT6PHY IC LAN8720ALWIP LWIP2 1 2STM32CUBEMX 6 6 1HAL V1 27 1 LAN8720A使用RMII接口与STM32的
  • git学习

    常用命令 查看当前文件夹下的文件与文件夹 xff1a ls ll 进入当前文件夹下的user文件夹 xff1a cd user 查看当前文件夹下的test txt文件 xff1a cat test txt 返回上一级目录 xff1a cd
  • 电赛专题---一.概述【电赛简介 /信号类需要准备什么?/怎么才能打好电赛?】

    1 电赛简介 全国大学生电子设计竞赛 xff08 National Undergraduate Electronics Design Contest xff09 是教育部和工业和信息化部共同发起的大学生学科竞赛之一 xff0c 是面向大学生
  • Postman安装

    1 官网下载 下载链接地址 xff1a https www postman com downloads 点击Download the App 根据自己的电脑以及需求选择对应的32位或者64位的版本 2 双击安装包 xff0c 不用任何操作
  • UART串口通信

    串口是 串行接口 的简称 xff0c 即采用串行通信方式的接口 串行通信将数据字节分成一位一位的形式在一条数据线上逐个传送 xff0c 其特点是通信线路简单 xff0c 但传输速度较慢 因此串口广泛应用于嵌入式 工业控制等领域中对数据传输速
  • Java第四课数据类型(二)

    一 变量类型 1 字节型 byte 2 整型 xff08 1 xff09 int 整型 4字节 xff08 2 xff09 show 短型 2字节 xff08 3 xff09 long 长型 8字节 3 浮点型 xff08 1 xff09
  • ESP32蓝牙配网

    注意 menuconfig 配置 xff08 必须打开蓝牙我这是C2所以使用NimBLE xff09 可以直接从demo的配置文件拷贝 Component config gt Bluetooth gt NimBLE BLE only Com
  • [C#] UDP协议 常用简单的代码(基于UdpClient类)(Thread实现)

    目录 说明1 消息发送端2 消息接收端 说明 在使用 C 开发Winform WPF等富客户端应用程序时 xff0c 有时会有 进程 之间 相互通信 的需求 下面是一种能够实现Udp 消息收发 常用且较为简单的 C 代码 使用注意 xff1
  • 掌控板OLED显示

    掌控板OLED显示 OLED显示文本内容 需要先将显示清空 xff0c 然后将想要显示的内容放在里面 xff0c 最后放入oled显示生效 源代码如下 xff1a span class token keyword from span mpy
  • 激光slam学习笔记1--RTK组合惯导、激光雷达传感器一些经验知识分享

    前言 xff1a 跟组合惯导和激光雷达打交道半年了 xff0c 过程中查找学习了这两方面的资料 xff0c 这里来个小结 如果有理解错误的 xff0c 望大佬们不吝赐教 一 RTK组合惯导 个人理解有两部分组成 xff0c 一个提供gps信
  • 计算机组成原理第五章:输入输出系统

    本章知识架构 接口 接口分类 xff1a 并行接口 xff1a 接口与系统总线 xff0c 接口与外设均按并行方式传送数据 串行接口 xff1a 接口与系统总线并行 xff0c 与外部设备串行 按I O传送控制方式划分 xff1a 直接程序
  • STM32串口通信 (缓冲区 发送不出数据&接收不到数据)

    STM32串口通信 流程附上代码注意事项一点心得 xff1a 流程 xff08 简单的发送数据 xff09 GPIO时钟使能串口时钟使能串口的GPIO配置写初始化串口函数 xff0c 配置串口USART Init USART1 amp US
  • 设计一个脉冲发生器,已知系统时钟为50MHz,生成脉冲宽度为1ms,脉冲间隔可调,最大间隔为1s

    设计一个脉冲发生器 xff0c 已知系统时钟为50MHz xff0c 生成脉冲宽度为1ms xff0c 脉冲间隔可调 xff0c 最大间隔为1s Design a pulse generator The system clock is kn
  • 详解结构体与链表

    目录 1 定义使用结构体变量 2 使用结构体数组 3 结构体指针 4 结构体内存对齐 重点 5 typedef的使用 6 动态内存的分配与释放 7 链表的创立 增删查改插 什么是链表 xff1f 静态链表和动态链表 链表的创立 链表的插入元