构建非循环依赖关系的最简单,最有效的数据结构是什么?

时间:2009-08-11 16:08:36

标签: c++ algorithm data-structures dependencies graph-theory

我正在尝试构建一个确定销毁对象的顺序的序列。我们可以假设没有周期。如果对象A在其(A)构造期间使用对象B,则对象B在对象A的销毁期间仍应可用。因此,所需的破坏顺序是A,B。如果另一个对象C在其(C)构造期间也使用对象B,则所需的顺序是A,C,B。通常,只要对象X仅被销毁在构造过程中使用该对象的所有其他对象之后,破坏是安全的。

如果到目前为止我们的销毁订单是AECDBF,我们现在得到一个X(我们从来不知道建筑最初会发生什么样的订单,它是在飞行中发现的),它在构造过程中使用了C和F,然后我们可以通过将X放在列表中当前较早的前一个C或F(恰好是C)之前获得一个新的安全订单。所以新订单将是AB X CDEF。

在X示例的上下文中,链接列表似乎不合适,因为将涉及大量线性扫描以确定哪个更早,C或F.数组将意味着缓慢插入,这将是更多常见的操作。优先级队列实际上没有合适的接口,没有,“在这些项目中最早的一个之前插入此项目”(我们事先不知道正确的优先级,以确保它在较低优先级元素之前插入并且不打扰其他条目)。

构造所有对象,计算所需的顺序,并且序列将被迭代一次并按顺序被破坏。不需要进行任何其他操作(实际上,在使用任何数据结构来确定顺序之后,可以将其复制到平面数组中并丢弃)。

编辑:只是为了澄清,第一次使用对象的是它的构造时间。因此,如果A使用B,则E使用B,当E尝试使用B时,它已经被创建。这意味着堆栈不会提供所需的顺序。当我们想要AEB时,AB将成为ABE。

Edit2:我正在尝试按顺序构建订单以保持算法到位。我宁愿避免建立一个大的中间结构,然后将其转换为最终结构。

Edit3:我太复杂了; p

7 个答案:

答案 0 :(得分:7)

由于依赖项总是在依赖于它们的对象之前初始化,并且在这些对象被销毁之后仍然可用,因此以完全相反的初始化顺序销毁对象应始终是安全的。因此,您只需要一个链接列表,在初始化对象时将其添加到其中,然后逐步销毁,并为每个对象请求初始化其初始化之前尚未初始化的所有依赖项。

因此,对于每个对象的初始化:

  • 初始化自我,在我们开始时初始化未初始化的依赖关系
  • 将自我添加到销毁列表的前面(或者如果您正在使用堆栈,则将self推入堆栈)

并且为了销毁,只需从正面向前走链接列表(或从堆栈中弹出项目直到空),随时随地销毁。因此,按照B,A,C顺序初始化的第一段中的例子将按照C,A,B的顺序销毁 - 这是安全的;您的编辑中的示例将按B,A,E(不是A,B,E,因为A取决于B)的顺序初始化,因此按照E,A,B的顺序销毁,这也是安全的。

答案 1 :(得分:2)

将其存储为树

  • 每个资源都有一个节点
  • 让每个资源都保留一个指向依赖于该资源的资源的链接列表
  • 让每个资源都依赖于它所依赖的资源数量
  • 保留一个没有依赖关系的资源的顶级链表

要生成订单,请浏览您的顶级链表

  • 对于每个已处理的资源,将其添加到订单
  • 然后减少依赖于它的每个资源的计数
  • 如果任何计数达到零,则将该资源推送到顶级列表。

当顶级列表为空时,您已创建完整订单。

typedef struct _dependent Dependent;
typedef struct _resource_info ResourceInfo;

struct _dependent 
{
  Dependent * next;
  ResourceInfo * rinfo;
}
struct _resource_info
{
  Resource * resource; // whatever user-defined type you're using
  size_t num_dependencies;
  Dependent * dependents;
}

