簡體   English   中英

從 IList 中刪除多個項目的最有效方法<t></t>

[英]Most efficient way to remove multiple items from a IList<T>

IList<T> object 中刪除多個項目的最有效方法是什么。假設我有一個包含所有要刪除的項目的IEnumerable<T> ,其出現順序與原始列表中的出現順序相同。

我想到的唯一方法是:

IList<T> items;
IEnumerable<T> itemsToDelete;
...

foreach (var x in itemsToDelete)
{
    items.Remove(x);
}

但我猜它效率不高,因為每次調用方法Remove時,它都必須從開始的列表開始 go。

隨着要刪除的項目數量變大,您可能會發現遍歷列表並根據“要刪除的項目”的哈希集檢查每個項目更有效。 像這樣的擴展方法可能會有所幫助:

static void RemoveAll<T>(this IList<T> iList, IEnumerable<T> itemsToRemove)
{
    var set = new HashSet<T>(itemsToRemove);

    var list = iList as List<T>;
    if (list == null)
    {
        int i = 0;
        while (i < iList.Count)
        {
            if (set.Contains(iList[i])) iList.RemoveAt(i);
            else i++;
        }
    }
    else
    {
        list.RemoveAll(set.Contains);
    }
}

我使用下面這個小程序進行基准測試。 (注意,如果IList<T>實際上是List<T> ,它使用優化路徑。)

在我的機器上(並使用我的測試數據),這個擴展方法執行需要1.5秒 ,而問題中的代碼需要17秒 但是,我還沒有測試過不同大小的數據。 我肯定只刪除幾個項目RemoveAll2會更快。

static class Program
{
    static void RemoveAll<T>(this IList<T> iList, IEnumerable<T> itemsToRemove)
    {
        var set = new HashSet<T>(itemsToRemove);

        var list = iList as List<T>;
        if (list == null)
        {
            int i = 0;
            while (i < iList.Count)
            {
                if (set.Contains(iList[i])) iList.RemoveAt(i);
                else i++;
            }
        }
        else
        {
            list.RemoveAll(set.Contains);
        }
    }

    static void RemoveAll2<T>(this IList<T> list, IEnumerable<T> itemsToRemove)
    {
        foreach (var item in itemsToRemove)
            list.Remove(item);
    }

    static void Main(string[] args)
    {
        var list = Enumerable.Range(0, 10000).ToList();
        var toRemove = new[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 
                              43,  47,  53,  59,  61,  67,  71,  73,  79,  83,  89,  97, 101,
                             103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167,
                             173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239,
                             241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313,
                             317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397,
                             401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467,
                             479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569,
                             571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643,
                             647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733,
                             739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823,
                             827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911,
                             919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997};
        list.RemoveAll(toRemove); // JIT 
        //list.RemoveAll2(toRemove); // JIT 

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < 10000; i++)
        {
            list.RemoveAll(toRemove);
            //list.RemoveAll2(toRemove);
        }
        sw.Stop();
        Console.WriteLine("Elapsed: {0} ms", sw.ElapsedMilliseconds);
        Console.ReadKey();
    }
}

更新 (對於@ KarmaEDV和Mark Sowul的評論如下):如果你需要使用自定義相等比較器,擴展方法可能會有一個帶有這樣一個比較器的重載:

public static void RemoveAll<T>(this IList<T> iList, IEnumerable<T> itemsToRemove, IEqualityComparer<T> comparer = null)
{
    var set = new HashSet<T>(itemsToRemove, comparer ?? EqualityComparer<T>.Default);

    if (iList is List<T> list)
    {
        list.RemoveAll(set.Contains);
    }
    else
    {
        int i = iList.Count - 1;
        while (i > -1)
        {
            if (set.Contains(iList[i])) iList.RemoveAt(i);
            else i--;
        }
    }
}

如果IList<T>引用恰好引用List<T>的實例,則轉換為該類型並使用RemoveAll比不依賴於其實現細節的任何其他方法更容易產生更好的性能。

否則,雖然最佳方法將取決於將要刪除的項目的相對比例和IList<T>的性質,但我建議您最好的選擇是將IList<T>復制到新List<T> ,清除它,並有選擇地重新添加項目。 即使列表中的項目不利於有效散列, IEnumerable<T>中的項目與IList<T>中的項目的順序相同也會使其無關緊要。 首先從IEnumerable<T>讀取一個項目。 然后將數組中的項目復制到列表中,直到找到該項目。 然后從IEnumerable<T>讀取下一個項目並從數組復制到列表,直到找到那個,等等。一旦IEnumerable<T>耗盡,將數組的余額復制到List<T>

對於IList<T>許多實現,這種方法會很快。 但它有一個主要的缺點:它刪除並重新添加每個項目的事實可能會對可觀察列表之類的東西產生不必要的副作用。 如果列表可能是可觀察的,則可能必須使用更慢的N ^ 2算法來確保正確性。 [順便說一句,我覺得IList<T>有一個Remove(T)方法,但缺少一個更有用的RemoveAll(Func<T,bool>)方法。 對於IndexOfRemoveAtRemove(T)在很大程度上是多余的,而如果不允許刪除和重新添加項目,則RemoveAll將允許O(N)實現多個O(N ^ 2)的操作。

也許這有幫助。 可以包括相同類型的其他想法。

IList<T> items;

IEnumerable<T> itemsToDelete;
...
{
   if(items.Equals(itemsToDelete)) //Equal lists?
     {
      items.Clear(); 
      return true;
     }


   if(  (double) items.Count/itemsToDelete.Count < 1){
      /* It is faster to iterate the small list first. */ 
              foreach (var x in items)
              {
                if(itemsToDelete.Contains(x)){/**/} 

              }
    }
   else{
           foreach (var x in itemsToDelete)
              {
               items.Remove(x);
              }
   }
}

如果IList<T>接口有可用的擴展方法RemoveAll ,這個問題會更容易解決。 所以這是一個:

/// <summary>
/// Removes all the elements that match the conditions defined by the
/// specified predicate.
/// </summary>
public static int RemoveAll<T>(this IList<T> list, Func<T, int, bool> predicate)
{
    ArgumentNullException.ThrowIfNull(list);
    ArgumentNullException.ThrowIfNull(predicate);

    int i = 0, j = 0;
    try
    {
        for (; i < list.Count; i++)
        {
            if (predicate(list[i], i)) continue;
            if (j < i) list[j] = list[i];
            j++;
        }
    }
    finally
    {
        if (j < i)
        {
            for (; i < list.Count; i++, j++)
                list[j] = list[i];
            while (list.Count > j)
                list.RemoveAt(list.Count - 1);
        }
    }
    return i - j;
}

這是自定義List<T>.RemoveAll實現的修改版本,可在此答案中找到。 由於IList<T>接口中缺少RemoveRange方法,因此 IList<T IList<T>中最右邊的剩余槽會隨着最后一個元素的重復移除而被清除。 在大多數IList<T>實現中,這應該是一個相當快的操作。

現在可以像這樣有效地解決從IList<T>中刪除多個項目的原始問題:

IList<T> items;
IEnumerable<T> itemsToDelete;
//...

HashSet<T> itemsToDeleteSet = new(itemsToDelete);
items.RemoveAll((x, _) => itemsToDeleteSet.Contains(x));

在線演示

暫無
暫無

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

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