双向循环型多链接链表

来源:互联网 发布:萨尔 知乎 编辑:程序博客网 时间:2024/06/04 20:14
大家都应该知道链表。
定义:双向循环链表
链表就是将一些不连续的空间(里面存储着不同的值)通过自身的指针串联起来。学习链表,最先熟悉的应该是单链表,就像火车一样,从头开始,指针指向下一个,下一个下一个,一直到为NULL为止。然后是循环链表,最终不是NULL而是又重新指向头,这种链表有一个好处就是不再需要这个所谓的“头”。比如一个管理该链表的容器(比如提供插入、删除、查询等功能的容器),有一个指针指向该链表,如果是循环链表,那么这个指针就不一定非要指向“头”,甚至循环链表就根本没有头,指针指向链表的任意位置就可以进行得到链表结构,就好像你拿起一串佛珠时,并没有那个佛珠会告诉你它是头,而是随便从一个佛珠开始就可以开始念经(*^_^*)。接下来就是双向链表。双向链表的每一个对象都有一个指向下一个对象的指针,还有一个指向上一个对象的指针,这样,这辆火车不仅可以从头开始数,也可以从尾部开始数。
使用链表可以实现很多东西,操作系统的内存管理、文件系统管理等都是使用链表实现的,其中文件管理的八叉树也是使用链表的基本操作方法来实现的。链表有一个很重要的特性就是增删方便,但是查询就没有这么方便了。
如果我们想要一个对象方便查询怎么办?用二叉树啊。哦,可以在对象中增加三个指针left, right, parent用来构建二叉树,这很简单。但是,现在有一个对象,它被放到管理容器的链表中,表示该对象存在(如果不存在则从链表中删除),然后另外一个渲染容器也想要这个对象在它的链表中,表示这个对象会在GUI中被画出来(画成什么样不用管),怎么办?复制这个对象然后放到渲染容器中?不,这样太费空间,而且修改时要对两个对象同时修改。最好的办法就是增加prev, next指针让渲染容器来进行管理,可是已经有prev, next被管理容器使用了哦?那么用prev1,next1-----哦不,用prev[2], next[2]哈哈,这样比较好吧?不是,改成prev[2],next[2]后管理容器的代码要全部改成prev[0], next[0],然后给渲染容器使用prev[1], next[1]。。。等等,如果还有其他容器还要用怎么办,prev[3], next[3]?
对于这种问题,有人说使用STL好了?我并不是反对STL,只是觉得我只需要增删查这些功能,而STL提供太多函数(conj等等)反而显得臃肿,并且VC6不能很好支持STL,而且大多程序是使用C在linux上编译,使用template的STL就显得无力了。这样,就只能由我们自己来写管理代码,并且希望每个容器使用同一个增删改代码,这样才能最大限度精简代码。

我们注意到prev[N], next[N],对于一个容器来说,只需要记住这个N就可以管理了,并且只影响本管理容器里的链表,而不影响其他容器(N是其他值),这样就可以提出我们的新数据类型构想方案了:双向循环多链接链表。

基础0:

本文旨在为后面的开发做基础,构想是能够使用全平台兼容的模式,所以不希望太过于依赖类库,并且希望采用C语言来进行演示,但是又需要用面向对象,所以做了一个

