【问题标题】:How to get results efficiently out of an Octree/Quadtree?如何有效地从八叉树/四叉树中获得结果?
【发布时间】:2010-06-16 17:34:03
【问题描述】:

我正在开发一款 3D 软件,该软件有时必须在大量曲线(有时约 100,000 条)之间执行交集。最自然的方法是进行 N^2 边界框检查,然后边界框重叠的曲线相交。

我听说八叉树的好消息,所以我决定尝试实现一个,看看我是否会提高性能。

这是我的设计: 每个八叉树节点都被实现为一个具有子节点列表和对象索引有序列表的类。

当一个对象被添加时,它被添加到完全包含该对象的最低节点,或者如果该对象没有填充所有子节点,则添加到该节点的一些子节点。

现在,我要做的是检索与给定对象共享树节点的所有对象。为此,我遍历所有树节点,如果它们包含给定的索引,我将它们的所有其他索引添加到有序列表中。

这是有效的,因为每个节点内的索引已经排序,所以找出每个索引是否已经在列表中很快。但是,列表最终必须调整大小,这占用了算法的大部分时间。所以我需要的是某种树状数据结构,它可以让我有效地添加有序数据,并且在内存中也很有效。

有什么建议吗?

【问题讨论】:

  • 我确定您误认为 调整大小 会导致问题。调整大小与添加具有相同的复杂性,O(N) 用于填充大小为 N 的列表。另一方面,如果不添加元素,则移动元素以保持列表排序具有 O(N*N) 复杂性按顺序。

标签: c# performance graphics data-structures


【解决方案1】:

假设您将 OctTree 的大小作为树的属性,您应该能够预先分配一个大于您可能放入的东西数量的列表。预先分配大小将防止调整大小发生只要尺寸大于您的需要。我假设您正在使用 SortedList 来保留您的有序结果。

var results = new SortedList<Node>( octTree.Count );
// now find the node and add the points
results = result.TrimToSize(); // reclaim space as needed

另一种方法是扩充您的数据结构,将树的大小保持在节点本身的当前节点下方。然后您将能够找到感兴趣的节点并直接确定列表的大小。您所要做的就是修改插入/删除操作,以更新在操作结束时插入/删除的每个祖先节点的大小。

【讨论】:

  • 我也可以使用常规列表来做到这一点,但是预先分配如此大量的内存是不是有点脏?
  • @Reveazure -- 我建议了一种通过增加结构来减少过度分配的机制。如果您有另一个启发式方法来预测必要的数量,那也可以。由于列表实际上只是保留引用,而不是实际对象的副本,因此即使 100,000 个对象所需的内存量也相对较小(在 32 位机器上约为 800K)。
  • 向 SortedList 添加项目的问题显然不是重新分配,而是确保列表保持排序状态(将更大的元素移动到末尾以便可以插入新项目)。
  • @Rotsor - 我并不是建议他使用 SortedList,我假设他是,并建议他如何提高效率,消除调整大小的需要,这是他的观察,而不是我的.如果他愿意使用排序列表以外的其他东西 SortedDictionary 是绝对可能的。看来密钥是唯一的,所以我不知道为什么它不起作用。另一方面,也许他知道这些项目无论如何都是按关键顺序插入的,所以重新排序不是问题。
  • @tvanfosson 也许他知道这一点,并且在调整大小时遇到​​了一些神奇的问题。或者也许他没有,并且他的算法复杂度通常为 O(N*N)。甚至不知道什么是更可能的......
【解决方案2】:

SortedDictionary(.NET 2+)或SortedSet(仅限.NET 4)可能是您想要的。它们是树状结构。

SortedList 是一个哑类,在结构上与List 没有区别。

但是,我仍然不完全清楚为什么需要此列表进行排序。 也许如果你能详细说明这个问题,我们可以找到一个你根本不需要排序的解决方案。例如一个简单的HashSet 可以做到。如果散列处理得当,它在查找和插入方面都比SortedList 或任何树结构都快。

好的,现在当我清楚您想要合并排序列表时,我可以尝试编写一个实现。

