Как эффективно удалить из списка<T> (C#)?
Если я правильно понял (и, пожалуйста, поправьте меня, если я ошибаюсь), список реализуется массивом в.NET, что означает, что каждое удаление элемента в списке приведет к перераспределению всего списка (что, в свою очередь, означает, что O(n)
).
Я разрабатываю игру, в игре у меня много пуль, летающих в воздухе в любой момент, скажем, 100 пуль, каждый кадр я перемещаю их на несколько пикселей и проверяю на предмет столкновения с объектами в игре, мне нужно удалить из списка каждая пуля, которая столкнулась.
Поэтому я собираю столкнувшуюся пулю в другой временный список и затем делаю следующее:
foreach (Bullet bullet in bulletsForDeletion)
mBullets.Remove(bullet);
Потому что цикл O(n)
и удаление O(n)
, Я потратил O(n^2
время убрать.
Есть ли лучший способ удалить его или использовать более подходящую коллекцию?
7 ответов
Создать новый список:
var newList = `oldList.Except(deleteItems).ToList()`.
Попробуйте использовать функциональные идиомы везде, где это возможно. Не изменяйте существующие структуры данных, создавайте новые.
Этот алгоритм O(N) благодаря хешированию.
Наборы и связанные списки имеют постоянное время удаления. Можете ли вы использовать любую из этих структур данных?
Там нет никакого способа избежать O(N) стоимость удаления из List<T>
, Вам нужно будет использовать другую структуру данных, если это проблема для вас. Это может сделать код, который вычисляет bulletsToRemove, также более приятным.
ISet<T>
имеет хорошие методы для вычисления различий и пересечений между наборами объектов.
Вы теряете порядок, используя наборы, но, учитывая, что вы принимаете пули, я думаю, это не проблема. Вы все еще можете перечислить его в постоянное время.
В вашем случае вы можете написать:
mBullets.ExceptWith(bulletsForDeletion);
Вы не можете просто переключиться с List
(эквивалент Java ArrayList
выделить это) LinkedList
? LinkedList использует O(1) для удаления определенного элемента, но O(n) для удаления по индексу
То, как список реализован внутри, - это не то, о чем вы должны думать. Вы должны взаимодействовать со списком на этом уровне абстракции.
Если у вас есть проблемы с производительностью, и вы указываете их на список, самое время посмотреть, как это реализовано.
Что касается удаления элементов из списка - вы можете использовать mBullets.RemoveAll(predicate)
, где predicate
это выражение, которое идентифицирует предметы, которые столкнулись.
В идеале не должно быть важно думать о том, как C# реализует свои методы List, и несколько прискорбно, что он переназначает все элементы массива после индекса.
i
когда делаешь
somelist.RemoveAt(i).
Однако иногда необходима оптимизация реализаций стандартных библиотек.
Если вы обнаружите, что ваше выполнение требует неприемлемых усилий для перетасовки элементов списка при удалении их из списка с помощью RemoveAt, один подход, который может сработать для вас, — это поменять местами его с концом, а затем
RemoveAt
в конце списка:
if (i < mylist.Count-1) {
mylist[i] = mylist[mylist.Count-1];
}
mylist.RemoveAt(mylist.Count-1);
Это, конечно, операция O(1).
RemoveAt(int index)
быстрее чем Remove(T item)
потому что позже использовать первый внутри него, делая отражение, это код внутри каждой функции.
Также, Remove(T)
имеет функцию IndexOf
, который имеет внутри него второй цикл для оценки индекса каждого элемента.
public bool Remove(T item)
{
int index = this.IndexOf(item);
if (index < 0)
return false;
this.RemoveAt(index);
return true;
}
public void RemoveAt(int index)
{
if ((uint) index >= (uint) this._size)
ThrowHelper.ThrowArgumentOutOfRangeException();
--this._size;
if (index < this._size)
Array.Copy((Array) this._items, index + 1, (Array) this._items, index, this._size - index);
this._items[this._size] = default (T);
++this._version;
}
Я хотел бы сделать цикл, как это:
for (int i=MyList.Count-1; i>=0; i--)
{
// add some code here
if (needtodelete == true)
MyList.RemoveAt(i);
}
В духе функциональной идиомы, предложенной "usr", вы можете вообще не удалять из своего списка. Вместо этого пусть ваша процедура обновления возьмет список и вернет список. Возвращенный список содержит только "еще живые" объекты. Затем вы можете поменять списки в конце игрового цикла (или сразу, если это необходимо). Я сделал это сам.