//...
Resource ** generateOrdering( size_t const numResources, Dependent * freeableResources )
{
  Resource ** const ordering = malloc(numResources * sizeof(Resource *));
  Resource ** nextInOrder = ordering;

  if (ordering == NULL) return NULL;
  while (freeableResources != NULL)
  {
    Dependent * const current = freeableResources;
    Dependent * dependents = current->rinfo->dependents;

    // pop from the top of the list
    freeableResources = freeableResources->next;

    // record this as next in order
    *nextInOrder = current->rinfo->resource;
    nextInOrder++;
    free(current->rinfo);
    free(current);

    while (dependents != NULL)
    {
       Dependent * const later = dependents;

       // pop this from the list
       dependents = later->next;

       later->rinfo->num_dependencies--;
       if (later->rinfo->num_dependencies == 0)
       {
          // make eligible for freeing
          later->next = freeableResources;
          freeableResources = later;
       }
       else
       {
           free(later);
       }
    }
  }
  return ordering;
}

为了帮助创建树,您可能还需要一个快速查找表来将Resource映射到ResourceInfo

答案 2 :(得分:1)

听起来你应该尝试使用正如你所描述的模式构建一个有向的非循环图。一个邻接列表表示(链接列表的向量,可能,当你正在获取新的节点时),应该这样做。

有一件事我不清楚:你是否需要随机计算,或者在获得所有信息之后?我假设后者,你可以等到图表完成。如果是这种情况,那么您的问题恰好是topological sort,其中有时间线性的边缘和顶点实现。这是一个相对简单的算法。我的描述让我有点转过身来(吃午饭会让我变得缓慢而困倦,对不起),但实际上你需要一个“反向”的拓扑排序,但原则是相同的。我不会试图解释算法是如何工作的(参见:slow and sleepy),但我认为应用程序应该清楚。除非我完全错了,在这种情况下,没关系?

总结一下: 从某种意义上说,您正在建立数据结构,图形,在您希望的有效时间内(这取决于您的插入方式)。该图表反映了哪些对象需要等待哪些其他对象。然后,当你完成构建它时,你运行拓扑排序,这反映了它们的依赖性。

编辑:已经有一段时间了,因为我混淆了“你的”和“你是”。 :(

答案 3 :(得分:1)

听起来你有一个有向无环图,topological sort会给你一个对象破坏的顺序。 您可能还需要特殊处理图形具有循环(循环依赖性)的情况。

答案 4 :(得分:0)

代表它:如果A的析构函数必须在B之后运行,则从A到B的边的图形。现在插入X意味着添加两条边,如果你保留一个排序的节点索引,那就是O(n log n))。阅读销毁订单:选择任何节点,按照边缘直到你不能再进行。可以安全地调用该节点的析构函数。然后选择其中一个剩余节点(例如您遍历的上一个节点),然后重试。

根据你的说法,插入经常发生但是序列只被迭代一次以进行破坏:这个数据结构应该是合适的,因为它有快速插入,代价是查找速度较慢。也许其他人可以建议更快的方式在这个数据结构中进行查找。

答案 5 :(得分:0)

这听起来像是在从树叶上建造一棵树。

答案 6 :(得分:0)

您是否更有兴趣以正确的顺序销毁一流的C ++对象以避免依赖性,或者对您对算法和可重复性更感兴趣的一些外部现实行为进行建模?

在第一种情况下,您可以使用智能的引用计数指针(查找shared_ptr,可在Boost和即将推出的C ++标准中使用)来跟踪您的对象,可能还有工厂函数。当对象A初始化并想要使用对象B时,它会调用B的工厂函数并获得一个指向B的智能指针,从而增加B的引用计数。如果C也引用B,则B的引用计数再次递增。 A和C可以按任何顺序释放,B必须最后释放。如果您将shared_ptr存储在无序数据结构中的所有对象中,那么当您运行完毕后,您将释放所有对象的列表,shared_ptr将处理其余对象,正确的顺序。 (在此示例中,A和C仅由所有对象的列表引用,因此它们的引用计数均为1,并且B由每个A和C以及所有对象的列表引用,因此其引用计数为3。所有对象的列表释放它对对象的引用,A和C的引用计数变为0,因此它们可以按任何顺序释放.B的引用计数不会变为0,直到A和C都被释放,因此它将继续生活,直到所有引用都被释放。)

如果您对算法更感兴趣,可以在自己的数据结构中对引用计数进行建模,这样在完成后最终可能会看起来像有向非循环图。