C语言数据结构之链表

目录

1.什么是链表

2.链表的定义

3.链表的分类

4.动态内存分配

4.1.malloc函数

4.2.动态数组的构造

4.3.动态内存和静态内存的比较

4.链表的创建

5.遍历链表

6.链表节点的插入

7.链表节点的删除


 

 


1.什么是链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

2.链表的定义

1.n个节点离散分配

2.彼此通过指针相连

3.每个节点只有一个前驱节点,每个节点只有一个后续节点,首节点没有前驱节点,尾节点没有后续节点。

  • 专业术语

首节点:第一个有效节点

尾节点:最后一个有效节点

头节点:第一个有效节点之前的那个节点;头结点不存放有效数据;头节点的作用是为了方便对链表的操作。

头指针:指向头结点的指针变量

尾指针:指向尾节点的指针变量

简图如下所示:

确定一个链表需要几个参数?

答:只需要一个参数,可以通过头指针可以推算出链表的其他所有信息。

3.链表的分类

  • 单链表
  • 双链表:每一个节点有两个指针域
  • 循环链表:能通过任何一个节点找到其他所有的节点
  • 非循环列表

 4.动态内存分配

  • 传统数组的缺点:

1.数组的长度必须事先制定,且只能是常整数,不能是变量

例子:

int a[5];   //正确

int len = 5;   int a[len];    //错误

2.传统形式定义的数组,该数组的内存程序员无法手动释放,在一个函数运行期间,系统为该函数中的数组所分配的内存会一直存在,直到该函数运行完毕,数组的空间才会被系统释放。

3.数组的长度一旦确定,其长度不能更改,数组的长度不能在函数运行过程中动态的扩充或减小

4.A函数定义的数组,在A函数运行期间可以被其他函数使用,但A函数运行完毕之后,A函数中的数组将无法在其他函数使用,传统定义的数组不能跨函数使用。

例子:

  • 为什么需要动态内存分配?

动态数组很好的解决了传统数组的这四个缺陷,传统数组也叫静态数组。

4.1.malloc函数

malloc是memory(内存)allocate(分配)的缩写!

动态内存分配举例-动态数组的构造

假设动态构造一个int型一维数组

int  *p =(int *)malloc(int len);

1.本语句分配了两块内存,一块是动态分配的,总共len个字节,另一块是静态分配的。

2.(int *) 强制把首地址强制转换成  int * 类型,告诉你的机器,返回地址指向的数据占几个字节。

例子1:

int main(void)
{
	int i = 5;   //静态分配了四个字节的空间
	int *p = (int *)malloc(4);
	/*
		1.使用malloc函数需要添加malloc.h头文件
		2.malloc只有一个形参,且为整型
		3.4,代表的是请求系统为本程序跟配4个字节的内存
		4.malloc函数只能返回第一个字节的地址(注意是第一个字节,只有一个字节)
		5.malloc函数所在的一行,一共分配了8个字节的空间,p指针变量占4个字节,p所指向的内存也占四个字节
		6.p本身所占的内存也是静态分配的,p所指向的内存是动态分配的
	*/
	*p = 5; //p是一个int * 类型的,那么 *p 就是int 类型的,所以能进行赋值运算,且操作的正是动态分配的内存
	free(p);   //free(p)表示把p所指向的内存释放掉,注意p本身的内存是静态的,不能由程序员手动释放。
	printf("hello\n");
	system("pause");
	return 0;

}

例子2:

void f(int * q)
{
	//*p =200;    //错误,*p不存在
    //q  =200;    //错误,q是int * 类型的
	*q = 200;   //OK
	free(q);   //把q所指向的内存释放掉
}
int main(void)
{
	int * p = (int *)malloc(sizeof(int));//sizeof(int)返回值是int所占的字节数,为4

	*p = 10;   //把10赋值给以p的内容为地址的变量,即为动态分配的内存

	printf("%d\n", *p);
	f(p);
	printf("%d\n", *p);
	system("pause");

}

预测一下这个程序的执行结果:

可以看到*p既然成了一个垃圾值,而不是200?为什么?

分析:

4.2.动态数组的构造

1.malloc只有一个int型的形参,表示要求系统分配的字节数

2.malloc函数的功能是请求系统len个字节的内存空间,如果请求分配成功,则返回第一个字节的地址,如果分配不成功,返回NULL.

注意:malloc函数能且只能返回第一个字节的地址,所以我们需要把这个无任何实际意义的第一个字节的地址(称为干地址)转化成一个有实际意义的地址,因此,malloc前面必须加(数据类型 *),表示把这个无实际意义的第一个字节的地址转化成相应类型的地址。如:

