如何有效(性能)从Java中的List中删除许多项?

时间:2010-01-11 18:06:48

标签: java performance list collections

我有很大的List命名项(> = 1,000,000项)和一些条件由< cond>表示选择要删除的项目和< cond>对于我名单上的许多(可能是一半)项目都是如此。

我的目标是有效删除< cond>所选项目。并保留所有其他项目,可以修改源列表,可以创建新列表 - 应该考虑性能来选择最佳方法。

这是我的测试代码:

    System.out.println("preparing items");
    List<Integer> items = new ArrayList<Integer>(); // Integer is for demo
    for (int i = 0; i < 1000000; i++) {
        items.add(i * 3); // just for demo
    }

    System.out.println("deleting items");
    long startMillis = System.currentTimeMillis();
    items = removeMany(items);
    long endMillis = System.currentTimeMillis();

    System.out.println("after remove: items.size=" + items.size() + 
            " and it took " + (endMillis - startMillis) + " milli(s)");

和幼稚的实施:

public static <T> List<T> removeMany(List<T> items) {
    int i = 0;
    Iterator<T> iter = items.iterator();
    while (iter.hasNext()) {
        T item = iter.next();
        // <cond> goes here
        if (/*<cond>: */i % 2 == 0) {
            iter.remove();
        }
        i++;
    }
    return items;
}

如您所见,我使用项目索引模2 == 0作为删除条件(&lt; cond&gt;) - 仅用于演示目的。

可能会提供更好的removeMany版本以及为什么这个更好的版本实际上更好?

12 个答案:

答案 0 :(得分:37)

答案 1 :(得分:11)

正如其他人所说,你的第一个倾向应该是建立第二个清单。

但是,如果您还想尝试就地编辑列表,那么有效的方法是使用Guava中的Iterables.removeIf()。如果它的参数是一个列表,它会将保留的元素合并到前面,然后简单地将其删除 - 比逐个删除()内部元素要快得多。

答案 2 :(得分:6)

ArrayList中删除大量元素是O(n^2)操作。我建议只使用一个LinkedList,它更适合插入和删除(但不适用于随机访问)。 LinkedList有一点内存开销。

如果您确实需要保留ArrayList,那么最好创建一个新列表。

更新:与创建新列表相比:

重用相同的列表,主要成本来自删除节点并更新LinkedList中的相应指针。这是任何节点的常量操作。

构建新列表时,主要成本来自创建列表和初始化数组条目。两者都是廉价的操作。您可能还要承担调整新列表后端阵列大小的成本;假设最后一个数组大于传入数组的一半。

因此,如果您只删除一个元素,那么LinkedList方法可能会更快。如果要删除除1之外的所有节点,则新列表方法可能更快。

引入内存管理和GC时会出现更多复杂情况。我想把它们留下来。

最好的选择是自己实施替代方案,并在运行典型负载时对结果进行基准测试。

答案 3 :(得分:5)

我会创建一个新的List来添加项目,因为从列表中间删除项目非常昂贵。

public static List<T> removeMany(List<T> items) {
    List<T> tempList = new ArrayList<T>(items.size()/2); //if about half the elements are going to be removed
    Iterator<T> iter = items.iterator();
    while (item : items) {
        // <cond> goes here
        if (/*<cond>: */i % 2 != 0) {
            tempList.add(item);
        }
    }
    return tempList;
}
编辑:我没有对此进行测试,因此可能会出现很小的语法错误。

第二次编辑:当您不需要随机访问但快速添加时,使用LinkedList会更好。

<强> BUT ...

ArrayList的常数因子小于LinkedListRef)的常数因子。既然你可以合理地猜测将删除多少元素(在你的问题中你说“大约一半”),只要你不知道,在ArrayList的末尾添加一个元素是O(1)必须重新分配它。因此,如果您可以做出合理的猜测,我希望ArrayList在大多数情况下比LinkedList略快一些。 (这适用于我发布的代码。在你天真的实现中,我认为LinkedList会更快。)

答案 4 :(得分:2)

我认为构建一个新列表而不是修改现有列表会更有效 - 特别是当项目数量与您指示的一样大时。这假设您的列表是ArrayList,而不是LinkedList。对于非循环LinkedList,插入是O(n),但在现有迭代器位置的移除是O(1);在这种情况下,您的天真算法应该具有足够的性能。

除非列表是LinkedList,否则每次调用remove()时转移列表的成本可能是实施中最昂贵的部分之一。对于数组列表,我会考虑使用:

public static <T> List<T> removeMany(List<T> items) {
    List<T> newList = new ArrayList<T>(items.size());
    Iterator<T> iter = items.iterator();
    while (iter.hasNext()) {
        T item = iter.next();
        // <cond> goes here
        if (/*<cond>: */i++ % 2 != 0) {
            newList.add(item);
        }
    }
    return newList;
}

答案 5 :(得分:2)

对不起,但所有这些答案都没有提到,我想:你可能没有,也可能不应该使用List。

