C语言——链表(带头节点与不带头节点)

✅简介:与大家一起加油,希望文章能够帮助各位!!!!

💬保持学习、保持热爱、认真分享、一起进步!!!


前言

上篇文章写了顺序表(数组),虽然我们现在可以创造出来动态增加的数组,但是动态数组还是有一系列的问题:


一、动态数组的弊端

缺点一:我们的数组需要连续的整片的空间,而我们计算机中没有这么大的空间。

如下图:图中我们可以清晰的看到当第三次申请空间的时候,计算机明明有这么多空间但是却申请失败,因为数组需要连续不间断的空间来存放数据。

 第二个缺点就是当我们扩容的时候,我们需要遍历之前的数据进行拷贝,浪费时间。当数据规模不大的时候,可以忽略。但是当数据达到一定程度后就会出现拷贝整个数组可能需要较长的时间,对性能会产生一定的影响。

二、单链表

1.链表的优势

  1. 动态性:链表是一种动态数据结构,可以在运行时灵活地插入、删除和修改元素,而不需要预先指定容量。这意味着链表可以根据需要动态地增长或缩小,提供更好的灵活性。

  2. 内存管理:链表的节点可以在内存中非连续地分布,每个节点可以存储任意数量的数据。这种内存分配方式可以更有效地利用内存空间,并且可以避免由于连续内存分配导致的碎片化问题。

 从下面这张图也可以看出,链表和数组刚好互补。

2.什么是链表 

链表是一种常见的线性数据结构,它由一系列的节点组成,每个节点包含两部分:数据和指向下一个节点的指针。每个节点可以在内存中不连续地分布,通过指针将它们串联在一起。下面呢,进入正题!!! 

 

3.如何定义一个链表

首先呢,我们先看这张图!这是带头节点,带头节点的链表是在链表头部添加了一个额外的节点,该节点不存储任何数据,只是作为链表的起始位置。带头节点的链表中,头节点的下一个节点才是实际存储数据的第一个节点。

 不带头节点的主要区别就是在于第一个的问题,不带头结点首先定义一个同类型的指针,指向第一个存放元素的地址值,直接从存储数据的第一个节点开始。这里呢,因为不带头节点的代码更麻烦一点,所以我们主要以不带头节点的链表进行讲解!

 


3.1底层代码逻辑 

首先,定义一个结构体(链表)

typedef struct _abc {
	struct _abc* next;//指向下一个
	int value;
} link;

 现在,我们拥有了一个个的节点,该怎样去把他们去串在一起呢?

看完下面的代码,如果你学过数据结构的话不难看出这段代码的时间复杂度O(n^2),怎样去降低这个时间的耗费呢?自己可以先停下来思考一下,后面会提到呦!!!

link* head = NULL; //定义头指针
	int num;
	while (true){
		printf("请输入链表中的值:");
		link* l = (link*)malloc(sizeof(link));
		scanf("%d", &num);
		if (num == -1) //如果num==-1结束循环
			break;
		l->value = num;
		l->next = NULL;
		link* k = head;
		if (k) {//如果k=NULL进入
			while (k->next) {
				k = k->next;
			}
			k->next = l; //循环结束,链表走到了最后一个位置,我们需要把新增加的元素加上去
		} else {
			head = l;//如果head==NULL的话(第一次读入数据),则让head指向l的地址
		}
	}

遍历链表!!! 千万要记得malloc得到的空间一定要还回去,每次循环就是把指向地址的指针后移,读出数据。

for (link* p = head; p; p = p->next) {
			printf("%d\t", p->value);
		}
	printf("\n");
    //释放资源
	list* li = p->head;
	while (li != NULL) {
		list* temp = li;
		li = li->next;
		free(temp);
	}

3.2链表元素的插入、删除操作 

数组要插入一个元素:首先我们需要把这个元素插入位置后面所有的元素后移一位,然后再增加进去。而链表就不需要这么麻烦:

  1. 创建一个新的节点,并将要插入的元素存储在该节点中。
  2. 将新节点的指针指向原本插入位置上的节点,即将新节点连接到链表中。
  3. 将插入位置前一个节点的指针指向新节点,即将前一个节点与新节点连接起来。
    如下图:

 

void inBehind(arr* m, int locat, int value) {
	if (locat < 0)
		printf("输入有误!\n");
	if (locat == 0) {
		list* l = (list*)malloc(sizeof(list));
		l->value = value;
		l->next = m->head;
		m->head = l;
	} else {
		list* p = m->head;
		for (int i = 0; i < locat - 1; p = p->next, i++); //因为函数头指向了第一个list所以这里需要-1
		list* l = (list*)malloc(sizeof(list));
		l->value = value;
		l->next = p->next;
		p->next = l;
	}
}

数组删除一个元素通常需要将被删除元素后面的所有元素向前移动一位,以填补被删除元素的位置。这样,就完成了元素在数组中的删除操作。相对于插入操作,数组的删除操作时间复杂度通常也是O(n),因为需要移动后续元素。

链表删除一个元素的步骤如下:

  1. 找到要删除的节点的前一个节点(如果是双向链表,还需找到后一个节点)。
  2. 将要删除的节点从链表中断开,即调整前一个节点(和后一个节点)的指针连接关系。

这样,就完成了元素在链表中的删除操作。相对于数组,链表的删除操作只需要修改节点之间的指针,时间复杂度通常是O(1)。

 

 

