在C
中,我可以单独,双倍地写一个来自node
的4,5个链接。但是我可以建立一个类似于哪里的结构,从节点中未指定链接数,即它必须动态添加新链接。如果在C
中无法实现,那我可以在哪里?例如:节点“a”具有到节点“b”,“c”,“d”,“e”和“f”的链接,并且“b”具有仅指向“c”,“d”,“a”的链接。 。 像那样。因此未指定链接数。希望你能理解我的问题。
答案 0 :(得分:1)
是的,通过为链接使用动态分配的数组。通常,您希望存储实际链接数(count
)以及动态分配的链接指针数(maxcount
):
struct node {
size_t maxcount;
size_t count;
struct node **child;
/* Plus node data or other properties */
};
如果每个链接(或边缘)都有相关的权重,您可以使用
struct node;
struct edge {
struct node *target;
double weight;
};
struct node {
size_t maxcount;
size_t count;
struct edge *edges;
/* Visited flag, for full graph traversal */
int visited;
/* Node data; here, name, as a C99 flexible array member */
char name[];
};
请注意,Graphviz是可视化此类图形的绝佳工具。我经常编写测试来在输出上生成DOT语言图定义,以便我可以使用例如来自Graphviz的dot
将它们画成漂亮的图形。
如果您使用上面struct edge
和struct node
的有向图,并且所有visited
字段都清零,那么您可以安全地 - 也就是说,不会卡在周期中 - 使用递归辅助函数为这样的图创建DOT输出,比如说
static void write_dot_node(struct node *graph, FILE *out)
{
size_t i;
if (graph->visited)
return;
graph->visited = 1;
fprintf(out, "\tnode%p [ label=\"%s\" ];\n", graph, graph->name);
for (i = 0; i < graph->count; i++) {
write_dot_node(graph->edges[i].target, out);
fprintf(out, "\tnode%p -> node%p [ taillabel=\"%.3f\" ];\n",
graph, graph->edges[i].target,
graph->edges[i].weight);
}
}
void write_dot(struct node *graph, FILE *out)
{
if (!graph || !out)
return;
fprintf(out, "digraph {\n");
write_dot_node(graph, out);
fprintf(out, "}\n");
}
如果你有真正巨大的图表,在某些情况下,上述情况可能会过于严重。然后需要将其转换为非递归循环,该循环使用尚未访问的显式堆栈节点,write_dot
函数初始化并丢弃堆栈:
#define VISITED_STACKED (1<<0)
#define VISITED_VISITED (1<<1)
int write_dot(struct node *graph, FILE *out)
{
struct node **unseen = NULL, **temp;
size_t unseens = 1;
size_t unseens_max = 1024; /* Initial stack size */
unseen = malloc(unseens_max * sizeof *unseen);
if (!unseen) {
errno = ENOMEM;
return -1;
}
unseen[0] = graph;
fprintf(out, "digraph {\n");
while (unseens > 0) {
struct node *curr = unseen[--unseens];
size_t i, n;
/* Already visited (since pushed on stack)? */
if (curr->visited & VISITED_VISITED)
continue;
else
curr->visited |= VISITED_VISITED;
fprintf(out, "\tnode%p [ label=\"%s\" ];\n", curr, curr->name);
for (i = 0, n = 0; i < curr->count; i++) {
/* Count unvisited child nodes */
n += !(curr->edges[i].target->visited & VISITED_STACKED);
fprintf(out, "\tnode%p -> node%p [ taillabel=\"%.3f\" ];\n",
curr, curr->edges[i].target, curr->edges[i].weight);
}
if (n + unseens > unseens_max) {
if (n + unseens > 1048576)
unseens_max = ((n + unseens) | 1048575) + 1048573;
else
if (n + unseens < 2 * unseens_max)
unseens_max = 2 * unseens_max;
else
unseens_max = 2 * (n + unseens);
temp = realloc(unseen, unseens_max * sizeof *unseen);
if (!temp) {
free(unseen);
errno = ENOMEM;
return -1;
} else
unseen = temp;
}
/* Add unvisited child nodes to stack. */
for (i = 0; i < curr->count; i++)
if (!(curr->edges[i].target->visited & VISITED_STACKED)) {
curr->edges[i].target->visited |= VISITED_STACKED;
unseen[unseens++] = curr->edges[i].target;
}
}
free(unseen);
fprintf(out, "}\n");
return 0;
}
在这种情况下,VISITED_STACKED
位掩码表示节点已经添加到堆栈中以供以后处理,VISITED_VISITED
位掩码表示节点已被处理。
正如Ovanes在对这个答案的评论中所指出的,对于一个非常密集的图,你可以使用c++映射或散列表,特别是如果你经常需要找出一对节点是否共享边缘或不。在这种情况下,您可以使用目标指针的可选哈希表来扩充上述结构。
为了好玩,我在实践中使用界面 digraph.h 对每个图形用户指定的重新分配大小函数和散列函数的定向加权图进行了测试:
#ifndef DIGRAPH_H
#define DIGRAPH_H
#include <stdlib.h>
#include <stdio.h>
struct digraph_node;
struct digraph_edge {
struct digraph_edge *hash_next; /* Hash table slot chain */
size_t hash_code; /* Hash value */
struct digraph_node *target; /* Target edge of the node */
double weight; /* Weight of this edge */
};
struct digraph_node {
struct digraph_node *hash_next; /* Hash table slot chain */
size_t hash_code; /* Hash value */
struct digraph_edge *edge; /* Array of edges */
size_t edges; /* Number of edges in this node */
size_t edges_max; /* Number of edges allocated for */
struct digraph_edge **hash_slot; /* Optional edge hash table */
size_t hash_size; /* Size of optional edge hash table */
char name[]; /* Name of this node */
};
typedef struct {
struct digraph_node **node; /* Array of pointers to graph nodes */
size_t nodes; /* Number of nodes in this graph */
size_t nodes_max; /* Number of nodes allocated for */
struct digraph_node **hash_slot; /* Optional node hash table */
size_t hash_size; /* Size of optional node hash table */
/* Graph resize policy and hash function */
size_t (*graph_nodes_max)(size_t nodes);
size_t (*graph_hash_size)(size_t nodes);
size_t (*graph_hash_func)(const char *name);
/* Node resize policy and hash function */
size_t (*node_edges_max)(size_t edges);
size_t (*node_hash_size)(size_t edges);
size_t (*node_hash_func)(struct digraph_node *target);
} digraph;
void digraph_init(digraph *graph);
void digraph_free(digraph *graph);
struct digraph_node *digraph_find_node(digraph *graph, const char *name);
struct digraph_edge *digraph_find_edge(digraph *graph, struct digraph_node *source, struct digraph_node *target);
struct digraph_node *digraph_add_node(digraph *graph, const char *name);
struct digraph_edge *digraph_add_edge(digraph *graph, struct digraph_node *source, struct digraph_node *target, double weight);
int digraph_dot(digraph *graph, FILE *out);
size_t digraph_default_graph_nodes_max(size_t nodes);
size_t digraph_default_graph_hash_size(size_t nodes);
size_t digraph_default_graph_hash_func(const char *name);
size_t digraph_default_node_edges_max(size_t edges);
size_t digraph_default_node_hash_size(size_t edges);
size_t digraph_default_node_hash_func(struct digraph_node *target);
#define DIGRAPH_INIT { NULL, 0, 0, \
NULL, 0, \
digraph_default_graph_nodes_max, \
digraph_default_graph_hash_size, \
digraph_default_graph_hash_func, \
digraph_default_node_edges_max, \
digraph_default_node_hash_size, \
digraph_default_node_hash_func \
}
#endif /* DIGRAPH_H */
为简单起见,digraph_add_node()
和digraph_add_edge()
函数始终设置为errno
;如果成功,则0
;如果此类节点或边缘已存在,则EEXIST
,否则返回错误代码。如果节点或边缘已经存在,则函数会返回现有节点(但errno
设置为EEXIST
而不是0
)。这使得添加新边缘非常容易。
在英特尔酷睿i5-6200U处理器上运行64位Linux的笔记本电脑上,生成5,000个随机节点,12,500,000个随机边缘(每边2,500个节点)需要大约18秒,并使用GraphViz点语言描述整个图形。这对我来说速度很快,因为我没有任何可视化这些图形的工具;甚至Graphviz也完全扼杀了这些。
请注意,因为图结构包含指向图中每个节点的指针数组,所以使用上述结构不需要递归:
int digraph_dot(digraph *graph, FILE *out)
{
size_t i, k;
if (!graph)
return errno = EINVAL;
if (!out || ferror(out))
return errno = EIO;
fprintf(out, "digraph {\n");
/* Describe all nodes. */
for (i = 0; i < graph->nodes; i++)
if (graph->node[i])
fprintf(out, "\tnode%p [label=\"%s\"];\n",
(void *)graph->node[i], graph->node[i]->name);
/* Describe all edges from all nodes. */
for (i = 0; i < graph->nodes; i++)
if (graph->node[i]) {
if (graph->node[i]->edges) {
for (k = 0; k < graph->node[i]->edges; k++)
fprintf(out, "\tnode%p -> node%p [taillabel=\"%.3f\"];\n",
(void *)(graph->node[i]),
(void *)(graph->node[i]->edge[k].target),
graph->node[i]->edge[k].weight);
} else {
fprintf(out, "\tnode%p;\n", (void *)(graph->node[i]));
}
}
fprintf(out, "}\n");
if (fflush(out))
return errno;
if (ferror(out))
return errno = EIO;
return errno = 0;
}
当然,如果你有那些密集的图形,每个节点平均有一个边缘到图中其他节点的一半,一个权重矩阵(零重量保留没有边缘,或一个单独的布尔矩阵描述哪些边缘确实存在)会更有意义,并且也会有更好的缓存局部性。
当矩阵以行主顺序存储时(例如,如果在C中定义二维数组),将每行对应一个源节点,每列对应一个目标节点是有意义的因此,描述来自一个节点的边和/或权重的向量在存储器中是连续的。检查从特定源节点向外指向的边缘比检查指向特定目标节点的边缘更常见;因此,具有更好缓存局部性的更常见情况应该有助于整体性能。
答案 1 :(得分:0)
如果我们在谈论图表,您应该决定要尝试实施的图表:
我假设您的问题,您需要实现有向图。 顶点(= 节点)A
可以有有向边(= link )到顶点B
,但不一定相反。一般来说,你正在寻求实现一个
Multigraph。在这里,您可以使用两种方法来表示这样的图表:
让我们举一个关于每种方法如何运作的例子。我将为您提供主要方向,以便您自己进行培训和实施。否则,我可能最终会做你的作业:)
给出以下图G:
A -> B -> C
^--D-^
E
请注意! E
没有传入或传出边缘。这仍然是一个有效的图表。
使用矩阵我们可以将其表示为:
A B C D E
+-+-+-+-+-+
A |0|1|0|0|0|
+-+-+-+-+-+
B |0|0|1|0|0|
+-+-+-+-+-+
C |0|0|0|0|0|
+-+-+-+-+-+
D |1|1|0|0|0|
+-+-+-+-+-+
E |0|0|0|0|0|
+-+-+-+-+-+
我们所有的数字都是0,我们没有从顶点X
到顶点Y
的边缘。大于1的数字表示从源顶点到目标顶点的边数。
一个例子:顶点C
是否指向顶点B
(例如有一个有向边)?
答案:如果有C
的号码,则找到行B
移至列>0
,是的。否则,没有。在我们的例子中,数字是0
=&gt;没有联系。
如果您需要表示加权连接,您可能会在矩阵的每个单元格中得到一个数组/列表。如果数组为空(hint: one of the ways to implement 0 sized array)=&gt;顶点之间没有连接,否则数组的每个元素代表连接的权重。元素数表示连接数。
此外,要注意在图表中,顶点可能有自己的边缘。不知道这是否适用于您的情况。
请记住,Matrix是一种非常耗费内存的代表。它允许您进行非常快速的查找,但始终需要O(|V|^2)
(|V|
代表图中顶点的总数)元素来表示带矩阵的图形。
2D阵列是C中受到良好支持的特性。这里有一个小例子,关于如何静态从C中的上述情况初始化5x5矩阵。对于动态初始化,稍微深入C语言(看看malloc
,free
):
#define ROWS 5
#define COLUMNS 5
int matrix[ROWS][COLUMNS] =
// A B C D E
/* A */ { { 0, 1, 0, 0, 0 }
/* B */ , { 0, 0, 1, 0, 0 }
/* C */ , { 0, 0, 0, 0, 0 }
/* D */ , { 1, 1, 0, 0, 0 }
/* E */ , { 0, 0, 0, 0, 0 }
}
;
使用第二种方法(哈希表或字典)有点不同。
你需要一个哈希表。 AFAIK,C和C99不支持哈希表。因此,您需要使用现有的一个开源实现或自己实现哈希表。您的问题不会质疑如何在C ++中执行,其中map
或unordered_map
(&gt; = C ++ 11)是标准库的一部分。但是你还在问什么可以用来代替C
。因此,此时的结束语言为C++
。
以下是关于如何做到这一点的想法:让每个顶点成为Hashtable中的一个键,它指向一组 连接存在的顶点或构建的第二个Hashtable 增加特定映射的权重。
A -> (B,)
B -> (C,)
C -> ()
D -> (A, B)
E -> ()
如果有多个连接,您需要引入类似的内容:
A -> {B -> 1,}
B -> {C -> 1,}
C -> {}
D -> {A -> 1, B -> 1}
E -> {}
让我们说A对B有两条边,比结构实例化如下:
A -> {B -> 2,}
B -> {C -> 1,}
C -> {}
D -> {A -> 1, B -> 1}
E -> {}
如果图表很大且稀疏,你可以节省很多内存。
最简单的实现IMO将在Python中,因为您可以使用什么代替C
。但您可以真正使用C
,C++
,Java
,Python
,Ruby
或任何其他语言来实现此类问题。