如果这种“查询”很常见,为什么不构建一个有序的数据结构,无需遍历所有数据节点?你没有告诉我们关于这个问题的充分信息,但考虑到你提供一个简单树的例子可以解决问题。每个项目都有一个插入开销,但您可以非常快速地找到包含匹配节点的子树,因此您可以避免现在正在进行的大部分比较。

此外:

  • 根据确切的问题以及您设置的确切数据结构,您可以加快删除速度 - 如果要杀死的节点确实减少到子树或某种类型,您只需删除该子树,而不是更新一大堆列表节点。

  • 每次删除列表项时,您都在更新指针 - 例如 lastNode.next nextNode.prev 或其他东西 - 但是如果它转了如果您还想删除 nextNode ,那么您刚刚引起的指针更新将被新更新丢弃。)

答案 6 :(得分:1)

您可以尝试的一件事是使用LinkedList而不是ArrayList,与ArrayList一样,如果从列表中删除元素,则需要复制所有其他元素。

答案 7 :(得分:1)

使用Apache Commons Collections。具体来说是this function。这实现方式与人们建议您实现它的方式基本相同(即创建新列表然后添加到其中)。

答案 8 :(得分:1)

由于速度是最重要的指标,因此可以使用更多内存并减少列表重新创建(如我的评论中所述)。但实际的性能影响完全取决于功能的使用方式。

该算法假设至少满足下列条件之一:

  • 不需要测试原始列表的所有元素。如果我们真的在寻找符合我们条件的前N个元素,而不是符合我们条件的所有元素,就会发生这种情况。
  • 将列表复制到新内存中的成本更高。如果原始列表使用超过50%的已分配内存,则可能会发生这种情况,因此就地工作可能会更好,或者内存操作变得更慢(这将是意外结果)。
  • 从列表中删除元素的速度惩罚太大而不能同时接受所有内容,但是在多个操作中传播该惩罚是可以接受的,即使总罚分大于一次全部罚分。这就像拿出20万美元的抵押贷款一样:30美元每月支付1000美元,每月可以负担得起,并拥有拥有住房和股权的好处,即使在贷款期限内总支付额为360K。

免责声明:语法错误很多 - 我没有尝试编译任何东西。

首先,继承ArrayList

public class ConditionalArrayList extends ArrayList {

  public Iterator iterator(Condition condition)
  { 
    return listIterator(condition);
  }

  public ListIterator listIterator(Condition condition)
  {
    return new ConditionalArrayListIterator(this.iterator(),condition); 
  }

  public ListIterator listIterator(){ return iterator(); }
  public iterator(){ 
    throw new InvalidArgumentException("You must specify a condition for the iterator"); 
  }
}

然后我们需要辅助类:

public class ConditionalArrayListIterator implements ListIterator
{
  private ListIterator listIterator;
  Condition condition;

  // the two following flags are used as a quick optimization so that 
  // we don't repeat tests on known-good elements unnecessarially.
  boolean nextKnownGood = false;
  boolean prevKnownGood = false;

  public ConditionalArrayListIterator(ListIterator listIterator, Condition condition)
  {
    this.listIterator = listIterator;
    this.condition = condition;
  }

  public void add(Object o){ listIterator.add(o); }

  /**
   * Note that this it is extremely inefficient to 
   * call hasNext() and hasPrev() alternatively when
   * there's a bunch of non-matching elements between
   * two matching elements.
   */
  public boolean hasNext()
  { 
     if( nextKnownGood ) return true;

     /* find the next object in the list that 
      * matches our condition, if any.
      */
     while( ! listIterator.hasNext() )
     {
       Object next = listIterator.next();
       if( condition.matches(next) ) {
         listIterator.set(next);
         nextKnownGood = true;
         return true;
       }
     }

     nextKnownGood = false;
     // no matching element was found.
     return false;
  }

  /**
   *  See hasPrevious for efficiency notes.
   *  Copy & paste of hasNext().
   */
  public boolean hasPrevious()
  { 
     if( prevKnownGood ) return true;

     /* find the next object in the list that 
      * matches our condition, if any.
      */
     while( ! listIterator.hasPrevious() )
     {
       Object prev = listIterator.next();
       if( condition.matches(prev) ) {
         prevKnownGood = true;
         listIterator.set(prev);
         return true;
       }
     }

     // no matching element was found.
     prevKnwonGood = false;
     return false;
  }

  /** see hasNext() for efficiency note **/
  public Object next()
  {
     if( nextKnownGood || hasNext() ) 
     { 
       prevKnownGood = nextKnownGood;
       nextKnownGood = false;
       return listIterator.next();
     }

     throw NoSuchElementException("No more matching elements");
  }

  /** see hasNext() for efficiency note; copy & paste of next() **/
  public Object previous()
  {
     if( prevKnownGood || hasPrevious() ) 
     { 
       nextKnownGood = prevKnownGood;
       prevKnownGood = false;
       return listIterator.previous();                        
     }
     throw NoSuchElementException("No more matching elements");
  }