int *p = (int *)malloc(50);

表示将系统分配好的50个字节的第一个字节的地址转化成int *类型的地址,更精确的说是把第一个字节的地址转化成四个字节的地址,这样p就指向了第一个四个字节,p+1就指向了第2个四个字节,p+i就指向了第i+1个的第4个字节。p[0]就是第一个元素,p[i]就是第i+1个元素。

double *p =(double *)malloc(80);

表示将系统分配好的80个字节的第一个字节的地址转化成double *类型的地址,更精确的说是把第一个字节的地址转化成8个字节的地址,这样p就指向了第一个8个字节,p+1就指向了第2个8个字节,p+i就指向了第i+1个的第8个字节。p[0]就是第一个元素,p[i]就是第i+1个元素。

例子:

4.3.动态内存和静态内存的比较

  • 静态内存是系统自动分配的,由系统自动释放。
  • 静态内存是在栈内分配的。
  • 动态内存是由程序员手动分配的,手动释放。
  • 动态内存是在堆分配的

跨函数使用内存的问题:

  • 静态内存不可以跨函数使用
  • 所谓静态内存不可以跨函数使用准确的说法:静态内存在函数执行期间可以被其他函数使用,静态内存在函数执行完毕之后就不能被其他函数使用了
  • 动态数组可以跨函数使用:动态内存在函数执行完毕之后任然可以被其他函数使用。

 

4.链表的创建

首先创建一个结构体用于存放链表一些静态内存分配的数据,如下:

typedef struct Node
{
	int data;           //数据域
	struct Node * pNext;//指针域
}NODE,* PNODE;          //NODE等价于struct Node,PNODE 等价于 struct Node *

链表的创建代码如下:

PNODE create_list(void)   //创建链表,返回一个PNODE类型的值,为头结点指针
{
	int len;
	int i;
	int val; //用来存放临时节点的数值
     //分配了一个不存放数据的头结点
	PNODE Phead=(PNODE)malloc(sizeof(NODE));  //创建一个头节点
	if(NULL==Phead)     //
	{
		printf("内存分配失败,程序终止!\r\n");
		exit(-1);
	}
	PNODE Ptail=Phead; //建立一个中间节点,然后然头节点指向空
	Ptail->pNext=NULL;  
	printf("请输入要创建链表节点的个数 len= ");
	scanf("%d",&len);
   for(i=0;i<len;i++)
   {
	   printf("请输入第%d个节点的值",i+1);
	   scanf("%d",&val);

	   PNODE Pnew = (PNODE)malloc(sizeof(NODE));
	   
	   if(NULL==Pnew)
	   {
		printf("内存分配失败,程序终止!\r\n");
		exit(-1);
	   }
	   Pnew->data=val;      //给新的块的数据赋值,其地址为Pnew
	   Ptail->pNext=Pnew;   //Ptail 第一次保存的是Phead ,然后下一次
	   Pnew->pNext=NULL;    //保存的是Phead->PNext(也是Pnew)
	   Ptail=Pnew;  //以此类推,每一次会把当前的地址更新
   }

  return Phead;
}

代码分析:

首先使用malloc函数动态分配内存,创建一个头结点:

 //分配了一个不存放数据的头结点
	PNODE Phead=(PNODE)malloc(sizeof(NODE));  //创建一个头节点
	if(NULL==Phead)     //
	{
		printf("内存分配失败,程序终止!\r\n");
		exit(-1);
	}

动态分配一个siziof(NODE) ,也就是NODE结构体大小的动态内存,然后强制转换成 (NODE *) 类型,即为:PNODE 类型

返回分配内存的首地址给Phead。而Phead的数据类型是PNODE的,其存放的是NODE类型变量的地址。

在这里需要注意,此处分配了两块内存,一个是静态分配的,一个是动态分配的。

Phead 就是静态分配的,数据类型是PNODE,  malloc分配是出来的内存是动态的,大小是NODE结构体大小。

那么Phead所占的大小是多少?

一个指针变量到底占用几个字节的内存空间?

预备知识

使用函数:sizeof(数据类型)

功能:返回值就是该数据类型所占的字节数。

例子:

sizeof(int) =4    sizeof(char) =1     sizeof(double) =8

sizeof(变量名)

功能:返回值是该变量所占的字节数。


 假设p指向char类型变量(一个字节),q指向int类型变量(4个字节),r指向double类型变量(8个字节)

请问:p q r本身所占的字节数是否有区别?