用c的struct来进行C++类class的"模拟"(可以参考我的其他文章),这个结构在本文后面一直要用到,请一定先理解这个数据结构基础。
进阶1:多链接
在双向循环多链接链表中,每一个下标对应的prev,next都形成一个双向链表环,叫做多链接。这些链接在对象初始化时设置为NLL,在对象删除时,检查对象的每一个prev,next,如果都为NULL才进行删除。
为了使该数据类型更具兼容性,我们使用在使用C语言的struct来实现C++的class 提到的C语言struct模拟class进行编程,而不使用C++提供的template。
定义链表元素:
typedef struct MultiLinkElement {
#define MultiLinkElementTemplate(T)\
    int linkcount;\
    T ** prev;\
    T ** next;\
    void(*final)(T *that);\
    T * (*free)(T * that);\
    void(*clear)(T * that);
#define TemplateMultiLinkElement(T, E) MultiLinkElementTemplate(struct T)
    TemplateMultiLinkElement(MultiLinkElement, NULL)
}MultiLinkElement;
链表元素类是一个基类,提供最基本的prev,next,以及free操作,prev, next是指向prev[N], next[N]的指针,由其继承类来提供,并指定linkcount大小,这样可以方便确定N的大小。free操作用于删除对象,检测prev, next是否全为NULL,如果是,调用final做一些事后操作。final也由继承类提供。有人问那数据存储在哪儿?那当然是继承类那里了。继承类只需要继承基类,并且重写final写好数据清除操作,其他事就不用管了。
进阶2:多链接管理
定义链表管理
typedef struct MultiLinkBase {
#define MultiLinkBaseTemplate(T, E) \
    int linkcount;\
    int linkindex;\
    E * link;\
    void(*insertLink)(T * that, E * link, E * before, E * after); \
    E * (*removeLink)(T * that, E * link); \
    E * (*get)(T * that, int index); \
    E * (*prev)(T *that, E * link); \
    E * (*next)(T *that, E * link);
#define TemplateMultiLinkBase(T, E) MultiLinkBaseTemplate(struct T, struct E)
    TemplateMultiLinkBase(MultiLinkBase, MultiLinkElement)
}MultiLinkBase;
链表管理也是一个基类,管理链表元素类的增删改查。并且最重要的一点,使用linkindex存储prev[N], next[N]中的N,并使用函数prev, next的调用(内部使用linkindex来指定prev[N], next[N])来获取前后节点。
进阶3:数据池
在一般的操作系统中,获取一个用于构建链表的链表元素,可以使用new来获取永久性内存。但是在嵌入式或者其他驱动中,并没有提供new操作。这里就可以定义一个数据池来管理链表元素。数据池使用数组存储对象,并使用一个mask数组,其每一个位代表数组池中对应的对象有没有被分配使用。具体定义如下:
typedef unsigned char UMAP;
#define MAP_SHIFT    8
#define POOL_MAX    10
#define MAP_MAX    POOL_MAX / MAP_SHIFT + 1
#define MAP_MASK    0xFF
typedef struct ElementPool {
#define ElementPoolTemplate(T, E)\
    E * pool;\
    UMAP * map;\
    int size;\
    int msize;\
    int count;\
    E * (*at)(T * that, int index);\
    E * (*get)(T * that);\
    void(*back)(T * that, E * o);
#define TemplateElementPool(T, E) ElementPoolTemplate(struct T, struct E)
    TemplateElementPool(ElementPool, MultiLinkElement)
}ElementPool;
它也是一个基类,用于提供基本对象池分配、回收。pool和map由继承类提供,以便由继承类制定size和msize来确定其大小。get和back提供分配回收功能。而最重要的一个at函数,需要继承类来提供,因为at使用下标来取得pool中对应位置的对象,如果由基类来提供,那么由于pool的类型是链表元素类,而不是继承自链表元素类的子类,其每一个大小会比实际要小,取得的元素就不是正确的对象,所以需要由子类来提供pool和at(i) { pool[i];}。
进阶4:管理链表
由以上设计不难看出,由链表管理类对链表元素进行增删改查处理,而由对象数据池提供对链表元素的空间分配。一般来说,对于一个完整的管理系统,管理对象继承自链表元素类,管理对象池类继承自对象池类,管理容器则继承自链表管理类,并且包含管理对象池类的对象。如下,比如要做一个取操作系统的任务管理
1. 任务对象
typedef struct OSTask OSTask;
struct OSTask {
    __SUPER(MultiLinkElement, OSTask, NULL);
    OSTask * _prev[2];
    OSTask * _next[2];

