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

Posted

技术标签:

【中文标题】构建非循环依赖关系的最简单和最有效的数据结构是啥?【英文标题】:What's the simplest and most efficient data structure for building acyclic dependencies?构建非循环依赖关系的最简单和最有效的数据结构是什么? 【发布时间】:2009-08-11 16:08:36 【问题描述】:

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

如果到目前为止我们的销毁顺序是 AECDBF,我们现在得到一个 X(我们事先不知道构造最初会以什么顺序发生,它是动态发现的),它在构造过程中使用 C 和 F,然后我们可以通过将 X 放在列表中当前较早的 C 或 F(恰好是 C)之前,获得一个新的安全命令。所以新的顺序是 ABXCDEF。

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

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

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

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

Edit3:我把它弄得太复杂了;p

【问题讨论】:

为什么不使用智能指针呢? 您在谈论多少个对象,这种清理多久会发生一次? 在您的第一次编辑中,您正在向后进行堆栈。如果 A 使用 B,则您有 AB(假设您在左侧添加和删除项目)。然后,如果 E 使用 B,你会得到 EAB,它工作得很好——你摧毁 E,然后是 A,然后是 B。 【参考方案1】:

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

所以对于每个对象的初始化:

初始化自身,在我们进行时初始化未初始化的依赖项 将 self 添加到销毁列表的前面(如果您正在使用堆栈,则将 self 推入堆栈)

为了销毁,只需从前面向前走链表(或将项目从堆栈中弹出直到为空),边走边销毁。因此,按 B、A、C 顺序初始化的第一段中的示例将按 C、A、B 顺序销毁 - 这是安全的;您编辑中的示例将按 B、A、E 顺序初始化(不是 A、B、E,因为 A 依赖于 B),因此按 E、A、B 顺序销毁,这也是安全的。

【讨论】:

他们不是。他的第二段详细说明了这是不可能的情况。 它说没有这样的事情。如果 C 和 F 在 X 的初始化期间没有被初始化,则启动无法继续,因为 X 依赖于它们。当然,他可以按照他的建议停止破坏 X,但他也可以简单地将其添加到当前安全命令中以获得新的安全命令。 “由于依赖项总是在依赖它们的对象之前初始化,并且在这些对象被销毁之前一直可用,”实现这一点。这不是真的。查看我的编辑。 在您编辑的示例中,B 必须在 A 之前初始化,因为 A 使用 B。因此堆栈将是 BAE,而不是 ABE 或 AEB;这是正确的,因为我们想最后摧毁 B。 好的,所以如果 A 没有预先初始化其所有依赖项,则在其初始化代码结束时让 A 将自己推入堆栈。效果是一样的。【参考方案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;

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

【讨论】:

【参考方案3】:

听起来您应该尝试使用您所描述的模式构建一个有向无环图。邻接表表示(可能是链表的向量,因为您正在动态获取新节点)应该这样做。

有一件事我不清楚:你需要随机计算,还是在你获得所有信息之后?我假设后者,你可以等到你的图表完成。如果是这种情况,您的问题正是topological sort,其中有 time-linear-in-edges-and-vertices 实现。这是一个相对简单的算法。你的描述让我有点反感(吃午饭让我又慢又困,对不起),但实际上你可能需要一个“反向”拓扑排序,但原理是相同的。我不会试图解释该算法是如何工作的(参见:slow and sleepy),但我认为应用程序应该是清晰的。除非我完全错了,在这种情况下,没关系?

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

编辑:自从我混淆“你的”和“你是”以来已经有一段时间了。 :(

【讨论】:

“你需要随机计算,还是在你获得所有信息之后?我假设是后者,你可以等到你的图表完成。” 【参考方案4】:

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

【讨论】:

【参考方案5】:

这样表示:如果 A 的析构函数必须在 B 之后运行,则图的边从 A 到 B。现在插入 X 意味着添加两条边,如果您保留节点的排序索引,那就是 O(n log n))。要阅读销毁顺序:选择任何节点,沿着边缘走,直到你再也不能了。可以安全地调用该节点的析构函数。然后选择剩余节点之一(例如您遍历的前一个节点)并重试。

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

【讨论】:

【参考方案6】:

这听起来就像你正在从叶子上建造一棵树。

【讨论】:

有点,除了我实际上不知道哪些项目是叶子或它们将在手前实例化的顺序。 我所看到的是,你有一个随机包的元素在某种原始的先存中。然后,正如您所了解的那样,您可以开始将数据依赖关系设置为树。是的?没有?【参考方案7】:

您是对以正确的顺序销毁一流的 C++ 对象以避免依赖关系更感兴趣,还是对建模一些您对算法和可重复性更感兴趣的外部真实世界行为更感兴趣?

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

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

【讨论】:

以上是关于构建非循环依赖关系的最简单和最有效的数据结构是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在 SQL 中找到两个集合的最紧凑和最有效的方法是啥? [复制]

显示一对一关系的最有效方法是啥[Laravel]

如何以增量方式构建非二叉树(具有依赖关系)

在不对所有记录执行循环的情况下从数据库中检索特定数据的最有效方法是啥?

从其他数据库更新表数据的最有效方法是啥?

在 Julia 中定义一个非常稀疏的网络矩阵的最有效方法是啥?