  /** 
   * Note that nextIndex() and previousIndex() return the array index
   * of the value, not the number of results that this class has returned.
   * if this isn't good for you, just maintain your own current index and
   * increment or decriment in next() and previous()
   */
  public int nextIndex(){ return listIterator.previousIndex(); }
  public int previousIndex(){ return listIterator.previousIndex(); }

  public remove(){ listIterator.remove(); }
  public set(Object o) { listIterator.set(o); }
}

当然,我们需要条件接口:

/** much like a comparator... **/
public interface Condition
{
  public boolean matches(Object obj);
}

以及测试的条件

public class IsEvenCondition {
{
  public boolean matches(Object obj){ return (Number(obj)).intValue() % 2 == 0;
}

我们终于准备好了一些测试代码


    Condition condition = new IsEvenCondition();

    System.out.println("preparing items");
    startMillis = System.currentTimeMillis();
    List<Integer> items = new ArrayList<Integer>(); // Integer is for demo
    for (int i = 0; i < 1000000; i++) {
        items.add(i * 3); // just for demo
    }
    endMillis = System.currentTimeMillis();
    System.out.println("It took " + (endmillis-startmillis) + " to prepare the list. ");

    System.out.println("deleting items");
    startMillis = System.currentTimeMillis();
    // we don't actually ever remove from this list, so 
    // removeMany is effectively "instantaneous"
    // items = removeMany(items);
    endMillis = System.currentTimeMillis();
    System.out.println("after remove: items.size=" + items.size() + 
            " and it took " + (endMillis - startMillis) + " milli(s)");
    System.out.println("--> NOTE: Nothing is actually removed.  This algorithm uses extra"
                       + " memory to avoid modifying or duplicating the original list.");

    System.out.println("About to iterate through the list");
    startMillis = System.currentTimeMillis();
    int count = iterate(items, condition);
    endMillis = System.currentTimeMillis();
    System.out.println("after iteration: items.size=" + items.size() + 
            " count=" + count + " and it took " + (endMillis - startMillis) + " milli(s)");
    System.out.println("--> NOTE: this should be somewhat inefficient."
                       + " mostly due to overhead of multiple classes."
                       + " This algorithm is designed (hoped) to be faster than "
                       + " an algorithm where all elements of the list are used.");

    System.out.println("About to iterate through the list");
    startMillis = System.currentTimeMillis();
    int total = addFirst(30, items, condition);
    endMillis = System.currentTimeMillis();
    System.out.println("after totalling first 30 elements: total=" + total + 
            " and it took " + (endMillis - startMillis) + " milli(s)");

...

private int iterate(List<Integer> items, Condition condition)
{
  // the i++ and return value are really to prevent JVM optimization
  // - just to be safe.
  Iterator iter = items.listIterator(condition);
  for( int i=0; iter.hasNext()); i++){ iter.next(); }
  return i;
}

private int addFirst(int n, List<Integer> items, Condition condition)
{
  int total = 0;
  Iterator iter = items.listIterator(condition);
  for(int i=0; i<n;i++)
  {
    total += ((Integer)iter.next()).intValue();
  }
}

答案 9 :(得分:0)

也许列表不是您的最佳数据结构?你能改变吗?也许您可以使用一个树,其中项目的排序方式是删除一个节点删除满足条件的所有项目?或者至少可以加快您的运营速度?

在简单的示例中,使用两个列表(一个包含i%2!= 0的项为真,另一个包含i%2!= 0的项为false)可以很好地服务。但这当然非常依赖于域名。

答案 10 :(得分:0)

而不是混淆我的第一个答案,这已经很长了,这是第二个相关的选项:你可以创建自己的ArrayList,并将事物标记为“已删除”。这个算法做出了假设:

  • 在施工期间浪费时间(较低的速度)比在拆除操作期间做的更好。换句话说,它将速度惩罚从一个位置移动到另一个位置。
  • 现在最好浪费内存,并在计算结果后计算垃圾,而不是花时间在前面(你总是被时间垃圾收集困住......)。
  • 一旦删除开始,元素永远不会被添加到列表中(否则重新分配flags对象会出现问题)

此外,这也是未经测试的,因此存在prlolly语法错误。

public class FlaggedList extends ArrayList {
  private Vector<Boolean> flags = new ArrayList();
  private static final String IN = Boolean.TRUE;  // not removed
  private static final String OUT = Boolean.FALSE; // removed
  private int removed = 0;

  public MyArrayList(){ this(1000000); }
  public MyArrayList(int estimate){
    super(estimate);
    flags = new ArrayList(estimate);
  }

  public void remove(int idx){
    flags.set(idx, OUT);
    removed++;
  }

  public boolean isRemoved(int idx){ return flags.get(idx); }
}

和迭代器 - 可能需要更多工作来保持同步,这次省去了许多方法:

public class FlaggedListIterator implements ListIterator
{
  int idx = 0;

  public FlaggedList list;
  public FlaggedListIterator(FlaggedList list)
  {
    this.list = list;
  }
  public boolean hasNext() {
    while(idx<list.size() && list.isRemoved(idx++)) ;
    return idx < list.size();
  }
}

答案 11 :(得分:-6)

尝试在算法中实现递归。