链表
一、概述
什么是链表?链表是一种线性数据结构,即除末尾元素外,每个元素都仅有一个后继元素。对于这种一对一的逻辑关系,数组是通过物理内存地址连续来保持,而链表则通过指针域来保存后继元素的内存地址来保持,因此链表能将散落在内存中的各个元素一一串联起来.
在此,我们用C语言实现一个单链表,其拥有遍历、插入、删除、构造、析构这5种操作.写在LinkList.h,BaseType.h,Iterate.h,LinkList.c,Basic.c这五个文件中,这些文件下面会一一阐述它们的作用.
二、公共定义
定义一些后面会用的公共数据类型与宏,这些东西写在BaseType.h与Basic.c中.
BaseType.h
1 #ifndef BASETYPE_H 2 #define DEBUG 3 #define x86 4 5 /***************基本类型*************/ 6 #define BASETYPE_H 7 #define True 1 8 #define False 0 9 typedef char int8; 10 typedef unsigned char uint8; 11 typedef short int16; 12 typedef unsigned short uint16; 13 typedef long int32; 14 typedef unsigned long uint32; 15 typedef long long int64; 16 typedef unsigned long long uint64; 17 /**********断言**************/ 18 #ifdef DEBUG 19 void _assert(char *info, char *file, uint32 line); 20 #define e_assert(f,s) \ 21 if (f) NULL; \ 22 else \ 23 _assert(s,__FILE__,__LINE__); 24 #else 25 #define e_assert(f,s) NULL 26 #endif 27 /***********指针大小*****************/ 28 #ifdef x86 29 typedef int32 intptr; 30 #endif 31 #ifdef x64 32 typedef int64 intptr; 33 #endif 34 /*****************常用宏**********************/ 35 /********************************* 36 *@summary 交换ab的值 37 *@param a a元素的地址 38 *@param b b元素的地址 39 *@remark 例如:int a=100,b=101;swap(&a,&b); 40 **********************************/ 41 #define swap(a,b) *a=*a^*b;*b=*a^*b;*a=*a^*b 42 #endif
Basic.c
#include "BaseType.h" #include <stdio.h> #include <stdlib.h> void _assert(char *info,char *file,uint32 line) { fflush(stdout); fprintf(stderr, "exception: file:%s line:%d \ninfo:%s", file, line, info); fflush(stderr); abort(); }
主要为基本数值类型起了别名以便于使用,定义了断言宏.
三、接口设计
链表实现后,如何给用户一组直观简洁的接口?我的定义如下:
LinkList.h
/******************************************************** *@summary 单链表 *@author 易水寒 *@date 2015/3/18 * *********************************************************/ #ifndef LINKLIST_H #define LINKLIST_H #include "BaseType.h" #include "Iterate.h" struct TageLinkList { iterate_begin iterate_begin; iterate_end iterate_end; iterate_destory iterate_destory; iterate_before iterate_before; iterate_equal iterate_equal; iterate_read iterate_read; iterate_write iterate_write; }; typedef struct TageLinkList* LinkList; /***************************************************** *@summary 销毁子项的析构函数 *@param item 子项指针 ******************************************************/ typedef void(*link_destory_item)(void *item); /***************************************************** *@summary 创建一个单链表 *@return >0为链表句柄,否则为失败 ******************************************************/ LinkList link_new(link_destory_item freeItem); /***************************************************** *@summary 销毁链表 *@param handle 链表句柄 *@remark 将会释放所有元素内存 ******************************************************/ void link_destory(LinkList handle); /***************************************************** *@summary 在尾部加入一个节点 *@param handle 链表句柄 *@param item 元素指针 *@remark true or false ******************************************************/ int32 link_push(void *item, LinkList handle); /***************************************************** *@summary 删除尾部节点 *@param handle 链表句柄 *@return 元素指针 ******************************************************/ void link_pop(LinkList handle); /***************************************************** *@summary 获取链表元素数量 *@param handle 链表句柄 *@return 元素数量 ******************************************************/ int32 link_count(LinkList handle); /***************************************************** *@summary 将一个元素插入迭代器所指位置之后 *@param handle 链表句柄 *@param iter 迭代器 *@param item 要插入的元素 *@return true or false ******************************************************/ int32 link_insert_after(void *item, Iterate iter, LinkList handle); /***************************************************** *@summary 删除迭代器所指位置的后继元素 *@param handle 链表句柄 *@param iter 迭代器 ******************************************************/ void link_remove_after(Iterate iter, LinkList handle); #endif
方法名加link前缀,表示它们同属一个对象,有遍历、插入、删除、构造、析构。我们希望这组函数能根据不同的链表句柄操作不同的链表,而不是一次只能操作一个链表,如何达到这个效果?其不同点只是内部的状态信息不同,我们只需要将每个链表的内部状态信息用不同的内存分割开来,这部分内存由构造函数来分配,并把块区域的入口地址给用户,由用户来保存,然后用户在调用函数时将其传入。这其实就是面向对象的思维了,将操作与属性结合在一起,只是这种结合在C里没有显示表达出来,而是由程序员隐性关联。定义的数据结构如下:
LinkList.c
typedef struct TageNode { void *data; struct TageNode *next; } Node; typedef struct _TageLinkList { struct TageLinkList func; link_destory_item free_item; /******头节点数据区存储连接元素数量***************/ Node *head; Node *end; } _LinkList;
Node结构描述了节点的信息,data用来存储元素数据,next用来存储后继节点的地址._LinkList描述了整个链表对象的属性,func是迭代器操作,用来实现遍历的,这部分会在下面讲遍历时专门阐述,这里其实可以说_LinkList继承了TageLinkList;free_item是由用户传入的释放元素数据的函数指针;head是头指针,end是尾指针,作用会在将插入的时候阐述.这两个结构用户不需要知道,因此放在实现文件里.
四、构造与析构
构造由构造函数link_new来完成,其作用是分配一块内存用来存储链表的内部状态信息,并且初始化,返回这块内存的入口地址,称为句柄,用来表示一个链表对象.构造函数如下:
LinkList link_new(link_destory_item freeItem) { _LinkList *link; link = (_LinkList*)calloc(1, sizeof(_LinkList)); if (link > 0) { link->free_item = freeItem; /***********注册迭代器操作************/ link->func.iterate_begin = ll_iterate_begin; link->func.iterate_destory = ll_iterate_destory; link->func.iterate_end = ll_iterate_end; link->func.iterate_equal = ll_iterate_equal; link->func.iterate_read = ll_iterate_read; link->func.iterate_write = ll_iterate_write; link->func.iterate_before = ll_iterate_before; link->head = calloc(1, sizeof(Node)); if (link->head == 0) return 0; link->end = link->head; } return (LinkList)link; }
freeItem是释放元素数据的析构函数,因为我们的链表支持所有数据类型,而这些数据只有用户自己知道如何释放,所以要有用户传入。函数里分配一块_LinkList大小的内存,并初始化各个字段.因为_LinkList的开始处与TageLinkList一样,所以可以将其安全转为TageLinkList,这样做是为了封装,也为了用户方便使用遍历操作.
析构函数link_destory用来释放各个元素与链表对象用来存储状态信息的内存.实现如下:
1 #define invalide_handle(h,l) e_assert(h>0,"集合句柄无效");l=(_LinkList*)h 2 #define delete_item(link,item) if(link->free_item>0) link->free_item(item) 3 4 void link_destory(LinkList handle) 5 { 6 _LinkList *link; 7 Node *temp_node, *t2_node; 8 invalide_handle(handle, link); 9 /*******释放所有元素**********/ 10 temp_node = link->head->next; 11 while (temp_node> 0) 12 { 13 t2_node = temp_node; 14 temp_node = temp_node->next; 15 //if (link->free_item > 0)link->free_item(t2_node->data); 16 delete_item(link, t2_node->data); 17 free(t2_node); 18 } 19 free(link); 20 }
因为经常要写验证链表句柄是否为空,将句柄转为_LinkList,所以定义invalide_handle来简化这些代码;在释放元素数据,又要经常写若元素析构函数不为空就调用它是否数据,所以定义delete_item宏来简化.
四、插入
链表元素的插入就好比穿针引线,就是把要插入位的前一个元素的指针域指向新元素就行了,有头插法与尾插法两种。在_LinkList里有一个head(Node*)字段,它的作用是用来指向第一个元素,作为遍历的入口点,称为头指针,是不是非得用它呢,直接用第一个元素做入口不行吗?不是非得用它,用它是为了统一插入操作,而不用做第一个元素的特殊处理,这个特殊处理就是,如果是第一个元素,就把数据存到头节点的数据区里,而不用新分配一个Node,用了头指针后就统一为分配一个Node,把数据写在该Node的数据区,再把该Node加入链表,用代码来表示就是:
1 /************************** 2 *这里采用尾插法,假设item为void *, 3 *表示要插入的元素数据 4 ***************************/ 5 _LinkList *link; 6 Node *new_node; 7 invalide_handle(handle, link); 8 /**********不用头指针*************/ 9 if (link->head->next == 0) 10 link->head->data = item; 11 else 12 { 13 new_node = (Node*)calloc(1, sizeof(Node)); 14 new_node->data = item; 15 link->end->next = new_node; 16 link->end = new_node; 17 } 18 /**********用头指针***************/ 19 new_node = (Node*)calloc(1, sizeof(Node)); 20 new_node->data = item; 21 link->end->next = new_node; 22 link->end = new_node;
头插法好比让新加入的元素用根线来串链表的末尾,即它指向链表末尾,这种串法导致逆序,但同时只需要一个工作指针即可完成(可以就用头指针),该串法代码表示如下:
_LinkList *link; Node *new_node; invalide_handle(handle, link); new_node = (Node*)calloc(1, sizeof(Node)); if (new_node == 0) return; new_node->next=link->head->next; linke->head->next=new_node;
尾插法好比让链表尾部元素用根线去串新加入的元素,即末尾元素指向新元素,这种串法不会逆序,但由于需要保存尾元素信息,所以需要额外一个指针,不可能用头指针,头指针要保存入口点,这也就是为什么_LinkList有head与end.
插入的操作实现link_push与link_insert_after就行了,前者是将元素插入尾部,后者是将元素作为指定位置的元素的后继,为什么是后继而不是该位置,因为这里是单链表,链表新加元素需要知道其前驱,而单链表没法直接通过元素找到其前驱,所以就是插入到后继.代码如下:
int32 link_push(void *item, LinkList handle) { _LinkList *link; Node *new_node; intptr count; invalide_handle(handle, link); new_node = (Node*)calloc(1, sizeof(Node)); if (new_node == 0) return False; new_node->data = item; /*****将新节点加入尾部,并将新节点作为尾指针******/ link->end->next = new_node; link->end = new_node; count = (intptr)link->head->data; count++; link->head->data = count; return True; } int32 link_insert_after(void *item, Iterate iter_handle, LinkList handle) { _LinkList *link; Node *new_node; IterateInfo *iter; intptr count; invalide_handle(handle, link); e_assert(iter_handle > 0, "迭代器句柄无效"); iter = (IterateInfo *)iter_handle; new_node = (Node*)malloc(sizeof(Node)); if (new_node == 0) return False; new_node->data = item; new_node->next = iter->location->next; iter->location->next = new_node; count = iter->handle->head->data; /***********处理迭代器指向尾部时***************/ if (iter->handle->end == iter->location) iter->handle->end = new_node; count++; iter->handle->head->data = count; return True; }
五、遍历
所谓遍历,就是指提供顺序读写集合(容器)每个元素的能力,因为不同容器的遍历都是一样的操作,所以做一步抽象,统一接口。这里,我们抽象出迭代器的概念,一个迭代器标识容器的某个位置,两个迭代器组成一个迭代范围,该迭代范围要在容器的范围内,首迭代器标识容器第一个元素位置,尾迭代器标识容器尾元素的后面,这段迭代范围囊括容器所有元素.迭代器的操作我们定义如下:
Iterate.h
/***************************************************************** *@summary 迭代器,用来遍历容器,每两个迭代器组成一个迭代范围 *@author 易水寒 *@date 2014/3/18 * ******************************************************************/ #ifndef ITERATE_H #define ITERATE_H #include "BaseType.h" typedef intptr Iterate; #define end_behind -1//尾后 /***************************************************** *@summary 获取一个首迭代器 *@param handle 容器句柄 ******************************************************/ typedef Iterate (*iterate_begin)(intptr handle); /***************************************************** *@summary 获取一个尾后(最后一个元素的后面)迭代器 *@param handle 容器句柄 ******************************************************/ typedef Iterate (*iterate_end)(intptr handle); /***************************************************** *@summary 销毁迭代器 *@param iterate 迭代器句柄 ******************************************************/ typedef void (*iterate_destory)(Iterate iter_handle); /***************************************************** *@summary 迭代器前移 *@param iter_handle 迭代器句柄 ******************************************************/ typedef void(*iterate_before)(Iterate iter_handle); /***************************************************** *@summary 迭代器后移 *@param iter_handle 迭代器句柄 ******************************************************/ typedef void(*iterate_back)(Iterate iter_handle); /***************************************************** *@summary 读取迭代器当前位置的元素 *@param iterate 迭代器句柄 ******************************************************/ typedef void *(*iterate_read)(Iterate iterate); /***************************************************** *@summary 将一个元素写入迭代器当前位置 *@param iterate 迭代器句柄 *@remark 会删除旧元素 ******************************************************/ typedef void (*iterate_write)(void *value, Iterate iterate); /***************************************************** *@summary 两个迭代器的位置是否相等 *@param a/b 迭代器句柄 *@remark true or false ******************************************************/ typedef int32(*iterate_equal)(Iterate a, Iterate b); #endif
这套接口由容器实现即可.在链表中,我们采用TageLinkList结构来暴露迭代接口的形式,主要是为了解决统一函数名,解决不同容器命名冲突的问题.链表的实现如下:
LinkList.c
/*********************************迭代器操作**************************************/ typedef struct TagIterateInfo { _LinkList *handle; Node *location; }IterateInfo; Iterate ll_iterate_begin(intptr handle) { _LinkList *link; IterateInfo *iterate = (IterateInfo*)calloc(1, sizeof(IterateInfo)); invalide_handle(handle,link); iterate->handle = link; iterate->location = link->head->next == 0 ? end_behind : link->head->next; return (Iterate)iterate; } static Iterate ll_iterate_end(intptr handle) { _LinkList *link; IterateInfo *iterate = (IterateInfo*)calloc(1, sizeof(IterateInfo)); invalide_handle(handle, link); iterate->handle = link; iterate->location = end_behind; return (Iterate)iterate; } static void ll_iterate_destory(Iterate iter_handle) { e_assert(iter_handle > 0, "迭代器句柄无效"); free(iter_handle); } static void *ll_iterate_read(Iterate iter_handle) { IterateInfo *iter; e_assert(iter_handle > 0, "迭代器句柄无效"); iter = (IterateInfo *)iter_handle; return iter->location==0 ? 0 : iter->location->data; } static void ll_iterate_write(void*value, Iterate iter_handle) { IterateInfo *iter; e_assert(iter_handle > 0, "迭代器句柄无效"); iter = (IterateInfo *)iter_handle; if (iter->location == iter->handle->head) return; //if (iter->handle->free_item > 0 && iter->location->data > 0)iter->handle->free_item(iter->location->data); delete_item(iter->handle, iter->location->next->data); if (iter->location == 0)return; iter->location->data = value; } static void ll_iterate_before(Iterate iter_handle) { IterateInfo *iter; e_assert(iter_handle > 0, "迭代器句柄无效"); iter = iter_handle; if (iter->location==end_behind) return; iter->location = iter->location->next==0? end_behind:iter->location->next; } static int32 ll_iterate_equal(Iterate a, Iterate b) { IterateInfo *iter, *iter2; e_assert(a > 0 && b>0, "迭代器句柄无效"); iter = a; iter2 = b; return iter->location == iter2->location; } /************************************************************************/
在我们的链表中,用户用如下代码即可遍历:
#include <stdio.h> #include "LinkList.h" Iterate begin, end; ll = link_new(0); end = ll->iterate_end(ll); begin = ll->iterate_begin(ll); while (!ll->iterate_equal(begin, end)) { printf("%d\n", ll->iterate_read(begin)); ll->iterate_before(begin); }
六、删除
单链表的删除与插入一样,只能删除指定位置的后继,而不能直接删除该位置,在这里的实现中,用迭代器来表示位置.链表节点的删除,就是将节点的指针指向其后继节点的后继,再释放该节点的后继,核心代码如下:
delete_item(iter->handle, iter->location->next->data);
iter->location->next = iter->location->next->next;
同样,我们定义了两种删除方式link_pop与link_remove_after,前者删除尾部元素,后者删除指定位置节点的后继,代码如下:
1 void link_remove_after(Iterate iter_handle, LinkList handle) 2 { 3 _LinkList *link; 4 IterateInfo *iter; 5 intptr count; 6 invalide_handle(handle, link); 7 e_assert(iter_handle > 0, "迭代器句柄无效"); 8 iter = (IterateInfo *)iter_handle; 9 if ( iter->location==0||iter->location->next==0) return; 10 count = iter->handle->head->data; 11 //if (iter->handle->free_item > 0) iter->handle->free_item(iter->location->next->data); 12 delete_item(iter->handle, iter->location->next->data); 13 iter->location->next = iter->location->next->next; 14 count--; 15 iter->handle->head->data = count; 16 } 17 18 void link_pop(LinkList handle) 19 { 20 _LinkList *link; 21 uint32 k; 22 Iterate begin; 23 invalide_handle(handle, link); 24 if (link->head == link->end) return; 25 k = (uint32)link->head->data-1; 26 begin = link->func.iterate_begin((intptr)handle); 27 while (--k> 0) 28 { 29 link->func.iterate_before(begin); 30 } 31 link_remove_after(begin, handle); 32 }
七、获取元素数量
头指针的数据区域是空的,所以可用来保存元素数量,在有新增与删除,维护该字段即可.