void del(arr* m,int locat){
	list* p=m->head;
	if(locat<1)
		printf("输入有误!\n");
	if(locat==1){
		list* a=p->next;
		free(p);
		m->head=a;
	}else{
		list* a=p;//让a指向p的后面 
		list* b=p->next;//b指向p的前面 
		for(int i=0;i<locat-1;i++){
			b=b->next;
			p=p->next;
			if(i==locat-2)
				continue;
			a=a->next;
		}
		//循环后,p指向了要删除的地址
		a->next=b;
		free(p);	//释放资源 
	}
}

 如果是插入或者删除数据在第一的元素的话需要我们单独讨论,而封装成函数的话,这时候我们需要改变head指针的指向,如果我们传入的只是head的话,是改变不了的head指针的指向的。在函数中修改指针指向的内容,不会对指针本身产生影响。如果你想修改指针本身的值(如指向不同的地址),你需要传递指针的指针或使用返回值将修改后的指针返回给调用函数。

4.如何让我们在读入数据时降低时间开销 

 避免重复遍历:在每次插入节点时,可以记录链表的尾部节点,避免每次都从头部开始遍历链表找到尾部节点。这样可以减少遍历的次数,提高效率。

typedef struct _list {
	struct _list* next;
	int value;
} list;
typedef struct {
	list* head;//链表头部
	list* behind;//链表尾部
} arr;

 上面定义一个struct结构体,一个指向链表头部,一个指向链表尾部。这样每次写入数据可以减少遍历的次数。时间复杂度为O(n)。

arr a;
	arr* p = &a;
	p->behind = NULL;
	p->head = NULL;
	printf("请输入添加的数据:");
	while (true) {
		int num;
		scanf("%d", &num);
		if (num == -1) break;
		list* q = (list*)malloc(sizeof(list));
		q->value = num;
		q->next = NULL;
		if (p->head == NULL) {
			p->head = q; //刚开始没有数据,让链表的头和尾的地址都指向q
			p->behind = q;
		} else {
			p->behind->next = q;
			p->behind = q;
		}
	}

总结

这篇文章主要讲述了单链表的一些操作,代码里面重要的地方也有注释的。

以后也会持续更新!!!

成功不是将来才有的,而是从决定去做的那一刻起,持续累积而成。

以上均是个人的理解,如果有不对的地方请各位大佬帮忙斧正!

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
好的,这个任务需要实现一个拓扑控制算法,然后用C语言编写程序来模拟这个算法的执行过程,并且通过图形化界面来展示算法的执行效果。下面是一个简单的实现思路: 1. 随机生成50个节点,每个节点的位置随机分布在指定的部署区域内,可以使用rand()函数来实现随机生成。 2. 建立一个全连通网络,即每个节点都与其他任意节点至少存在一条链路。可以使用Kruskal算法或Prim算法来生成最小生成树,也可以使用其他算法生成全连通网络。 3. 在全连通网络中进行拓扑控制,修改网络的拓扑结构。可以使用分布式拓扑控制算法,如Laplacian算法、PageRank算法、Kleinberg算法等。 4. 用图形化的界面展示算法的执行效果,包括算法执行前后的拓扑对比图、链路数量统计对比以及节点角色的标注。可以使用OpenGL库或其他图形库来实现图形化界面。 下面是一个简单的C语言程序框架,可以根据上述思路进行修改和完善: ```c #include <stdio.h> #include <stdlib.h> #include <time.h> #include <math.h> #include <GL/glut.h> #define N 50 // 节点数目 #define RADIUS 50.0 // 通信半径 #define AREA_SIZE 500 // 部署区域大小 typedef struct { float x, y; // 节点的坐标 int role; // 节点的角色,0表示普通节点,1表示控制节点 int degree; // 节点的度数,即与该节点相连的边的数量 int neighbors[N]; // 与该节点相连的节点的编号 } Node; Node nodes[N]; void drawNodes(); void drawEdges(); void drawText(char *str, float x, float y); void initNodes(); void initEdges(); void updateTopology(); int main(int argc, char **argv) { srand(time(NULL)); initNodes(); initEdges(); // 初始化OpenGL界面 glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB); glutInitWindowSize(600, 600); glutCreateWindow("Topology Control"); glutDisplayFunc(display); glutMainLoop(); return 0; } void initNodes() { // 随机生成节点坐标和角色 for (int i = 0; i < N; i++) { nodes[i].x = rand() % AREA_SIZE; nodes[i].y = rand() % AREA_SIZE; nodes[i].role = 0; nodes[i].degree = 0; for (int j = 0; j < N; j++) { if (i != j && sqrt(pow(nodes[i].x - nodes[j].x, 2) + pow(nodes[i].y - nodes[j].y, 2)) <= RADIUS) { nodes[i].neighbors[nodes[i].degree++] = j; } } } // 随机选择一个节点作为控制节点 int control_node = rand() % N; nodes[control_node].role = 1; } void initEdges() { // 生成全连通网络 for (int i = 0; i < N; i++) { for (int j = 0; j < nodes[i].degree; j++) { int neighbor = nodes[i].neighbors[j]; if (i < neighbor) { // 添加边 } } } } void updateTopology() { // 实现拓扑控制算法,修改拓扑结构 } void display() { glClear(GL_COLOR_BUFFER_BIT); // 绘制节点和边 drawNodes(); drawEdges(); // 绘制文本信息 char str[100]; sprintf(str, "Before topology control: %d links", countLinks()); drawText(str, 10, 10); updateTopology(); sprintf(str, "After topology control: %d links", countLinks()); drawText(str, 10, 30); glutSwapBuffers(); } void drawNodes() { // 绘制节点 } void drawEdges() { // 绘制边 } void drawText(char *str, float x, float y) { // 绘制文本信息 } int countLinks() { // 统计链路数量 } ``` 注意:以上代码只是一个简单的框架,需要根据具体的拓扑控制算法进行修改和完善。如果您对其中的细节有疑问,可以在评论区留言,我会尽快回复。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小羊没烦恼~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值