C的抽象数据类型:链表、队列
1 抽象数据类型
抽象数据类型ADT abstract data type;
抽象数据类型由基本数据结构封装而成,链表,队列,二叉树等都属于有基本数据结构封装而成的抽象数据类型;
2 链表
链表作用:统一管理和存储实时分配的动态内存;而在rtos系统中用链表来管理各类节点的优先级非常优美;
1.1 链表举例
1.1.1 链表结构存储数据举例
/*链表定义*/
struct files * phead=NULL; struct files *ppre , *pcurr; int Arrrate[10]= {1,2,3,4}; struct files { int rating ; struct files * pnext ; } /*使用Arrrate[10]来初始化链表;*/
for(int i=0;i<10;i++) { pcurr=(struct files *)malloc(sizeof(files)); if(phead==NULL) phead=pcurr; else ppre->pnext=pcurr; /*首先将当前结构体的地址放入上一个结构体的指针中*/ pcurr->rating= Arrrate[i]; pcurr->pnext=NULL; ppre=pcurr; /*存放当前结构体的地址,供下一个结构体的指针找到位置*/ }
1.1.2 链表结构取出数据举例
/*链表取出数据只能顺序取出;并且最后一个链表变量的pnext=NULL;*/
pcurr=phead; while(pcurr!=NULL) { printf("%d", pcurr->rating ); pcurr=pcurr->pnext; }
1.1.3 使用完后销毁链表内存
/*链表想要销毁内存也只能从phead开始顺序销毁;*/
pcurr=phead; while(pcurr != NULL) { phead=pcurr->pnext; free(pcurr); pcurr=phead; }
1.2 建立链表的抽象数据类型接口
目的:从用户的使用角度来对ADT数据类型进行封装,使得用户可以像调用普通接口的方式调用ADT数据类型;
以下例子只是举例如何对定义进行封装,进一步还需要提供封装后如何使用的函数声明以及函数定义;
typedef struct files { ... } Node; typedef Node * List; ... ... /*将前面的files结构体重新声明为Node,然后再将指向结构体的指针声明为List;*/ /*这样的话就将具体的代码抽象化了*/ /*然后利用这些抽象过的类型进行代码编写,有利于逻辑开发,也保护了代码的开发权限*/ /*List scores中的scores是地址来着,这样的话就将files结构体封装成了scores,使得用户直接操作scores就可以了*/
2 队列queue:
队列是一种简单的链表;称之为队列,是因为工作方式和排队相像;数据要求从队尾加入,队首离开(“先进先出”FIFO);
队列先进先出的特点是其结构所致,不值得着重强调;以下给出一个队列定义的例子;
2.1 队列:声明
#define MAXCNT 10
/*定义队列Queue;队列结构包含首节点,尾节点,节点项数*/ typedef struct queue{ struct Node * front; struct Node * rear; int itemsCnt; }Queue; /*定义节点Node;Item结构存储每个节点的数据,作为接口留给用户自定义;*/ typedef struct node{ Item item; struct node * next; }Node;
2.2 队列:简单使用函数
/*初始化函数;先建一个queue类型的空壳子;*/ void initQueue(Queue *pq ) { pq->front=NULL; pq->rear=NULL; pq->itemsCnt=0; } /*判断队列是否为空;*/ bool queueIsEmpty(const Queue * pq) { return pq->itemsCnt==0; } /*判断队列是否为满;*/ bool queueIsFull(const Queue * pq) { return pq->itemsCnt==MAXCNT; } /*判断队列当前的项数*/ int countQueue(const Queue * pq) { return pq->itemsCnt ; } /*清空队列*/ int emptyQueue(Queue *pq) { while(!queueIsEmpty(pq)) { delQueue(pq);//这是下面小节节点的删除函数; } }
2.3 队列:增加尾节点
正常步骤:新建一个节点,配置节点数据和节点next指针为NULL;
节点地址放入前一个节点中;把节点添加到队列尾端,并且项数加1;
注意事项:如果该节点是首项,front和rear是同一项,要配置front;由于队列的性质决定了加入的节点不是首节点就是尾节点;
/*item为需要增加的节点数据,pq为队列指针*/ int addQueue(Item item , Queue * pq) { Node * pnode; if(queueIsFull(pq)) return 0; pnode=(Node*)malloc(sizeof(Node)); if(pnote==NULL) return 0; else pnode->item=item; /*尾节点的数据*/ pnode->next = NULL; /*尾节点的next指针为NULL*/ if(queueIsEmpty(pq)) pq->front=pnode; /*如果是首项,front和rear是同一项;需要配置front的地址*/ else pq->rear->next=pnode; /*否则是尾项,需要将当前节点的地址放入前一个节点的指针中;*/ pq->rear=pnode; /*设置struct queue的rear*/ pq->itemsCnt++; /*设置struct queue的itemsCnt*/
return 1; }
2.4 队列:删除首节点
步骤:将首节点中存储的第二个节点的地址拿出来先,然后free首节点;重新配置首节点;
/*pq为指向队列的指针; */ int delQueue(Queue *pq) { if(queueIsEmpty(pq)) return 0; Node * pSecNode; pSecNode=pq->front->next; //将首节点中存储的后面一个节点的地址取出先; free(pq->front); //删除首节点 pq->front=pSecNode; //重新配置首节点 pq->itemsCnt--; //项数减1 if(pq->itemsCnt==0) pq->rear=NULL; return 1; }
1&2 小结:队列是一种简单的链表,所以相似的地方比较多;但是队列结构更规范些;
链表的phead,搬运数据的pcurr和ppre同普通变量定义相似,较为零散;而队列将这些指针放在结构体里,定义成了队列;
链表没有对数据的节点个数进行统计,队列结构体对数据的节点个数单独做了统计;
链表的变量是一些指针加一个数据结构体,而队列的变量是两个结构体;队列更加规范,符合逻辑性;
5 节点函数
当插入的list_node前后都有node时,插入list_node的代码顺序应该如图,如果是after则从后往前写代码,如果是before则从前往后写代码,完美;
5.1 第一次插入node
list_head和list_node在初始化的时候,都初始化成了自身地址;如果只有一个list_head,那么在插入第一个list_node的时候节点代码又是如何处理的呢?
以insert_after函数举例,步骤1的l->next->prev = l->prev = n;即list_head->prev = list_rear; list首可寻址尾;
步骤3的n->next = l->next = l; 即list_rear->next = list_head;list尾可寻址首;
这个首尾寻址十分巧妙嘞,既不用多余的代码处理第一次插入,又使得链表可以时间片轮询,一举两得;
#ifndef __RT_SERVICE_H__ #define __RT_SERVICE_H__ //rtservice.h //已知结构体节点,反推结构体首地址; #define rt_container_of(ptr, type, member) ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member))) #define rt_list_entry(node, type, member) rt_container_of(node, type, member) //简单整理了一下上面的宏变成下面这样方便理解;只有最外层和node的括号可以去,其他括号都有用; //#define rt_list_entry(nodeAddr, struct, member) (struct *)( (char *)nodeAddr - (unsigned long)( &((struct *)0)->member ) ) //将list_head初始化成自身,这样第二个node插入之后,会让两个节点首位相连十分地妙; rt_inline void rt_list_init(rt_list_t *l) { l->next = l->prev = l; } //将n_node插入l_node之后,代码顺序如图,如果先修改l节点则存储在l节点中的数据就会丢失; rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n) { l->next->prev = n; n->prev = l; n->next = l->next; l->next = n; } //将n_node插入l_node之前,代码顺序如图; rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n) { l->prev->next = n; n->next = l; n->prev = l->prev; l->prev = n; } //将前后节点连起来后,再初始化n_node为自身地址; rt_inline void rt_list_remove(rt_list_t *n) { n->prev->next = n->next; n->next->prev = n->prev; n->next = n->prev = n; } //空节点的next和prev都被初始化成了自身,判断一个即可; rt_inline int rt_list_isempty(const rt_list_t *l) { return l->next == l; } #endif /* __RT_SERVICE_H__ */
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?