    USTACK *OSStack;
};
void OSTask_final(OSTask * that) {
}
OSTask * _OSTask(OSTask * that, int prority) {
    that->prev = that->_prev;
    that->next = that->_next;
    _MultiLinkElement(&that->super, 2);

    that->final = OSTask_final;

    that->OSStack = NULL;

    return that;
}
包含任务元素基类,以及提供给基类用的prev,next,并且要实现final函数。注意提供prev,next时,使用继承对象提供后,_prev, _next就不再被使用, 称作内部对象。
2.任务对象池类
typedef struct OSTaskPool OSTaskPool;
struct OSTaskPool {
    __SUPER(ElementPool, OSTaskPool, OSTask);
};
OSTask * OSTaskPool_at(OSTaskPool * that, int index) {
    return &that->pool[index];
}
void _OSTaskPool(OSTaskPool * that, OSTask * pool, UMAP * map, int size) {
    _ElementPool(&that->super, (MultiLinkElement *)pool, map, size);

    that->at = OSTaskPool_at;
}
纯粹继承自数据池类,并重写了at方法。pool,map由任务容器类来提供。
3.任务容器类
typedef struct OSTaskControl OSTaskControl;
struct OSTaskControl {
    __SUPER(MultiLinkBase, OSTaskControl, OSTask);

    OSTask pool[POOL_MAX];
    UMAP map[MAP_MAX];
    OSTaskPool taskPool;

    OSTask * (*add)(OSTaskControl * that, void(*task)(void *), void *pdata, USTACK *ptos, int prority);
    OSTask * (*remove)(OSTaskControl * that, OSTask * link);
};
OSTaskControl * _OSTaskControl(OSTaskControl * that, int index) {
    int i;
    _MultiLinkBase(&that->super, index);
    for (i = 0; i < POOL_MAX; i++) {
        _OSTask(&that->pool[i], 0);
    }
    _OSTaskPool(&that->taskPool, that->pool, that->map, POOL_MAX);

    that->add = OSTaskControl_add;
    that->remove = OSTaskControl_remove;

    return that;
}
继承自链表管理类,并且包含数据池类对象自己pool, map,在构造函数中初始化pool,并交给任务对象池类,然后执行pool大小,map会在对象池类中进行初始化。其中index用于指定链表元素prev[N], next[N]的N。
4.应用
作为任务容器类的任务管理器,其任务add可以如下进行概述:
从任务池中获取任务链表元素:
ptask = that->taskPool.get(&that->taskPool);
将任务链表元素插入容器链表:
that->insertLink(that, ptask, NULL, NULL);
其任务remove可以如下:
将任务链表元素从容器链表中移除:
that->removeLink(that, ptask);
判断是否应该销毁该任务链表元素:
if (link->free(link) == NULL) {
 // 将任务的堆栈还原(即内存管理回收该分配的内存,本例无关)
memMan.free(&memMan, (MEM_ADDR)link->OSStackTop);
//将任务链表元素归还给任务池
 that->taskPool.back(&that->taskPool, link);
}
这样就完成了一个大概的任务管理流程。

5.结束语
使用数据池双向循环多链表时,可以像这样来使用一个额外的链表:
typedef struct OSTaskLink OSTaskLink;
struct OSTaskLink {
    __SUPER(MultiLinkBase, OSTaskLink, OSTask);
};
void _OSTaskLink(OSTaskLink * that, int index) {
    _MultiLinkBase(&that->super, index);
}
OSTaskLink link;
_OSTaskLink(&link, 1);
由于这里指定1,只要不与管理容器指定的index冲突,就可以随心所欲地控制任务链表,比如进行分组,排序,查询等等,而不会影响管理容器的链表,而且link与任务容器使用的是同一个对象,因此不需要在link销毁时对对象进行清理,对象的清理在任务管理容器中进行。
但是这样来说,如果任务在link使用过程中被指定要销毁,那么由于free函数返回不为空,因此任务实际并没有被消除,而是“挂在”link链表中,如果link销毁而任务对象并没有销毁,则会造成任务池leak,这就需要在操作系统的鲁棒性上花一些心思了。

stophin 2016/11/26
原创粉丝点击