结论:一个指针变量,无论它指向的变量占几个字节,该指针变量本身只占四个字节,一个变量的 地址是用该变量首字节的地址来表示。


值得注意的是,在硬件中每个地址都有一个编号,而且都是一个字节一个编号的。

为什么一个变量的地址用首字节表示,那为什么指针变量需要使用四个字节?

举例:

房子的大小和房子的编号是没有关系的,就像是变量的大小和变量地址所占内容大小是没有关系的,因为一个变量的地址仅仅用首地址来表示。

  • 如果现在我的房子只有100间的话,那么我的房间编号用8个位(一个字节)表示就够咯,如上(0-255)。无论在哪个位置我都可以用一个字节来表示你的位置。
  • 但是如果你的房间有2的32次方(等于4G的空间)那么大,你的一个字节还够表示吗?一个字节最大只能表示255啊,后面的房间编号就表示不了了,所以此时你需要的房间编号数量应该大于或者等于房间的数量吧,那么就应该就是2的32次方个编号咯,转换成字节就是4个字节。所以用四个字节表示地址,最大的内存是4G,就是这个道理!

注意:通过上面知道,无论表示哪一个房间号都应该用四个字节的地址,即使表示的是第一个,例如第1个表示的地址为:0x0001,第16个:0x000F。

接着执行下面的语句,创建出首节点,然后对首节点赋值,接着,把头结点和首节点相连起来

这一个循环执行完了,如果还有下一次循环呢,这个程序是如何运行的呢?

下一次循环又是执行下面的语句:

         PNODE Pnew = (PNODE)malloc(sizeof(NODE));

		if (NULL == Pnew)
		{
			printf("内存分配失败,程序终止!\r\n");
			exit(-1);
		}
		//给新的块的数据赋值,其地址为Pnew
		Pnew->data = val;
		//Ptail 第一次保存的是Phead ,然后下一次
		Ptail->pNext = Pnew;  
		//保存的是Phead->PNext(也是Pnew)
		Pnew->pNext = NULL; 
		//以此类推,每一次会把当前的地址更新
		Ptail = Pnew;         


5.遍历链表

链表的遍历就是把所有链表里面的数据逐一输出。

当我们成功创建了一个链表之后,则数据应该是如下面简图所示:

头结点是不存放任何数据的,所以我们只需要判断头结点的指针域(pNext)是否为NULL,如果不为空说明有数据,那就输出,如果为NULL,说明已经到了尾节点,就不输出了。

代码如下:

void traveser_list(PNODE Phead)  //遍历链表
{
	PNODE p = Phead->pNext;
	while (NULL != p) //链表不为空
	{
		printf("%d ", p->data);
		p = p->pNext;

	}
	printf("\n");
	return;
}

执行流程,如下简图所示: 


6.链表的插入

先说一说节点插入的一个算法和思路:

  • 算法1:

假如我们现在的链表简图如下:

现在想在序号为①的块后面插入一个块(假设p指向这个块)

既然需要在①和②之间插入一个块的话,就需要把它们断开,如果直接断开的话,你就找不到咯,因为最后还是要把它们连接起来的,所以找一个中间变量 r 把它保存起来。

然后把①的指针域指向p指向的这个块也就是想要插入的块咯;

这样就剩下最后一步,把插入的块(p指向的块)的指针域指向块②就OK了。

这样子就把一个块插入我们想要的位置了,但是这种算法显得复杂一点,我们也可以使用另一种算法,先把想要插入的块的指针域保存块②(指向块②)的地址,然后把块①的指针域指向要插入的块。

  • 算法二

算法二相对算法一来说会简介很多。

 

整体代码如下:

bool insert_list(PNODE Phead,int pos,int val)//pos从1开始
{
    int i=0;
    PNODE p=Phead;
	while(NULL!=p->pNext && i<pos-1)//此函数的目的是进行指向定位
	                           //定位到插入元素前面一个元素
	{
		p=p->pNext;                 
		i++;                  //统计pos前面
	}
	if(i>pos-1 ||p->pNext==NULL)
	 return false;
	PNODE Pnew=(PNODE)malloc(sizeof(NODE)); //分配一个新的块
	if(Pnew==NULL)
	{
		printf("内部分配失败,终止运行\n");
		exit(-1);
	}
	Pnew->data=val;  //给新元素赋值
	Pnew->pNext=p->pNext;
	p->pNext=Pnew;
    return true;
}

此函数需要三个参数,第一个是指令哪个链表的头指针,第二个是插入的位置,第三个是插入的值。

首先,在函数中新建一个指针变量p保存链表的头指针:PNODE p=Phead;

