8

If I understood correctly (and please correct me if i'm wrong), list is implemented by array in .NET, which means that every deletion of an item in the list will cause re-allocation of all the list (which in turn means O(n)).

I'm developing a game, in the game i have many bullets fly in the air on any giving moment, let's say 100 bullets, each frame I move them by few pixels and check for collision with objects in the game, I need to remove from the list every bullet that collided.

So I collect the collided bullet in another temporary list and then do the following:

foreach (Bullet bullet in bulletsForDeletion)
    mBullets.Remove(bullet);

Because the loop is O(n) and the remove is O(n), I spend O(n^2) time to remove.

Is there a better way to remove it, or more suitable collection to use?

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
OopsUser
  • 4,642
  • 7
  • 46
  • 71

7 Answers7

8

Sets and linked lists both have constant time removal. Can you use either of those data structures?

There's no way to avoid the O(N) cost of removal from List<T>. You'll need to use a different data structure if this is a problem for you. It may make the code which calculates bulletsToRemove feel nicer too.

ISet<T> has nice methods for calculating differences and intersections between sets of objects.

You lose ordering by using sets, but given you are taking bullets, I'm guessing that is not an issue. You can still enumerate it in constant time.


In your case, you might write:

mBullets.ExceptWith(bulletsForDeletion);
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • How "set" called in .net ? i don't see any generic called set. The problem with linked list (and sets) is that the data doesn't sit on the same spot in the memory, so when i read from a List, i have the benefit of caching, but when i use sets/linked lists i loose this benefit. Although i have to remove items from the list about once a second, i need to read from the list 100 time more (collision checking etc..) – OopsUser Dec 29 '12 at 13:44
  • @OopsUser, you're looking for `ISet`, which you can [read about here](http://msdn.microsoft.com/en-us/library/dd412081.aspx). – Drew Noakes Dec 29 '12 at 14:08
  • @OopsUser, also, it would be useful to see how you calculate `bulletsForDeletion`. Perhaps there's a way of making that more optimal too. – Drew Noakes Dec 29 '12 at 14:11
  • Here is the code : foreach (Bullet bullet in mBullets) { foreach (Player player in players) { if (player.Location.Intersects(bullet.Location) { bulletsForDeletion.Add(bullet); player.HP-=1; } } } – OopsUser Dec 29 '12 at 14:19
6

Create a new list:

var newList = `oldList.Except(deleteItems).ToList()`.

Try to use functional idioms wherever possible. Don't modify existing data structures, create new ones.

This algorithm is O(N) thanks to hashing.

usr
  • 168,620
  • 35
  • 240
  • 369
  • Why is it O(n) ? when he copies the variable to another list he need to check for each item if he exist in the "deleteItems" list, it looks like O(n^2) – OopsUser Dec 29 '12 at 13:41
  • @OopsUser `Except` builds a hash table internally, so the exists check is O(1) per item in `oldList`. That makes for `O(oldList.Count + deleteItems.Count) ~ O(N)` – usr Dec 29 '12 at 14:11
  • +1. @OopsUser, the [`Except` extension method](http://msdn.microsoft.com/en-us/library/system.linq.enumerable.except.aspx) reads every item from the source enumeration and outputs those which are not in `deleteItems`. Internally it builds a hash of `deleteItems`, and tests the presence of each input against that hash. Only items which are not found in the hash are passed through to the output. This is O(N), however it will result in allocating new hashes and new arrays (created by `ToList`). It may be that you don't need to worry about this memory overhead, but you should know that it's there. – Drew Noakes Dec 29 '12 at 14:17
  • 1
    Thanks, this is exactly what i wanted, because i think it's better not to change list to Set or LinkedList so i'll have the best performance while reading from it. – OopsUser Dec 29 '12 at 15:12
2

Can't you just switch from List (equivalent of Java's ArrayList to highlight that) to LinkedList? LinkedList takes O(1) to delete a specific element, but O(n) to delete by index

usr-local-ΕΨΗΕΛΩΝ
  • 26,101
  • 30
  • 154
  • 305
1

How a list is implemented internally is not something you should be thinking about. You should be interacting with the list in that abstraction level.

If you do have performance problems and you pinpoint them to the list, then it is time to look at how it is implemented.

As for removing items from a list - you can use mBullets.RemoveAll(predicate), where predicate is an expression that identifies items that have collided.

Oded
  • 489,969
  • 99
  • 883
  • 1,009
1

RemoveAt(int index) is faster than Remove(T item) because the later use the first inside it, doing reflection, this is the code inside each function.

Also, Remove(T) has the function IndexOf, which has inside it a second loop to evalute the index of each item.

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;
}

I would do a loop like this:

for (int i=MyList.Count-1; i>=0; i--) 
{
    // add some code here
    if (needtodelete == true)
    MyList.RemoveAt(i);
}
sharp12345
  • 4,420
  • 3
  • 22
  • 38
  • Your last code block is an inefficient version of `MyList.Clear()`. Did you mean to write something else? – Drew Noakes Dec 29 '12 at 14:12
  • @DrewNoakes , Yes, I assume that the asking user will add few conditions to delete some items, he will not actually be deleting all items, edited my post. – sharp12345 Dec 29 '12 at 14:17
1

Ideally, it shouldn't be important to think about how C# implements its List methods, and somewhat unfortunate that it reassigns all the array elements after index i when doing somelist.RemoveAt(i). Sometimes optimizations around standard library implementations are necessary, though.

If you find your execution is expending unacceptable effort shuffling list elements around when removing them from a list with RemoveAt, one approach that might work for you is to swap it with the end, and then RemoveAt off the end of the list:

if (i < mylist.Count-1) {
     mylist[i] = mylist[mylist.Count-1];
}
mylist.RemoveAt(mylist.Count-1);

This is of course an O(1) operation.

Mars
  • 195
  • 1
  • 10
0

In the spirit of functional idiom suggested by "usr", you might consider not removing from your list at all. Instead, have your update routine take a list and return a list. The list returned contains only the "still-alive" objects. You can then swap lists at the end of the game loop (or immediately, if appropriate). I've done this myself.

Griffin
  • 1,586
  • 13
  • 24