【问题标题】:Efficient way to remove element from a list at specified indices从指定索引处的列表中删除元素的有效方法
【发布时间】:2016-12-10 03:53:38
【问题描述】:

假设我有一个包含 N 个元素的列表 (list1)。

然后我有一个 M list2) 中删除的元素的所有索引。

我想以最有效的方式使用list2 索引相应地删除list1 上的元素。

第一种方法是通过 for 循环:

for (int i = 0; i < list2.Count; i++)
    list1.RemoveAt(list2[i]);

这项工作非常好(当然,我需要订购 list2),但在最坏的情况下具有 O(M*N) 复杂度(M 次迭代,RemoveAt 为 O(n))

另一种解决方案是创建一个临时列表,用不应删除的元素填充它,然后使用 LINQ intersect 方法

List<T> tempList = new List<T>();
for (int i = 0; i < list1.Count; i++)
    if (list2.Contains(i))
        tempList.Add(list1[i]);
list1.Intersect(tempList);

虽然起初我对 Intersect 的 O(N+M) 复杂性感到兴奋,但我最终意识到我需要首先进行 N 次迭代来填充 tempList,然后所有优势都消失了(让我们假设list2 是HashSet 在第二种情况下,我不关心排序,而只关心 O(1) Contains)

经过一番挖掘,我仍然无法找到一种方法来执行类似RemoveAll 的方法,该方法将list1 中的所有元素都删除为list2 中存储的值。

有没有机会让它尽可能高性能?

PS:对于那些认为“过早的优化是万恶之源”的人,请考虑我的代码实际上运行良好,但是由于我正在处理一个严格的时间依赖问题,因此每次迭代节省了几个 ns (我将进行大约 150k 次迭代)可以带来显着的改进。

编辑:正如@InBetween 正确指出的那样,在第二个解决方案上使用Intersect 实际上是没有用的,降低它的复杂性会下降。

【问题讨论】:

  • 优化是乐趣的根源,恕我直言。早不早!那么我们可以假设list2 已经排序了吗?
  • 我们可以假设很多关于list2 的事情。我们可以假设它已排序,或者它是HashSet。我可以控制list2 以及它是如何创建的,所以我可以对它做很多有趣的事情。但我同意你的看法,优化很酷!
  • 你为什么要相交?保留新创建的列表。
  • @InBetween 因为起初它是一个例外,而不是一个交叉,然后我忘记了我在做什么,但你是对的(不过,这似乎不是最好的方法对我来说)
  • 第一种使用LinkedList 而不是List 的方法是O(n),因为删除操作变成了O(1)

标签: c# linq optimization


【解决方案1】:

如果订购了list2,则只需使用优化的第二个解决方案:

var exceptIndex = 0;
var newList = new List<T>();

for (var i = 0; i < list1.Length; i++)
{
    if (i != list2[exceptIndex]) newList.Add(list1[i]);
    else exceptIndex++
}

return newList;

【讨论】:

  • 谢谢。最后,我意识到我可以将您的解决方案合并到一个新的解决方案:将 list2 作为一个布尔数组,它说明哪些索引应该保留,哪些不应该保留。不过,这与我最初的问题相去甚远。
【解决方案2】:
List1 = Enumerable.Range(0,List1.Count).Except(List2).Select(i=>List1[i]).ToList()

bool[] shouldWeSayGoodbye = new bool[List1.Count];
for(var i=0;i<List2.Count;i++){
   shouldWeSayGoodbye[List2[i]]=true;
}

typeof_list1 List3 = new List<typeof_list1>();
for(var i=0;i<List1.Count;i++){
   if(!shouldWeSayGoodbye[i]){
      List3.Add(List1[i])
   }
}

一些测试表明循环至少击败 Linq x4 次。

