在排序数组中查找插入点的速度比O(n)快吗?

时间:2015-04-20 22:09:26

标签: c# arrays performance insert

这适用于游戏编程。假设我有一个单位可以跟踪其范围内的10个敌人。每个敌人的优先级在0-100之间。所以数组目前看起来像这样(数字代表优先级):

Enemy - 96
Enemy - 78
Enemy - 77
Enemy - 73
Enemy - 61
Enemy - 49
Enemy - 42
Enemy - 36
Enemy - 22
Enemy - 17

假设一个新的敌人在范围内徘徊并且优先级为69,这将插入7361之间,并且17将从数组中移除(好吧,在插入之前,17会被删除,我相信)。

有没有办法弄清楚它需要在没有O(n)操作的7361之间插入?

3 个答案:

答案 0 :(得分:2)

我觉得你在这里问错了问题。您必须先找到要插入的点然后插入元素。这两个操作都捆绑在一起,我觉得你不应该问如何在没有另一个的情况下更快地找到哪一个。在问题结束时,为什么会有意义。但我正在解决实际插入速度更快的问题。

简答:

回答你会从那些对自己太聪明的人那里得到答案:

实现此目的的唯一方法是不使用数组。在数组中除非您插入第一个或最后一个权限,否则插入将为O(n)。这是因为数组由占据内存中连续空间的元素组成。这就是你能够在O(1)时间内引用特定元素的方式,你知道该元素的确切位置。成本是插入中间,你需要移动数组中的一半元素。因此,虽然您可以在log(n)时间内查找二进制搜索,但您无法在该时间内插入。

因此,如果您要做任何事情,那么您需要一个不同的数据结构。一个简单的二叉树可能是它将在log(n)时间内插入的解决方案。另一方面,如果你正在为它排序一个排序的数组,你必须担心树平衡,所以不是你可能需要一棵红色和黑色的树。或者,如果您总是弹出最接近或最远的元素,那么您可以使用堆排序。堆排序是优先级队列的最佳算法。它还具有在数组中拟合树结构的额外优势,因此它具有更好的空间局部性(稍后将详细介绍)。

真相:

你最多可能在附近有十几个或几十个敌人。在该级别,渐近性能无关紧要,因为它是专门为大值的“n”而设计的。你正在看的是对你的CS 201教授关于Big Oh的电话的宗教信仰。线性搜索和插入将是最快的方法,它的扩展答案是,谁在乎。如果你试图实现一个复杂的算法来扩展它,那么你几乎总是会变慢,因为确定你的速度不是软件,而是硬件,你最好坚持做硬件知道的事情。如何妥善处理:"线性降低记忆"。事实上,在prefetchers做了他们的事情之后,即使有几千个元素而不是实现一棵红色和黑色的树,线性地穿过每个元素会更快。因为像树一样的数据结构会在整个地方分配内存,而不考虑空间局部性。为节点分配更多内存的调用本身比读取一千个元素所花费的时间更昂贵。这就是显卡在整个地方使用插入排序的原因。

堆排序

堆排序实际上可能更快,具体取决于输入数据,因为它使用线性阵列,尽管它可能会混淆预取器,因此很难说。唯一的限制是您只能弹出优先级最高的元素。显然,您可以将最高优先级定义为最低或最大元素。堆排序对我来说太花哨了,试图在这里描述它,只是谷歌它。它确实将插入和删除分成两个O(log(n))操作。堆排序的最大缺点是它将严重降低代码的可调试性。堆不是一个排序数组,它有一个顺序,但除了堆排序是一个复杂的非直观算法,如果正确设置堆,它对人类来说显然是不可见的。所以你会在最好的情况下引入更多的bug,效果不大。地狱,我最后一次做堆排序时,我复制了它的代码,并且里面有bug。

使用二进制搜索进行插入排序

所以这就是你想要做的事情。事实是,这是一个非常糟糕的主意。平均插入排序需要O(n)。我们知道这是将随机元素插入排序数组的硬限制。是的,我们可以通过使用二进制搜索找到我们想要更快插入的元素。但是平均插入仍然需要O(n)。或者,在最好的情况下,如果要插入并且元素进入最后位置,则插入排序需要O(1)时间,因为插入时它已经在正确的位置。但是,如果您进行二进制搜索以找到插入位置,那么找出您应该在最后位置插入的时间需要O(log(n))时间。插入本身需要O(1)时间。因此,在尝试优化它时,您已经严重降低了最佳案例性能。查看您的用例,此队列以其优先级保存敌人。敌人的优先权很可能是他们的力量和距离的函数。这意味着当敌人进入优先级队列时,它可能具有非常低的优先级。这非常适合插入O(1)性能的最佳情况。如果你降低最佳案例表现,你会弊大于利,因为这也是你最常见的情况。

  

预先优化是所有邪恶的根源 - 唐纳德克努特

答案 1 :(得分:0)

由于您始终维护已排序的搜索池,因此您可以使用二进制搜索。首先检查中间元素,然后检查中间元素之间的中间元素和数组中哪一端更近,依此类推,直到找到位置。这将为您提供O(log 2 n )时间。

答案 2 :(得分:0)

当然,假设您使用数组类型来容纳列表,这非常容易。

我将假设Enemy是您的类名,并且有一个名为Priority的属性来执行排序。我们需要IComparer<Enemy>,如下所示:

public class EnemyComparer : IComparer<Enemy>
{
    int IComparer<Enemy>.Compare(Enemy x, Enemy y)
    {
        return y.Priority.CompareTo(x.Priority); // reverse operand to invert ordering
    }
}

然后我们可以编写一个简单的InsertEnemy例程,如下所示:

public static bool InsertEnemy(Enemy[] enemies, Enemy newEnemy)
{
    // binary search in O(logN)
    var ix = Array.BinarySearch(enemies, newEnemy, new EnemyComparer());
    // If not found, the bit-wise compliment is the insertion index
    if (ix < 0)
        ix = ~ix;
    // If the insertion index is after the list we bail out...
    if (ix >= enemies.Length)
        return false;// Insert is after last item...
    //Move enemies down the list to make room for the insertion...
    if (ix + 1 < enemies.Length)
        Array.ConstrainedCopy(enemies, ix, enemies, ix + 1, enemies.Length - (ix + 1));
    //Now insert the newEnemy into the position
    enemies[ix] = newEnemy;
    return true;
}

还有其他数据结构可以让它更快一些,但这应该足够有效。如果列表变大,B树或二叉树就可以了,但是对于10个项目来说,它会更快,这是值得怀疑的。

通过添加以下方法测试上述方法:

public class Enemy
{
    public int Priority;
}

public static void Main()
{
    var rand = new Random();
    // Start with a sorted list of 10
    var enemies = Enumerable.Range(0, 10).Select(i => new Enemy() {Priority = rand.Next(0, 100)}).OrderBy(e => e.Priority).ToArray();
    // Insert random entries
    for (int i = 0; i < 100; i++)
        InsertEnemy(enemies, new Enemy() {Priority = rand.Next(100)});
}