【问题标题】:C++ Tree Data StructureC++ 树数据结构
【发布时间】:2018-06-30 01:53:26
【问题描述】:

背景:

因此,我一直在将一些较旧的 Java 代码移植到 C++ 中,但遇到了一个问题,这让我很难进行。我的项目使用树数据结构来表示 3D 动画的节点层次结构。

Java:

public final class Node {

    private final Node mParent;
    private final ArrayList<Node> mChildren;

    //private other data, add/remove children / parents, etc ...
}

在 Java 中,创建允许修改等的树非常简单。

问题:

我遇到了 C++ 的问题,如果不手动分配新的内存块并移动现有的内存块,就无法轻松添加数组,因此我切换到 std::vector。向量的问题是我刚才在内部描述的操作会使指向该元素的任何指针无效。所以基本上如果你不想使用指针,你需要一种方法来支持它们,这样保存实际节点的内存就不会移动。我认为您可以使用std::shared_ptr/std::unique_ptr 将节点包装在std::vector 中,我尝试使用这种方法,但它变得非常笨拙。另一种选择是有一个“树”类来包装节点类并且是操作它的接口,但是(对于我的用例)处理切断分支并将它们放入自己的树中会很烦人并可能附加不同的分支。

我在网上看到的大多数示例是具有 2 个节点而不是动态的二叉树,或者它们有许多关于内存泄漏 / 等的 cmet。我希望上面显示的 java 代码有一个很好的 C++ 替代方案(没有内存泄漏问题等)。此外,我不会进行任何排序,树的目的是维护层次结构而不是对其进行排序。

老实说,我真的不确定该往哪个方向发展,过去 2 天我一直在尝试不同的方法,但没有一个“感觉”正确,而且通常很难管理,任何帮助将不胜感激!

编辑:

关于为什么 shared_ptrs 不实用的编辑:

class tree : std::enable_shared_from_this<tree> {

    std::shared_ptr<tree> parent;
    std::vector<std::shared_ptr<tree>> children;

public:

    void set_parent(tree& _tree) {
        auto this_shared_ptr = shared_from_this();
        if (parent != nullptr) {
            auto vec = parent->children;

            auto begin = vec.begin();
            auto end = vec.end();

            auto index = std::distance(begin, std::find_if(begin, end, [&](std::shared_ptr<tree> const& current) -> bool {
                return *current == this_shared_ptr;
            }));

            vec.erase(std::remove(begin, end, index), end);
        }
        parent = std::shared_ptr<tree>(&_tree);
        if (parent != nullptr) {
            parent->children.push_back(this_shared_ptr);
        }
    }

};

使用像上面这样的指针变得非常冗长,我希望有一个更简单的解决方案。

【问题讨论】:

  • 尝试std::list 而不是std::vector 以获得快速而肮脏的解决方案。如果列表中的元素被删除,指向列表元素的指针不会失效。但同样,如果您可以使用 std::list,这又快又脏。
  • 一个节点最多可以拥有多少个子节点?您是否非常关心性能(1),(2)一点点,或者(3)不是真的?您需要能够从树中删除节点,还是只添加它们?
  • @JohnZwinck 这将主要是一个只读树,因此如果这意味着迭代会更快,那么对结构的修改可能会更慢。至于子节点的最大数量,并没有明确定义的最大值,每个节点可以有任意数量的子节点。最好能够添加/删除节点,但是一旦构建了树,这些操作就不太常见了。
  • std::shared_ptr 到底有什么“笨拙”的地方?唯一的问题是,您必须弄清楚如何处理循环引用。对此的经典解决方案是对父节点使用弱指针。
  • 如果std::shared_ptr&lt;tree&gt; 无处不在,请使用别名。

标签: c++ tree


【解决方案1】:

您可以将节点存储在单个向量中,并使用在调整向量大小时不会更改的相对指针:

typedef int32_t Offset;
struct Node {
    Node(Offset p) : parent(p) {}
    Offset parent = 0; // 0 means no parent, so root node
    std::vector<Offset> children;
};

std::vector<Node> tree;
std::vector<uint32_t> free_list;

添加节点:

uint32_t index;
if (free_list.empty()) {
    index = tree.size();
    tree.emplace_back(parent_index - tree.size());
} else {
    index = free_list.back();
    free_list.pop_back();
    tree[index].parent = parent_index - index;
}

tree[parent_index].children.push_back(index - parent_index);

删除节点:

assert(node.children.empty());
if (node.parent) {
    Node* parent = &node + node.parent;
    auto victim = find(parent->children.begin(), parent->children.end(), -node.parent);
    swap(*victim, parent->children.back()); // more efficient than erase from middle
    parent->children.pop_back();
}
free_list.push_back(&node - tree.data());

【讨论】:

  • 是的,我正在考虑这种方法,最初让我远离的是节点列表将是一个单独的“部分”,我希望数据结构可以是自包含的在没有外部“帮助器”的节点本身中。在我的用例中,“节点”是直接操作的对象,而不是充当访问器的“树”接口。但如果这是唯一简单的方法,也许我会再试一次。
【解决方案2】:

您看到的差异的唯一原因是,如果您将对象直接放在 c++ 中的向量本身中(这在 Java 中是无法做到的。)然后它们的地址将绑定到向量中当前分配的缓冲区。不同之处在于Java中,所有对象本身都是分配的,因此数组中实际上只有一个“对象引用”。 c++ 中的等价物是创建一个指针向量(希望包装在智能指针对象中),因此向量元素只是一个地址,但对象存在于固定内存中。它增加了一个额外的指针跳跃,但它的行为更像你在 java 中所期望的。

struct X {
   char buf[30];
};
std::vector<X> myVec{ X() };

鉴于上述情况,myVec 中的 X 元素在分配中是连续的。 sizeof(myVec[0]) == sizeof(X)。但是如果你把指针放在向量中:

std::vector<unique_ptr<X>> myVec2{ make_unique<X>() };

这应该更像你想要的,并且当向量调整大小时指针不会变得无效。指针只会被复制。

您可以这样做的另一种方法是在您的设计中稍作改动。考虑完全替代指针,其中您的树包含元素向量,并且您的节点包含整数向量,它们是该向量的索引。

【讨论】:

    【解决方案3】:

    vector, forward_list, ..., 可以使用任何 std 容器类(除了内置数组或 std::array)。 您的问题似乎是 java 类是引用类型,而 C++ 类是值类型。下面的 sn-p 在编译时触发“无限递归”或“使用不完整类型”错误:

    class node{
        node mParent;//trouble
        std::vector<node> children;
        //...
    };
    

    mParent 成员必须是引用类型。为了强加引用语义,您可以将其设为原始指针:

    node* mParent;

    您也可以使用指针作为容器的参数类型,但作为 C++ 初学者,这很可能会导致内存泄漏和奇怪的运行时错误。我们现在应该尽量远离手动内存管理。所以我将你的 sn-p 修改为:

    class node{
    private:
        node*  const mParent;
        std::vector<node> children;
    public:
        //node(node const&)=delete;//do you need copies of nodes? you have to properly define this if yes.
        node(node *parent):
            mParent{parent}{};
        void addChild(/*???*/){
             children.emplace_back(this);
            //...
        };
        //...
    };
    

    【讨论】:

      猜你喜欢
      • 2010-09-09
      • 1970-01-01
      • 1970-01-01
      • 2010-10-30
      • 2010-10-30
      • 2011-03-19
      • 2012-04-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多