起初,我使用SortedDictionary 实现了合并以存储所有数组的头。在每次迭代中,我从字典中删除了最小的元素,并从同一个数组中添加了下一个元素。性能测试表明 SortedDictionary 的开销很大,因此几乎不可能使其比简单的串联+排序更快。在小型测试中,它甚至难以匹配 SortedList 的性能。

然后我用定制的二进制堆实现替换了SortedDictionary。性能提升是巨大的(超过 6 倍)。在某些测试中,这个 Heap 实现甚至成功击败了 .Distinct()(通常是最快的)。

这是我的代码:

class Heap<T>
{
    public Heap(int limit, IComparer<T> comparer)
    {
        this.comparer = comparer;
        data = new T[limit];
    }

    int count = 0;
    T[] data;

    public void Add(T t)
    {
        data[count++] = t;
        promote(count-1);
    }

    IComparer<T> comparer;

    public int Count { get { return count; } }

    public T Pop()
    {
        T result = data[0];
        fill(0);
        return result;
    }

    bool less(T a, T b)
    {
        return comparer.Compare(a,b)<0;
    }

    void fill(int index)
    {
        int child1 = index*2+1;
        int child2 = index*2+2;
        if(child1 >= Count)
        {
            data[index] = data[--count];
            if(index!=count)
                promote(index);
        }
        else
        {
            int bestChild = child1;
            if(child2 < Count && less(data[child2], data[child1]))
            {
                bestChild = child2;
            }

            data[index] = data[bestChild];
            fill(bestChild);
        }
    }

    void promote(int index)
    {
        if(index==0)
            return;
        int parent = (index-1)/2;
        if(less(data[index], data[parent]))
        {
            T tmp = data[parent];
            data[parent] = data[index];
            data[index] = tmp;
            promote(parent);
        }
    }
}

struct ArrayCursor<T>
{
    public T [] Array {get;set;}
    public int Index {get;set;}
    public bool Finished {get{return Array.Length == Index;}}
    public T Value{get{return Array[Index];}}
}

class ArrayComparer<T> : IComparer<ArrayCursor<T>>
{
    IComparer<T> comparer;
    public ArrayComparer(IComparer<T> comparer)
    {
        this.comparer = comparer;
    }

    public int Compare (ArrayCursor<T> a, ArrayCursor<T> b)
    {
        return comparer.Compare(a.Value, b.Value);
    }
}

static class HeapMerger
{
    public static IEnumerable<T> MergeUnique<T>(this T[][] arrays)
    {
        bool first = true;
        T last = default(T);
        IEqualityComparer<T> eq = EqualityComparer<T>.Default;
        foreach(T i in Merge(arrays))
            if(first || !eq.Equals(last,i))
            {
                yield return i;
                last = i;
                first = false;
            }
    }

    public static IEnumerable<T> Merge<T>(this T[][] arrays)
    {
        var map = new Heap<ArrayCursor<T>>(arrays.Length, new ArrayComparer<T>(Comparer<T>.Default));

        Action<ArrayCursor<T>> tryAdd = (a)=>
        {
            if(!a.Finished)
                map.Add(a);
        };

        for(int i=0;i<arrays.Length;i++)
            tryAdd(new ArrayCursor<T>{Array=arrays[i], Index=0});

        while(map.Count>0)
        {
            ArrayCursor<T> lowest = map.Pop();
            yield return lowest.Value;
            lowest.Index++;
            tryAdd(lowest);
        }
    }
}

【讨论】:

  • 我之所以选择在节点内部使用排序列表,是因为每个节点中的条目数量通常会很少,因此哈希表似乎没有必要。但是,由于我有排序列表,看起来我应该能够比未排序列表更有效地合并它们。这是我要问的问题。至于遍历节点,我显然只查看包含我想与之相交的对象的那些。每个节点都有一个最小和最大索引;如果对象的索引在最小值和最大值之间,我检查节点是否包含对象。等等。
  • 你是对的,这是元素的移动,而不是实际的重新分配需要时间。这个我以前也想过,后来忘记了。
  • 好吧,你可以利用这些数组通过合并来排序,不过是a bit more complex,我相信hashtable可能还更快。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-09
  • 1970-01-01
  • 1970-01-01
  • 2017-08-09
  • 1970-01-01
相关资源
最近更新 更多