【讨论】:

  • 非常酷的答案,非常晦涩。你介意解释一下为什么它是高性能的吗?
  • 因为删除一个元素会强制列表移动它之后的每个元素。生成新列表的速度要快得多。 stackoverflow.com/questions/14082042/…
  • @Fylax 这基本上是您的第二个解决方案的 LINQ 版本,删除了不必要的 Except
  • @InBetween 我知道这是什么(你已经指出有一个超出的Intersect 实际上没有意义,我同意你的观点),但我仍然不清楚为什么它更高效。正如我在问题中解释的那样,保存 ns 是一个巨大的改进,根据我的经验,LINQ 往往比 for 循环慢
  • 编辑后没有理由写新的答案。我的想法只是改变 list2 结构,让它从一开始就是你所说的shallWeSayGoodby。似乎是执行此任务的最有效和最干净的方式
【解决方案3】:

List&lt;T&gt;.Add实际上是O(1),如果不超过容量的话。 这是我想出的,应该去O(n)

List<T> resultList = new List<T>(list1.Count); // high capacity!
int curIdx = list1.Count - 1; // start at the end of list1

// assumes list2 is sorted descendingly
list2.Add(-1); // add a final -1 index to make following code nicer

foreach (int targetIdx in list2)
{
    while (curIdx > targetIdx)
    {
        resultList.Add(list1[curIdx]); // both operations are O(1)
        curIdx--;
    }
    curIdx--;
}

// resultList is reversed

澄清:

list1: [10, 11, 12, 13, 14]
list2: [1, 3]
想要的结果:[10,12,14]

我们希望对list2 进行降序排序,并为其添加最后的-1:

list2: [3, 1, -1]

最终结果实际上是相反的想要的结果。之后可以在O(n) 中反转列表,因此它不会改变整体复杂性,但是您可以进一步优化代码,以便最终列表实际上是正确的顺序(家庭作业!)

【讨论】:

  • 即使您在 foreach 中看到了一段时间 - 我们实际上只循环 n 个元素
  • 我猜,使用反向 for 循环会使我的最终列表按正确的顺序排列。但是最终的顺序实际上并不是什么大问题,正如我之前所说,我做了很多随机访问(注意随机),所以它只会增加熵。另外,我知道容量成本限制。我在我的代码中尝试了所有可能的技巧来提高性能:) 无论如何感谢大家,我找到了一个非常好的和有效的解决方案(尽管它极大地改变了 list2)
  • @Fylax,愿意分享您的解决方案吗?
  • 当然,这是我的计划。
【解决方案4】:

您最好的选择可能是坚持使用您的 for 循环。它们将是最快的,假设list2 以相反的顺序排序(例如)。

您可以尝试使用RemoveAll 或许...

list1.RemoveAll(l1item => list2.Contains(list1.IndexOf(l1item)));

缺点是,虽然这看起来更简洁明了,但它的基础实际上相当复杂。

public int RemoveAll(Predicate<T> match)
{
    if (match == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    int num = 0;
    while (num < this._size && !match(this._items[num]))
    {
        num++;
    }
    if (num >= this._size)
    {
        return 0;
    }
    int i = num + 1;
    while (i < this._size)
    {
        while (i < this._size && match(this._items[i]))
        {
            i++;
        }
        if (i < this._size)
        {
            this._items[num++] = this._items[i++];
        }
    }
    Array.Clear(this._items, num, this._size - num);
    int result = this._size - num;
    this._size = num;
    this._version++;
    return result;
}

另一方面,查看您当前正在使用的RemoveAt

public void RemoveAt(int index)
{
    if (index >= this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException();
    }
    this._size--;
    if (index < this._size)
    {
        Array.Copy(this._items, index + 1, this._items, index, this._size - index);
    }
    this._items[this._size] = default(T);
    this._version++;
}

这会在每次删除项目时执行Array.Copy,而RemoveAll 会处理项目并将索引项目向上移动到“已删除”项目。

您可以在这两个上运行一些简单的基准测试,看看哪个更好。

不过,使用以下方法可能会给您带来最好的结果:

for (int i = 0; i < list1.Count; i++)
{
   bool exists = false;
   for (int j = 0; j < list2.Count; j++)
      if (i == list2[j]) 
      {
         exists = true;
         break;
      }
   if (!exists) newList.Add(list1[i]);
}

这不应该对列表顺序有任何依赖。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-24
    • 2019-09-19
    相关资源
    最近更新 更多