其次,也是比较重要的一个步骤,就是定位到我们需要插入块的前面的序号

假如此时我想插入一个块到第3个位置,也就是在pos=2这个位置的后边,那我调用的函数就是

insert_list(Phead,3,10);  //在3的位置,插入一个值为10的数据

然后再进行循环判断,这两个条件仍然是成立的,继续

运行完这一段后,i已经是等于2了,不满足i<2这个条件了,所以跳出while循环,继续向下执行

if(i>pos-1 ||p->pNext==NULL)
	 return false;

对 i 的值和p->pNext的值进行判断是否能进行插入工作

此时  i=2 不满足 i >pos-1=2,此时p->pNext 应该是pos=3这个位置,所以也是不成立的,所以不返回false,继续向下执行。

假设现在我只有三个存放数据的块,但是我却想插入位置为pos=4的块可以吗?

可以接着上面的分析

可以看到此时while里面的两个条件仍然是成立的,所以可以继续执行循环语句

执行完上面的一次,两个条件都不成立了,那么就退出循环了,接着向下执行

if(i>pos-1 ||p->pNext==NULL)
	 return false;

此时i的值为3,不满足i>pos-1=3,但是p->pNext已经是NULL了,所以这个条件是成立的,直接就返回false了。

所以说如果只有三个有效节点的话,想插入第四个的话肯定是不成功的。


同理,可以看一看能否插入7的位置,答案肯定是不能的,因为根本没地方插进去,还是看看程序分析的逻辑吧

此时可以继续往下执行:

因为此时p->pNext 已经为NULL,所以循环不成立,所以跳出循环了,接着往下执行程序

if(i>pos-1 ||p->pNext==NULL)
	 return false;

此时i=3,不满足i>pos-1=6 ,但是满足p->pNext=NULL,所以返回的是false。不能进行插入工作

如果确认了可以插入块可,那么就动态分配一个新的数据块

PNODE Pnew=(PNODE)malloc(sizeof(NODE)); //分配一个新的块
	if(Pnew==NULL)
	{
		printf("内部分配失败,终止运行\n");
		exit(-1);
	}

然后对新的块进行赋值

Pnew->data=val;  //给新元素赋值

使用如上的插入算法2进行插入

Pnew->pNext=p->pNext;
p->pNext=Pnew;

通过上面我们已经把指针变量  p 定位到要插入位置的前一个块上 

插入过程如下图所示:


7.链表节点的删除

链表节点的删除思路和链表的插入差不多,只要熟悉了链表的插入的逻辑,理解删除相对来说会容易很多。

删除节点的思路,我们同样是需要三个参数,第一个:操作的链表;第二个:删除节点的位置;第三个:被删除的数值。

首次使用一个指针变量,定位到需要删除节点的上一个节点,判断是否能进行删除操作:

程序如下:

bool delete_list(PNODE Phead,int pos,int *pval)
{
  int i=0;
    PNODE p=Phead;
	while(NULL!=p->pNext && i<pos-1)  //此函数的目的是进行指向定位
	                           //定位到删除元素前面一个元素
	{
		p=p->pNext;                 
		i++;                  //统计pos前面
	}
	if(i>pos-1 ||p->pNext==NULL)
	 return false;

	PNODE r=p->pNext;     //先记住删除的节点,待会释放
	*pval=r->data;       //记住删除的值
	p->pNext=p->pNext->pNext;
	free(r);
	r->pNext=NULL;
    return true;

}

前面一段进行定位和判断是否能删除节点,详细看上一节吧

while(NULL!=p->pNext && i<pos-1)  //此函数的目的是进行指向定位
	                           //定位到删除元素前面一个元素
	{
		p=p->pNext;                 
		i++;                  //统计pos前面
	}
	if(i>pos-1 ||p->pNext==NULL)
	 return false;

因为我们删除的是p所指向的块的下一个节点,此时我们需要去记住我们要删除的节点,当我们操作完成之后再释放这块被我们删除的内存(这也是动态内存分配的好处)。

使用 r指针变量,记住将被删除块的地址:r = p->pNext;

然后为了验证,把删除的值也传出来      :*pval=r->data;

然后使用语句: p->pNext = p->pNext->pNext;  删除一个节点

 

接着执行:free(r);   释放r所指向的内存空间

此时还有一句:r->pNext=NULL; 在这里要注意,free(r);释放的是r所指向的空间,不是释放r的控件,r本身是系统静态分配的,程序员无法手动释放,只能在系统运行结束之后自动释放。


 

 

 

 

 

 

 

 

 

 

 

  • 12
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值