数据结构——双链表(C语言详述通用双链表)
说明:
本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。
QQ 群 号:513683159 【相互学习】
内容来源:
《系统程序员成长计划》
程序源码整体可从:系统程序员成长计划 ——学习篇3:双链表处获取。
目录:
- 概念
- 双链表的结构体
- 结点结构体(C语言实现)
- 头结点结构体
- 返回值的状态(枚举类型)
- 双链表的函数——创建函数
- 双链表结点的创建
- 双链表表头的创建
- 双链表的函数——获取函数
- 获取双链表的长度
- 获取指定结点位置
- 双链表的函数——插入函数
- 头插法函数
- 尾插法函数
- 双链表的函数——结点销毁函数
- 双链表的函数——删除函数
- 双链表的函数——销毁函数
- 双链表的函数——输出函数
- 双链表的函数——主函数
- 双链表的函数——替换函数
- 双链表的函数——获取指定结点的数据函数
- 双链表的函数——遍历函数(回调函数的再次应用)
- 新任务:
- 双链表函数——略
- 双链表函数——查找函数
概念
了解双链表之前应先对单链表有所了解,单链表虽能100%解决逻辑关系为“一对一”数据的问题,但在解决某些特殊问题时,效率并不是最优的,如:算法中需大量找指定结点的前驱结点时,使用单链表无疑到灾难性的,从它的结构体中可知它更适合“从前往后”找,而“从后往前”找并不是它的强项,为此引出了双链表。在单链表的结构体中增加了前驱指针。
双链表的结构体
结点结构体(C语言实现)
typedef struct _DListNode{struct _DListNode* prev; //前驱指针void* data; //数据(空指针类型)struct _DListNode* next; //后继指针
}DListNode;
结构体:
①:属于复杂(或构造)数据类型(自定义),不占内存空间,注意结束分号(;
)不可忘。
②:结构体名:首字母大写,多个单词连写。即: _DListNode
(结构体名可省略【后面就无法定义新变量】))
③:typedef
作用为:取别名。即:typedef struct _DListNode{} DListNode;
后,struct _DListNode
等效于DListNode
④:struct _DListNode*
为结构体数据类型的指针。已知指针用来存储的是地址,指针的字节大小固定,数据类型是用来表示接下来准备跳过多少字节数才能找到上(或下)个的结构体,因为结构体还未声明成功,还没来得及取别名,故不可直接用DListNode
而要用struct _DListNode*
。
⑤:void*
为空指针类型,表示该指针数据的类型未知,但确实指向实实在在的数据,在后续使用过程中需进行强制类型转换。即:表示该双链表的数据可以为任何数据类型。
头结点结构体
typedef struct _DList
{DListNode* first; //头结点仅有一个指针指向下一个结点,无数据
}DList;
双链表的初始化一般分为两种:带头结点与不带头结点。{这边使用的是带头结点的链表,相对不带头结点的链表来说,带头结点不需要对在操作的实现上做一些特殊处理,因为头结点一般不存储数据,且不需要指向前面结点(非循环链表)故结构体与前面有区别}。
返回值的状态(枚举类型)
定义该枚举类型的意义在于对函数调用时,根据该函数的返回值来判断该函数实现情况,根据不同返回值情况进行不同的处理,如:若函数调用失败,由于内存溢出,则可返回:DLIST_RET_OOM
,这样能够增强程序的健壮性。
typedef enum _DListRet
{DLIST_RET_OK, //正常DLIST_RET_OOM, //内存溢出DLIST_RET_STOP, //停止DLIST_RET_PARAMS, //返回参数DLIST_RET_FAIL //失败
}DListRet;
Ret
表示的return
的意思,主要就是用来描述返回值状态,一般返回值都是非常少量的整数。但直接用整数来表示的话显得非常的不直观,故可选择使用enum
即枚举类型来表示。
枚举类型默认第一个标识符为0,后面标识符依次往上递增。也可选择单独对其中某一个标识符赋初值,后面若无再赋值,则在前面的基础上+1。
注意:
①:这些标识符作用范围是全局(严格说是main函数内部),不可再定义相同名的变量。
②:这些标识符均为常量,不可进行赋值操作,只可赋值给其他变量。
③:枚举与宏非常类似,宏在预处理将标识符替换,枚举在编译阶段将标识符替换。
④:枚举类型最后的结束符;
不可忘。
⑤:因为在编译阶段被替换为对应的常量,故不占用数据区的内存,而直接被编译到命令中,即放入代码区,故不可用取址符&
取得地址。
双链表的函数——创建函数
双链表结点的创建
前面已完成对双链表结点结构体的定义,但那只是单纯的定义,并没有产生实际的产物,而想实现双链表的话,自然是需要一些实际的产物的,故需要在内存中实际的划分一个个结点大小的空间用来实际的存储数据和指向前后结点的指针。
/* 双链表结点创建 参数:data 为结点数据*/
static DListNode* dlist_node_create(void* data)
{DListNode* node = (DListNode*)malloc(sizeof(DListNode)); //分配结点空间if(node != NULL) //增加健壮性,若内存分配失败则不执行。{node->prev = NULL; //前驱指针指向为空node->next = NULL; //后继指针指向为空node->data = data; //将函数行参赋值}return node; //返回结点空间的起始地址
}
前提知识:
C的初始动态分配语句(申请:malloc,释放:free,包含在stdlib.h的头文件中):
L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);
====》malloc函数:会申请一整片的存储空间,并会返回该片存储空间的起始地址的指针,故:
①需强制转型为定义的数据元素类型指针,即:(ElemType*)
。
②多大存储空间由参数决定,即:sizeof(ElemType)*InitSize
决定。
结果:
这边InitSize
就一个,故省略,最终结果将会创建出一个结点大小的具体空间,且该空间中的数据由形参传值得到,指针进行对应的初始化,不会有野指针的现象。
对于前面定义的static
关键字,是为了隐藏内部函数,内部函数有哪些缺点可看:系统程序员成长计划 ——学习篇2:封装
双链表表头的创建
DList* dlist_create(void)
{DList* thiz = (DList*)malloc(sizeof(DList)); //创建头结点大小空间if(thiz != NULL) //增加健壮性,若头结点空间分配失败{thiz->first = NULL; //给头结点后继结点指针赋值为NULL}return thiz; //返回头结点地址(也是双链表的表头)
}
对双链表表头的创建,实际上就是对双链表的创建,只不过该双链表后还未接任何结点,是一个空的双链表。
表头的创建过程与结点的创建过程几乎一致,就是表头的结构体会更加简单,且意义存在一定的区别,返回的都是对应结点的地址。但大家可能会对结点指针变量名thiz
存在一定的疑问,可看:系统程序员成长计划 ——学习篇1:代码风格中,面向对象的命名方式,其中的thiz
表示的对象的意思。网易对该单词的翻译是即时的意思。
DListNode*
表示结点,DList*
表示双链表。
双链表的函数——获取函数
获取双链表的长度
/* 获取双链表长度 参数:thiz 为对象,即双链表表头指针(该双链表的长度)*/
size_t dlist_length(DList* thiz)
{size_t length = 0; //定义length变量并初始值为0DListNode* iter = thiz->first; //将头指针地址传给iter指针(双链表结点指针类型)while(iter != NULL) //若指向不为空(下面还有结点),则{ length++; //length自增iter = iter->next; //继续指向下一个结点}return length; //返回双链表长度
}
计算双链表的长度是比较简单的,一看代码应该就懂。首先定义了长度的变量和结点(指向双链表表头)后,通过不停的遍历(能否成功移到下一结点处,成功移到则长度+1),最终返回想要的双链表表头。
这边的循环判断条件,只需要当前结点不为空即可,这样就可把双链表结点全部计算进去。
变量名:iter
应该为:iteration
,即迭代、重复的意思。
获取指定结点位置
/* 获取指定结点的地址 参数:thiz 为对象,双链表表头指针,index 为想要读取的双链表中的第几个结点, fail_return_last 为是否需要判断指定结点有没有超过链表长度,0表示需要,1表示不需要
*/
static DListNode* dlist_get_node(DList* thiz, size_t index, int fail_return_last)
{DListNode* iter = thiz->first; //将头结点指针地址给iter指针while(iter != NULL && iter->next != NULL && index > 0) //若当前结点不为空(存在),下一个结点也不为空(存在),链表下标(大于0){iter = iter->next; //指向下一个结点index--; //下标自减}if(!fail_return_last) //若为fail_return_last为0则执行{iter = index > 0 ? NULL : iter; //若下标大于0则iter指针指向NULL,表示没找到该结点,小于0则iter指针指向正常,即找到指定结点} return iter; //返回对应结点的地址
}
其实总体与获取长度的思路差不多,也是不断的遍历,不过这边的返回值为地址。且最重要的这边的判断条件比获取长度的条件会更加严格,需要将可能的情况都包含进去。
条件1:iter != NULL
,想要第几个结点的位置,这个结点至少得存在,才能继续遍历。
条件2:iter->next != NULL
想要第几个结点的位置,必须下一个结点也存在才能继续遍历。
条件3: index > 0
,想要第几个结点的位置,第几个一定是要大于0的,才能继续遍历。
若不需查找指定结点,即:index=0
(指向链表表头)或index=-1
(-1表示极大,不断遍历链表,指向链表表尾),即不存在index长过表长情形,故无需判断是否越界,fail_return_last =1
即可。
若需查找指定结点,即:0<index<dlist_length
,则需要判断是否超过表长,即fail_return_last =0
,遍历后在判断是否越界。
注:双链表的下标起始是从0开始的,故:若想获取下标为5的结点,实际上获取的是第6个结点的位置。(循环起始为5,减到0停止,循环6次)
双链表的函数——插入函数
插入函数是双链表中最难理解的地方,对链表进行补充添加的作用。
/* 双链表插入 参数:thiz 为双链表表头指针 ,index 为想要插入双链表的下标位置, data 为想要插入的数据*/
DListRet dlist_insert(DList* thiz, size_t index, void* data)
{DListNode* node = NULL; //双链表结点指针指向NULLDListNode* cursor = NULL; //双链表节点指针指向NULL(cursor表示游标)if((node = dlist_node_create(data)) == NULL) //若双链表结点创建失败{return DLIST_RET_OOM; //返回内存不足溢出错误}if(thiz->first == NULL) //若头结点后继指针为NULL(双链表表头为空,说明后面没有结点则直接将结点给表头的后继结点即可){thiz->first = node; //将node结点地址赋值给头结点后继指针(这边的node结点地址为上一个条件判断的中创建的结点地址[若分两句应该会清楚很多])return DLIST_RET_OK; //返回双链表创建成功}cursor = dlist_get_node(thiz, index, 1); //获取想要插入结点的地址,此时游标地址就为想要插入的地址if(index < dlist_length(thiz)) //若想要插入的结点的位置在双链表的长度里面,则{if(thiz->first == cursor) //判断双链表表头指针与游标指针相等不相等(头指针需要的特殊处理){ thiz->first = node; //相等,则将node结点地址赋值给头结点的后继指针}else //不相等{ cursor->prev->next = node; //此时游标即为要插入的结点位置,故需要先找到前面结点,再把创建的结点连接在前面结点的后继指针上node->prev = cursor->prev; //再给新插入的结点的前驱指针赋值,指向前面这个结点} //以上完成了结点插入的操作,但是新插入的结点后继指针还未指向。(还有后面结点的连接)node->next = cursor; //这边就是将新插入的结点的后继指针指向指向原先这个结点(现在便为后面结点)的位置 cursor->prev = node; //将后面结点的前继指针指向新结点}else //若想要插入结点位置超出双链表的长度了{ cursor->next = node; //因为能够获取得到对应的结点,故实际上是新结点插在双链表的最后一个结点位置node->prev = cursor; //即进行最后结点的前驱和后继指针的插入。}return DLIST_RET_OK; //返回成功
}
这边要实现的通用链表,根据对形参index
的不同传值和两层条件判断语句,可实现:
① 头插法:index = 0
,即:dlist_prepend()
函数
1️⃣cursor = dlist_get_node(thiz, 0, 1);
永远指向链表表头(头结点)。
2️⃣满足条件index < dlist_length(thiz)
,满足条件thiz->first == cursor
,即:
thiz->first = node;
node->next = cursor;
cursor->prev = node;
3️⃣return DLIST_RET_OK;
② 指定插法(中插法)index = n,0<n<dlist_length
,
1️⃣cursor = dlist_get_node(thiz, index, 1);
指向要插入的位置。
2️⃣满足条件index < dlist_length(thiz)
,不满足条件thiz->first == cursor
,即:
cursor->prev->next = node;
node->prev = cursor->prev
node->next = cursor
cursor->prev = node
3️⃣return DLIST_RET_OK;
③ 尾插法:index = -1
(注:-1
表示极大,>0
),即:dlist_append()
函数
1️⃣cursor = dlist_get_node(thiz,-1, 1);
永远指向链表表尾(尾结点)。
2️⃣不满足条件index < dlist_length(thiz)
,
cursor->next = node;
node->prev = cursor;
3️⃣return DLIST_RET_OK;
图解三种插入方法,请看下图:
头插法函数
/* 双链表的追加 ,头插法*/
DListRet dlist_prepend(DList* thiz, void* data)
{return dlist_insert(thiz, 0, data);
}
尾插法函数
/* 双链表的附加 ,尾插法*/
DListRet dlist_append(DList* thiz, void* data)
{return dlist_insert(thiz, -1, data);
}
双链表的函数——结点销毁函数
结点销毁,若想要删除某个结点,虽然可通过指针的指向跳过要删除结点,最终形成新的双链表,但删除的结点若没有将其释放掉,它仍会占用内存空间,故删除结点后,需要释放对应结点的空间,故常与删除函数配合使用。
/* 销毁双链表结点 参数:node 为双链表的结点*/
static void dlist_node_destroy(DListNode* node)
{if(node != NULL) //若双链表结点不为空,则{node->next = NULL; //将前驱指针指向空(注意,一定要赋值为空,否则指针还是有指向,虽然下面会将对应空间释放)node->prev = NULL; //将后继指针指向空(注意,一定要赋值为空,否则指针还是有指向,虽然下面会将对应空间释放)free(node); //释放该结点空间}return;
}
双链表的函数——删除函数
删除函数,即删除指定结点,形成新的双链表。
根据要删除结点是否存在前驱结点和后继结点来进行指向,可能存在三种情形,还有特殊情形:指向头结点:
情形一:无前驱结点,有后继结点。
情形二:无后继结点,有前驱结点。
情形三:前驱与后继结点均有。
特殊情形:指向头结点。
/* 双链表的删除 参数: thiz为对象,即该双链表 ,index 为想要删除的双链表下标位置 */
DListRet dlist_delete(DList* thiz, size_t index)
{DListNode* cursor = dlist_get_node(thiz, index, 0); //cursor指向指定结点的地址if(cursor != NULL) //该结点存在{if(cursor == thiz->first) //若指向链表表头{thiz->first = cursor->next; //则表头指针指向下一结点}if(cursor->next != NULL) //删除结点后还存在结点(非链表表尾结点){cursor->next->prev = cursor->prev; //将下一结点前驱指针指向删除结点的前一结点}if(cursor->prev != NULL) //删除结点前还存在结点(非链表表头){cursor->prev->next = cursor->next; //前一结点后继指针指向删除结点的后一个结点}dlist_node_destroy(cursor); //回收要删除结点的空间}return DLIST_RET_OK;
}
通过图片能更好的感受不同情形具体实现。下图是根据头插法绘制,尾插法类似。
双链表的函数——销毁函数
有创建对象就要有销毁对象。
void dlist_destroy(DList* thiz)
{DListNode* iter = thiz->first; //指向首元结点,第一个结点(非头结点)DListNode* next = NULL; //定义双链表结构体指针while(iter != NULL) //只要首元结点不为空,则不停遍历,销毁每次的首元结点{next = iter->next; //将首元结点指向的下一个结点给next指针dlist_node_destroy(iter); //销毁该结点(第一个结点)iter = next; //原先的第二个结点变为第一个结点,}thiz->first = NULL; //令头结点指向空,即为空链表free(thiz); //释放空链表return;
}
双链表的函数——输出函数
该函数为1.4拥抱变化中最重要的部分。
编写完程序后为验证程序能否正常工作,就需要一条输出函数将数据直观显示到屏幕上,作为通用链表,存储的数据data
的数据类型具有多样性,那对应的输出函数同样也要具有多样性才可以,作者列举很多种方法并介绍了新方法:回调函数法。
根据使用者来编辑对应的回调函数,实现双链表的输出功能。那调用者如何提供函数给dlist_print()
呢?
通过函数指针,即指针指向一段代码(函数),指针指向不同函数便有不同的行为。
先声明:
typedef DListRet (*DListDataPrintFunc)(void* data); //声明函数指针
函数指针的理解:
前面已经知道typedef
关键字的作用:取别名。一般格式为:typedef oldName newName;
故咋一看似乎并不理解上面代码的意思,那可先看对数组类型定义别名的例子:
typedef char ARRAY20[20];
表示 ARRAY20 是类型char [20]
的别名。它是一个长度为 20 的数组类型。(要注意这边数组的长度是固定的哦!),再看为指针类型定义别名的例子:
typedef int (*PTR_TO_ARR)[4];
表示 PTR_TO_ARR 是类型int * [4]
的别名,它是一个二维数组指针类型。
通过以上的观察,可发现,新出现的单词即为函数指针的变量名:DListDataPrintFunc
。
typedef DListRet (*DListDataPrintFunc)(void* data);
可看成typedef DListRet (*)(void* data) DListDataPrintFunc;
这就比较清晰了。
(*)
为指针,后接(void *data)
:函数的形参,(*)(void *data)
即为函数指针,且该函数的数据类型为DListRet
。【要注意,(*)
的括号不能少,否则意义就变了】
故:函数指针作为形参时,就看作是一个指针,不过该指针是用来指向函数的地址,而函数的地址无需用取值符“&”
,只需函数名即可,与数组等类似。
函数指针的作用:`
实现回调函数,
通过函数指针调用函数。将函数的指针作为参数传递给一个函数,使得在处理相似的事件的时候可以灵活的使用不同的方法
调用者不关心谁是调用者。即需要知道存在一个具有特定原型和限制条件的被调用函数。
延伸:typedef
在表现上有时候类似于 #define
,但是存在关键性的区别。typedef
是一种更彻底的“封装”类型,声明后不可再往里增加别的东西。
①typedef
不可再扩展,define
可在扩展。
#define INTERGE int; unsigned INTERGE n;//没问题
typedef int INTERGE; unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned
②在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。
详细可看:C语言中文网-C语言typedef的用法详解
/*双链表输出函数 参数:thiz为对象,即该双链表,print 为函数指针*/
DListRet dlist_print(DList* thiz, DListDataPrintFunc print)
{DListRet ret = DLIST_RET_OK;DListNode* iter = thiz->first; //结构体指针指向该链表的表头while(iter != NULL) //不断的向后遍历{print(iter->data); //输出数据,这边的print即为函数指针iter = iter->next;}return ret;
}
以上函数即为输出函数,其中真正实现输出到屏幕的函数为形参函数指针传入的函数print
,该函数在主函数的文件中由调用者编写,可将数据data
强制类型转换为想要的数据类型。
双链表的输出顺序:从双链表表头开始,不停向下遍历。
双链表的函数——主函数
调用者编写对应数据类型的回调函数后,main()
函数中进行输出。如下所示:
static DListRet print_int(void* data)
{printf("%d ", (int)data);return DLIST_RET_OK;
}
int main(int argc, char* argv[])
{int i = 0; int n = 100; DList* dlist = dlist_create(); //创建头结点(只有一个指针大小空间,无数据)for(i = 0; i < n; i++){assert(dlist_preppend(dlist, (void*)i) == DLIST_RET_OK); //assert宏原型:void assert(int expression); 作用:若条件返回错误,则终止程序 //append追加函数:调用insert(thiz, -1, data); 参数-1为下标,这边表示头结点的下表,之后根据插入函数插入不断赋值}for(i = 0; i < n; i++){assert(dlist_prepend(dlist, (void*)i) == DLIST_RET_OK);}dlist_print(dlist, print_int); //输出函数dlist_destroy(dlist); //销毁链表return 0;
}
以下图解为根据头插法和尾插法进行输出详细步骤图解,这边用了三次循环,看起来较为冗长,实际两次循环差不多就可看出结果了。
双链表的函数——替换函数
/* 替换函数参数:thiz为对象,即该双链表, index为指定位置的下标(从0开始)data 为想要替换的数据功能:在指定链表中,找到对应下标的位置,将该位置数据替换为data
*/
DListRet dlist_set_by_index(DList* thiz, size_t index, void* data)
{DListNode* cursor = dlist_get_node(thiz, index, 0); //让cursor指针指向指定下标位置if(cursor != NULL){cursor->data = data; //将data数值赋给指定的结点的数据域}return cursor != NULL ? DLIST_RET_OK : DLIST_RET_FAIL; //该结点指向不为空(有得到指定结点位置)则成功,否则则失败
}
双链表的函数——获取指定结点的数据函数
/* 获取指定结点的数据函数参数:thiz为对象,即该双链表, index为指定位置的下标(从0开始)data 返回的对应指针的地址功能:在指定链表中,找到对应下标的位置,将该位置数据传给形参data指针的指针实际上得到的是结点数据域的地址,若结点存储的是复杂数据类型,也可以读取对应的数据
*/
DListRet dlist_get_by_index(DList* thiz, size_t index, void** data)
{/* **data表示指针的指针,传入的为指针的地址data 等价于 指针的地址 (指针的指针)*data 等价于 指针**data 等价于 指针指向的值*/DListNode* cursor = dlist_get_node(thiz, index, 0);if(cursor != NULL){*data = cursor->data; //将对应结点数据地址赋值给指针}return cursor != NULL ? DLIST_RET_OK : DLIST_RET_FAIL;
}
若形参中含有:指针变量*data
,故实参需要传入的是变量的地址。
若形参中含有:指针的指针变量**data
,故实参需要传入的是指针的地址。
此时的data
表示的就是传入的指针的地址。
而*data
表示的就是指针。
而 **data
表示的就是指针指向的变量
双链表的函数——遍历函数(回调函数的再次应用)
通过回调函数的定义,执行不同功能(不同功能有重复代码【逻辑有相同之处】)。
这边以双链表中实现的以下三种功能为例。
功能一:对一个存放整数的双向链表,依次输出链表所有值。
功能二:对一个存放整数的双向链表,累加链表中所有整数。
功能三:对一个存放整数的双向链表,找出链表中的最大值。
可以分别通过三个功能函数来实现以上的三种功能。编写完成后,会发现,这三个功能函数实际上有一个共通之处,那便是:都要实现对双链表的遍历。
即采用三个函数来实现会造成代码的重复,而重复的代码会造成很多问题。如:
问题一:重复的代码更容易出错。
问题二:重复的代码经不起变化。(在修正bug或增加新特性时,需要修改重复代码,若忘记一处,则要付出很大的代价)
为此可采用回调函数法,让调用者根据需求编写以上的三种功能,减少代码的重复。但在实现的过程中,会发现每个回调函数都要保存一些中间变量,为此你可能想到的办法便是通过全局变量来保存,但却违背了禁用全局变量的原则。为此,有什么办法能消除全局变量么?
有的,给回调函数传递额外的参数即可,该参数称为回调函数的上下文,用变量名ctx
(context的缩写),而为保证可保存任何数据类型,同样可选择void*
来表示该上下文。
那么,让我们来看看如何实现以上的功能。
typedef DListRet (*DListDataVisitFunc)(void* ctx, void* data); //给函数指针取别名,DListDataVisitFunc为函数指针名
/* 回调函数,实现功能一,依次输出链表所有值 */
static DListRet print_int(void* ctx, void* data)
{printf("%d ", (int)data);return DLIST_RET_OK;
}/* 回调函数,实现功能二,累加链表所有整数 */
static DListRet sum_cb(void* ctx, void* data)
{long long* result = ctx; //定义long long 类型指针 result *result += (int)data; //将使用解指针运算符,得到具体数值,进行累加return DLIST_RET_OK; //返回成功
}
/* 定义结构体 MaxCtx 两个整数类型变量*/
typedef struct _MaxCtx
{int is_first;int max;
}MaxCtx;
/* 回调函数,实现功能三,找出链表中的最大值 */
static DListRet max_cb(void* ctx, void* data)
{MaxCtx* max_ctx = ctx; //定义结构体指针if(max_ctx->is_first) //若结构体中变量is_first为数值非零则执行{max_ctx->is_first = 0; //令该变量为为0max_ctx->max = (int)data; //结构体变量max为data}else if(max_ctx->max < (int)data) //若结构体变量max<data{max_ctx->max = (int)data; //令结构体变量max=data}return DLIST_RET_OK;
}
/* 遍历函数 参数:thiz 为对象,即该双链表 visit为函数指针ctx(context) 为回调函数的上下文,给回调函数传递额外参数(避免使用全局变量)
按面向对象函数命名,应将上下文(ctx)放第一参数,而thiz也为函数上下文,这边与ctx相同,故无需改变位置
*/
DListRet dlist_foreach(DList* thiz, DListDataVisitFunc visit, void* ctx)
{DListRet ret = DLIST_RET_OK; //返回值状态DListNode* iter = thiz->first; //迭代指针指向双链表表头while(iter != NULL && ret != DLIST_RET_STOP) //迭代指针不为空,返回值状态不为停止时,不断往下遍历{ret = visit(ctx, iter->data); //调用回调函数iter = iter->next; //执行下个结点}return ret;
}
以上为函数定义,结合main()
函数实际代入数值整理一下思路。
int main(int argc, char* argv[])
{int i = 0;int n = 100;long long sum = 0;MaxCtx max_ctx = {.is_first = 1, 0}; //定义结构体(Max_ctx)名为max_ctx的,并其两个变量分别赋值DList* dlist = dlist_create(); //创建双链表dlistfor(i = 0; i < n; i++){assert(dlist_append(dlist, (void*)i) == DLIST_RET_OK); //以尾插法建立结点数100的链表,且数据从0递增}dlist_foreach(dlist, print_int, NULL); //遍历将双链表数值打印到屏幕 dlist_foreach(dlist, max_cb, &max_ctx); //遍历得到双链表最大值,并得到最大值max_ctxdlist_foreach(dlist, sum_cb, &sum); //遍历累加双链表所有制,并得到累加值sumprintf("\nsum=%lld max=%d\n", sum, max_ctx.max); //输出最大值与dlist_destroy(dlist);return 0;
}
以下为求和累加的回调函数的图解。
依据程序和图解,我们能很清楚的发现,若要使用回调函数中有需要保存中间数据处,即这边的数值累加函数,较为简单的方法就是使用全局变量sum,后在回调函数中对数值进行累加的操作。
但在写工程时应该避免使用全局变量,而采用的方法便是在使用回调函数的函数中添加一个指针变量,通过指针指向的地址的方式,实现数值的累加,这样就成功避免了使用全局变量。
故:一般情况下,要使用回调函数,在回调函数作为形参的函数处都要在定义一个指针变量,方便进行数值的传递,若没有用到,则可定义,留着之后的扩展,传递时只需将实参赋值为NULL即可。
新任务:
对一个存放字符串的双向链表,把存放在其中的字符串转换成大写字母。
常见错误:想要完成小写转换大写的操作,很容易想到它们在ASCII码表中的相对位置,只需在原来的基础上+('A'-'a')
或-('a'-'A')
,但在不同语言中并不一定存在这种关系,故不能简单的认为97(a)~122(z)之间的字符就是小写字符,而应该用islower
判断,同样转换大写应调用toupper
。而不能采取减去常量的方法。
扩展:
islower()
函数:
函数功能:检查所传字符是否是小写字母。
函数声明: int islower(int c);
参数:
c —— 要检查的字符
返回值 —— 若为小写字母返回非零值(true),否则返回0(false)
toupper()
函数:
函数功能:要被转换为大写的字母.
函数声明:int toupper(int c);
参数:
c —— 要检查的字符
返回值 —— 如果 c 有相对应的大写字母,则该函数返回 c 的大写字母,否则 c 保持不变。返回值是一个可被隐式转换为 char 类型的 int 值。
双链表函数——略
typedef void (*DListDataDestroyFunc)(void* ctx, void* data); //函数指针名:DListDataDestroyFunc/* 双链表 头结点结构体 */
struct _DList
{DListNode* first; //头结点指针DListDataDestroyFunc data_destroy; //函数指针void* data_destroy_ctx; //空指针类型
};
/* 双链表销毁*/
static void dlist_destroy_data(DList* thiz, void* data)
{if(thiz->data_destroy != NULL) //只要表头该数据结点不为空{thiz->data_destroy(thiz->data_destroy_ctx, data); //调用表头函数指针,}return;
}static void dlist_destroy_node(DList* thiz, DListNode* node)
{if(node != NULL){node->next = NULL;node->prev = NULL;dlist_destroy_data(thiz, node->data);free(node);}return;
}DList* dlist_create(DListDataDestroyFunc data_destroy, void* data_destroy_ctx)
{DList* thiz = malloc(sizeof(DList));if(thiz != NULL){thiz->first = NULL;thiz->data_destroy = data_destroy;thiz->data_destroy_ctx = data_destroy_ctx;}return thiz;
}
void dlist_destroy(DList* thiz)
{DListNode* iter = thiz->first;DListNode* next = NULL;while(iter != NULL){next = iter->next;dlist_destroy_node(thiz, iter);iter = next;}thiz->first = NULL;free(thiz);return;
}
双链表函数——查找函数
typedef int (*DListDataCompareFunc)(void* ctx, void* data); //函数指针
/* 查找函数:从表头开始遍历 ,寻找ctx相同的数值,返回该结点位置参数:thiz 当前对象,该双链表 , cmp 函数指针,指向实现比较功能的函数ctx 要查找的数值,传入函数指针
*/
int dlist_find(DList* thiz, DListDataCompareFunc cmp, void* ctx)
{int i = 0;DListNode* iter = thiz->first; //表头指针while(iter != NULL) //只要双链表还有下一结点{if(cmp(ctx, iter->data) == 0) //调用函数指针,比较两个值是否相等{break; }i++;iter = iter->next; //指向下一个结点}return i;
}
数据结构——双链表(C语言详述通用双链表)相关推荐
- 链表c++语言 解析,C++ 单链表的基本操作(详解)
链表一直是面试的高频题,今天先总结一下单链表的使用,下节再总结双向链表的.本文主要有单链表的创建.插入.删除节点等. 1.概念 单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数 ...
- 【数据结构入门实验C语言版】城市链表
实验内容描述: 2 .城市链表(设计性实验) 问题描述 将若干城市的信息存入一个带头结点的单向链表.结点中的城市信息包括城市名.城市的位置坐 标.要求能够利用城市名和位置坐标进行有关查找.插入.删除. ...
- c语言单链表_C语言笔试题—单链表逆序
前情回顾 之前更多的是给大家推荐的是好用的软件,经过反思之后觉得这些东西并不是我想要的,所以从今天开始我要转变方向了,更多的往我的专业方向去发展(虽然我是个小白),当然如果有说的不对的地方,希望大家能 ...
- 职工系统c语言链表,C语言职工信息管理系统(链表)..doc
<程序设计综合训练> 设 计 报 告 专 业: 数字媒体技术 班 级: 11媒体Z 学 号: 姓 名: 朱毅 指导教师: 陈湘军 陈明霞 成 绩: 计算机工程学院 2012年10月 第一部 ...
- C语言数据结构篇——双链表的创建,插入,节点删除,打印等操作
作者名:Demo不是emo 主页面链接:主页传送门 创作初心:对于计算机的学习者来说,初期的学习无疑是最迷茫和难以坚持的,中后期主要是经验和能力的提高,我也刚接触计算机1年,也在不断的探索,在CSD ...
- 数据结构一线性表 (顺序表、单链表、双链表)
版权声明:本文为openXu原创文章[openXu的博客],未经博主允许不得以任何形式转载 文章目录 1.线性表及其逻辑结构 1.1 线性表的定义 1.2 线性表的抽象数据类型描述 2.线性表的顺序存 ...
- 《恋上数据结构第1季》队列、双端队列、循环队列、循环双端队列
队列(Queue) 队列 Queue 队列的接口设计 队列源码 双端队列 Deque 双端队列接口设计 双端队列源码 循环队列 Circle Queue 循环队列实现 索引映射封装 循环队列 – %运 ...
- 【数据结构】链表 - Go 语言实现
文章目录 一.简介 二.最简单的链表 三.循环链表 1. 初始化循环链表 2. 创建一个指定大小 N 的循环链表,值全为空 3. 获取上一个或下一个节点 4. 获取第 n 个节点 5. 获取链表长度 ...
- LeetCode刷题---707. 设计链表(双向链表-带头尾双结点)
文章目录 一.编程题:707. 设计链表(双向链表-带头尾双结点) 1.题目描述 2.示例1: 3.提示: 二.解题思路 1.思路 2.复杂度分析: 3.算法图解(双向链表) 三.代码实现 三.单向链 ...
最新文章
- Tomcat 7.x热部署
- 各样本观察值均加同一常数_对色师傅分享:如何使不同观察者在灯箱下观察的色光一致?...
- 【PAT (Advanced Level) Practice】1054 The Dominant Color (20 分)
- python2执行程序内存溢出导致被killed的问题因果分析
- spring导入约束
- 如何在 ASP.Net Core 中使用 LoggerMessage
- Immutable Collections(3)Immutable List实现原理(中)变化中的不变
- 在java中的交换方法有哪些_java中交换两个变量的值有哪几种方法,交换两个变量a和b的值...
- 腾讯云联合信通院发布《超低延时直播白皮书》,推动直播延时降低90%以上
- 解析Pinterest:兴趣乐园背后的大文章
- bzoj 3360: [Usaco2004 Jan]算二十四(暴力+表达式求值)
- 关于C#使用DataContractJsonSerializer来进行JSON解析
- 用HTML5为你的网页添加音效(兼容Firefox 3.5+, IE 6-9, Safari 3.0+, Chrome 3.0+, Opera 10.5+)
- android 重复文件夹,清理手机空间小工具!搜索重复文件App
- 检测本地连接并自动连接宽带连接.cmd
- 拉卡拉考拉超收,关于它的全部信息!
- 尚鼎峰:抖音短视频是如何在几秒钟内吸引用户观看的?
- Linux/UNIX命令dd简介
- 历年茅台计算机招聘考试真题,2020贵州茅台招聘考试试题及答案(7)
- 最新北风网人工智能(完整版)
热门文章
- 百度知道推广敏感词汇总
- 4米乘以12米CAD图_简单四步,教你如何绘制好施工现场总平面布置图
- Nignx优化与防盗链
- 软件过程改进人才队伍建设的重要步骤
- c语言考试 程序填空题,计算机二级C语言程序填空题练习题
- 麻将胡牌算法lua 不支持癞子
- linux 修改终端字体,linux系统终端修改字体的方法
- html游戏 养狗,七个“养狗神器”让你舒服养狗,建议收藏
- python读取raw数据文件_Python rawkit如何从RAW文件读取元数据值?
- Microsoft.Office.Core 引用以及 Microsoft.Office.Core.MsoTriState 的问题