先看下传统的对图的定义,通常在GRAPH这层,逃不掉两个东西。
typedef struct{
VECTEX v;
EDGE e;
....
}GRAPH_;
就是说,这是个边集合,和顶点集合的整体。这里我们不要纠结GRAPH是否一定要包含两个不同类型的集合。但有点需要思考的,结构体是否是有序配对。再抽象点说,就是集合内部是否有序。
显然单纯的集合是没有序的。而G(V,E)不是单纯的集合,V和E实际上是有关联的。此时,这样定义,必然你会存在一个V,E关联操作的函数,用于定期扫描或对成批的单独V(或E)操作后进行扫描,V,E的关联正确性。直接说吧,你要保证E中所有的边,所指向的元素均在V里。
如果将V,E并列作为GRAPH的子集合,则上述扫描动作,你很难决策,究竟什么样的GRAPH的操作,需要处理,什么不需要。因为有些操作不是单纯的V(或E)的处理。
例如,将一个图分割成两个子图,是的任意一个子图中的任意一个顶点,都存在一个边和另一个子图相连。
甚至会混淆删除,新增,改动(查不存在)的不同含义。例如,删除一个边,可以独立处理。删除一个顶点,则不行,必须要将对应边在E集合内全部删除后,这个顶点才能删除。。。。
希望大家不要认为这个是逻辑游戏,绕来绕去。如果你做过复杂结构的图的算法开发,起初对图的定义和基本操作越清晰,那么你复杂的操作中,就越少发生逻辑性矛盾。这个很数学证明一样。如果你的每步陈述都是针对任意情况成立的,你的数学证明内容就不会复杂到连自己都看不懂。写代码和数学证明有很多相似的地方(当然APP级的我就不讨论了,因为把很多问题压给了下层处理)
那么我的做法,是
typedef struct{
EDGE e;
....
}GRAPH_;
...部分是其他辅助信息,和图本身没有关系。顶点则是边集合中的一个索引号。就是将那些 边的两个端点相同的边,所做的特殊标记处理。此处不考虑实际问题中,存在一个顶点自己存在一个边的情况,这个和有向图一样,属于基本图论定义上的另一个强约束。只要是定义上的,那么就可以由原有定义的C语言描述中,增添出来,而不用修改原有代码。
正是因为顶点,(采用上述做法),看作是特殊边的一个索引,所以不存在对顶点的增删的问题。
当然对于外部使用者而言,确实有顶点的增删,但是增,删,都是当作一个边的操作附带有内部代码自己实现的。
因此,图,使用C语言描述的基本内容如下
typedef struct {
int b;//big point
int s;//small point
}__ELEM;
typedef __ELEM * _P_ELEM;
此表示图中,最基本的二元配对就是(x,y)。此处b,s还不能称为顶点。只能说是端点。为了区分x,y有个大有个小以方便排序,因此定义了b,s。
b,s里的内容,并不是用户定义的。而是根据用户输入的边的信息,进行抽象出来,由系统确定的。例如用户输入如下信息
(1,10),(8,2),(2,10),(8,1)
则系统应该保存如下信息
(1,2),(3,4),(4,2),(3,1)。
这样的关联,和用户输入的关联并没有差异,只是1,2,3,4,5,6等等符号不同。
__ELEM的操作,有三个。分别如下
#define __SET_ELEM(pe,b,s) do{(pe)->b = b; (pe)->s = s;}while(0)
#define __CHECK_ELEM(pe) ((pe)->b >= (pe)->s)
#define __CHECK_NULL(pe) ((pe)->b == (pe)->s)
其中,之所以将
((pe)->b == (pe)->s)
定义为空。是因为,二元配对,从边的角度而言,有意义的是 (x,y) x != y,这里保留(x=x)是表示孤立顶点。从边的角度来看,属于一个空关联。
而__CHECK_ELEM,只是作为图的每个二元配对的刷新检测,一般应该用在,当整个图从外部载入,无论是网络还是磁盘文件时,做一次性的验证,防止一些默认的逻辑产生错误使用。图论的算法其他位置,应不考虑该操作。
__SET_ELEM,是为了简化二元配对的赋值。其实,对于二元配对确实存在比较等操作,但那属于二元配对作为一个整体,也即 两个 __ELEM之间的操作。
这里需要注意,所有的对结构体的操作,我都处理成指针,毕竟结点元素作为独立的一个变量访问的概率很小。
那么图中,最小的一个元素,应该包含如下内容,
typedef struct{
__ELEM e;
void *p;
unsigned int f;
}__NODE;
typedef __NODE * _P_NODE;
e很清楚,二元配对。__ELEM 不应当出现在任何实际代码里。因为这个只是对应数学上(x,y)的结构。甚至你可以用
int32_t e;来描述,高16位表示 b,低 16位表示s。当然优化的工程版本我会扩展到64位,甚至把f扩展进去。这里开源的原理性版本我就不多解说了。
void *p ,这个目的是将对应结点和实际用户有用信息结合起来。或者和一些强约束的图结点(不是顶点)信息结合起来。
例如。我们假设要对数据库里的用户,进行用户关联分析,那么每个用户总要有个名字,此时,如果这个结点看作顶点的话,那么这个p可能就是指向一个字符串,如“小王”,“老李”。
或者我们要计算一个城市每条马路的流量,此时可以直接将 void *p里面存放 (int)的值,当然你要确保 sizeof(void *) >= sizeof(T) ,T是你想直接存放的数据类型宽度。
而如果我们希望加强数据结构中的索引算法,此处可以用p去指向一个其他__NODE的地址。
关于f。是针对__NODE的一些标记位。例如,我们可以增加 dirty_flag,用1位描述,表示是否为脏。或者 used_flag,表示是否被使用。或者 dir_flag,表示该边是否为有向边。 ver_flag,是否是顶点描述。等等。如下定义。
enum{
_ZV_NODE_F=0x0, //is not vectex
_P_NODE_F=0x1, // p is not empty
_ZC_NODE_F=0x2,// p or e change
_ZU_NODE_F=0x3,// node is not used
_D_NODE_F=0x4,//is not directly edge
};
增加 Z符号,表示如果后续成立,则标记为0,否则成立时标记为1。类似硬件里,低电平有效还是高电平有效一样。
那个flag的标记位用Z还是不用Z,判断的准则是,大概率时间,对应符号,默认值为0来决定。
例如我们新增一个结点,此时的上述大概率事件是 e.b != e.s ,大概率事件是无向图, 同时默认 node 被change ,但被used了。
#define _cINIT_EF 0
#define __CLEAN_EF(f) do {f = _cINIT_EF;}while (0)
#define __SET_EF(f,b,v) do {f = (v!=0)<<b;}while (0)
#define REFRESH_NODE(pe) do {\
__SET_EF(pe,_ZV_NODE,__CHECK_NULL((pe)->e));\
__SET_EF(pe,_P_NODE,((pe)->p));\
} while(0)
#define SET_NODE(pe,v1,v2,_p) do{(pe)->p = _p; __CLEAN_EF((pe)->f);\
if (v1>v2){__SET_ELEM((pe)->e,v1,v2);}else{__SET_ELEM((pe)->e,v2,v1);} \
REFRESH_NODE(pe);}while(0)
#define SET_DIR_NODE(pe,v1,v2,_p) do{SET_NODE(pe,v1,v2,_p) ;__SET_EF(pe,_D_NODE_F,1);}while(0)
可以发现,正常对 NODE的SET处理,存在一个flag调整的操作,而此时,调整的操作只有 ZV_NODE,和 p是否不为空的检测。
由于数据结构,存在结点的不确定操作例如典型的排序算法,并不能严格要求两个比较的内容是普通的标准类型,因此,需要外部传入一个比较函数。图论的算法,无论对结点,还是对某个子集,例如树,或者某个链路,都可能有不同的操作模式,由此我们还需要一些基础的函数类型如下
typedef int (*_OP_1_)(void *);
typedef int (*_OP_2_)(void *,void *);
typedef _P_NODE (*_NOP_)(_P_NODE);
typedef _P_NODE (*_NOP_1_)(_P_NODE,void*);
typedef _P_NODE (*_NOP_2_)(_P_NODE,_P_NODE,void*);
OP通常用于非__NODE的类型处理。但是比如我们需要定义个对结点集合,寻找最值的函数,则需要返回 _P_NODE,
例如,我们需要寻找一条最长的边,如果想返回对应结点,则需要使用 _P_NODE,用 int进行强制转换不是不可以,只是又是会比较危险。
那么对于一个图,最简单的定义则如下:
typedef struct{
unsigned long u;//used;
unsigned long np;//new position
}BUF_INFO;
typedef struct{
_P_NODE edge_list;
_P_NODE vec_list;
_NOP_ del_node;
_NOP_ ins_node;
BUF_INFO e_buf;
BUF_INFO v_buf;
int new_ID; //next new node KEY
unsigned long SIZE;//no change by ext code ||E||
unsigned long ORDER;//no change by ext order |V|
}_GEN_G;
typedef _GEN_G * P_GEN_G;
这里存在一个edge_list,和vec_list的指针,实际使用时,需要malloc空间。但始终要清楚,vec_list只是个索引,并不存在独立的对vec_list的操作。对外。实际工程化优化时,我会将 vec_list给删除。而多一个 vex_bias指针,在vex_bias之前的,只存放(x,y) x!=y,vex_bias之后的,只存放(x,y) x==x的指针。而每当加入新边,存在新的端点时,会对edge_list[vec_bias--]进行操作。
毕竟V集合是P(E)产生的。分成两个数组,会带来不必要的麻烦操作。此处是原理性的代码,所以还是清楚些的好。
SIZE,ORDER,表示当前图最大可支持的边数,和阶的量(参见图论)。大写的意思是别改它。这个值只和对 edge_list ,vec_list的 malloc代码关联。
del_node ,ins_node函数指针,初始为0,0时也不会调用。一个是当结点删除时需要做的额外工作,一个是当结点新增时,需要做的额外工作。
由此目前,一个最简单的图的创建工作。则需要size (边数) order (阶),del函数,当然可以为0,ins函数。具体实现如下
P_GEN_G create_G(unsigned int size,unsigned int order,_NOP_ ins,_NOP_ del){
P_GEN_G p = 0;
if ((size & order) == 0) return p;
p = (P_GEN_G)c_malloc(__ALIGN_8_SIZEOF(_GEN_G)+__ALIGN_8_SIZEOF(__NODE)*size+__ALIGN_8_SIZEOF(__NODE)*order*2);//||e|| = |E| + |V|
p->edge_list = (_P_NODE)((void*)p + __ALIGN_8_SIZEOF(_GEN_G)+__ALIGN_8_SIZEOF(__NODE)*order);
p->vec_list = (_P_NODE)((void*)p->edge_list + __ALIGN_8_SIZEOF(__NODE)*size);
INIT_BUF_INFO(&(p->e_buf),size+order);
INIT_BUF_INFO(&(p->v_buf),order);
p->SIZE = size+order;
p->ORDER = order;
p->new_ID = 1;
p->del_node = del;
p->ins_node = ins;
return p;
}
上面使用了我在 linux下 C编程 。。。。说明过的一个malloc_free库。用于安全的使用malloc。这里c_malloc和__ALIGN_8_SIZEOF均在上述库中处理。后者的定义如下
#define __ALIGN_8_SIZEOF(T) ((sizeof(T) + 0x7) & (~0x7))
没有什么特殊意义,就是整数倍byte对齐