簡體   English   中英

在排序數組中查找插入點的速度比O(n)還快?

[英]Finding insertion points in a sorted array faster than O(n)?

這是用於游戲編程的。 可以說我有一個可以追蹤其范圍內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之間?

我覺得您在這里問錯了問題。 您必須首先找到要插入的位置,然后再插入元素。 這是兩個捆綁在一起的操作,我覺得您不應該問如何找到一個可以在沒有其他方法的情況下更快地執行操作的方法。 為什么要在問題的結尾處講道理。 但我要解決的是實際插入速度更快的問題。

簡短答案:

答案是您會從對自己太聰明的人那里得到:

實現此目的的唯一方法是不使用數組。 在數組中,除非您要插入第一個或最后一個權限,否則插入將為O(n)。 這是因為數組由占據內存中連續空間的元素組成。 這樣一來,您便可以在O(1)時間內引用特定元素,而您確切地知道該元素在哪里。 成本是插入到中間,您需要移動數組中一半的元素。 因此,盡管您可以在log(n)時間中進行二進制搜索來查找,但您無法在該時間內插入。

因此,如果您要執行任何操作,則將需要其他數據結構。 一個簡單的二叉樹可能是解決方案,它將在log(n)時間中進行插入。 另一方面,如果要給它排序的數組,則必須擔心樹的平衡,因此您可能不需要紅色和黑色的樹。 或者,如果始終彈出最接近或最遠的元素,則可以使用堆排序。 堆排序是優先級隊列的最佳算法。 將樹結構裝配到數組中還具有另一個優勢,因此它具有更好的空間局部性(稍后會對此進行更多介紹)。

真相:

您極有可能在附近最多有十二個敵人或幾十個敵人。 在該級別上,漸進性能並不重要,因為它是專為'n'的較大值而設計的。 您正在尋找的是對CS 201教授關於“大哦”的呼吁的宗教依從性。 線性搜索和插入將是最快的方法,而擴展的答案是,到底誰在乎。 如果您嘗試實現復雜的算法以進行擴展,則幾乎總是會變慢,因為確定速度的不是軟件,而是硬件,因此最好還是堅持做硬件知道如何做的事情。處理得好:“線性下降內存”。 實際上,在預取器完成任務之后,即使有成千上萬個元素,線性遍歷每個元素也比實現一棵紅黑樹要快得多。 因為像樹這樣的數據結構會在整個位置分配內存,而不用考慮空間位置。 而且,為節點分配更多內存的調用本身比讀取一千個元素所花費的時間更昂貴。 這就是圖形卡在各處使用插入排序的原因。

堆排序

堆排序實際上可能更快,具體取決於輸入數據,因為它使用的是線性數組,盡管它可能會使預取器感到困惑,所以很難說。 唯一的限制是您只能彈出最高優先級的元素。 顯然,您可以將最高優先級定義為最低或最大元素。 堆排序對於我來說太花哨了,因此無法嘗試在此處進行描述,僅是Google。 它將插入和刪除操作分別分為兩個O(log(n))操作。 堆排序的最大缺點是,它將嚴重降低代碼的可調試性。 堆不是排序數組,它有一個順序,但是除了堆排序是一種復雜的,不直觀的算法之外,如果正確設置了堆,則對於人類來說顯然不可見。 因此,在最佳情況下,您會引入更多錯誤,但收效甚微。 地獄,上一次我必須進行堆排序時,我為其復制了代碼,並且其中包含錯誤。

二進制搜索插入排序

這就是您要嘗試執行的操作。 事實是,這是一個非常糟糕的主意。 平均而言,插入排序需要O(n)。 我們知道這是將隨機元素插入已排序數組的硬限制。 是的,我們可以使用二進制搜索來更快地找到要插入的元素。 但是平均插入仍然需要O(n)。 另外,在最佳情況下,如果要插入並且元素進入最后一個位置,則插入排序需要O(1)時間,因為插入時它已經在正確的位置。 但是,如果您執行二進制搜索來找到插入位置,那么找出您應該插入的最后一個位置將花費O(log(n))時間。 並且插入本身需要O(1)時間。 因此,在嘗試對其進行優化時,會嚴重降低最佳案例的性能。 查看您的用例,此隊列將敵人置於優先位置。 敵人的優先級可能取決於他們的力量和距離。 這意味着當敵人進入優先級隊列時,它的優先級可能會非常低。 這對於插入O(1)性能的最佳情況起到了很好的作用。 如果降低最佳案例性能,則弊大於利,因為這也是您最普遍的案例。

預優化是萬惡之源-唐納德·努斯(Donald Knuth)

由於您一直都在維護排序的搜索池,因此可以使用二進制搜索。 首先檢查中間元素,然后檢查中間元素與數組中較近端之間的中間位置,依此類推,直到找到位置。 這將給您O(log 2 n )時間。

當然,假設您使用數組類型來容納列表,這真的很容易。

我將假定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)});
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM