我有很大的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
版本以及为什么这个更好的版本实际上更好?
答案 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
的常数因子小于LinkedList
(Ref)的常数因子。既然你可以合理地猜测将删除多少元素(在你的问题中你说“大约一半”),只要你不知道,在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)
由于速度是最重要的指标,因此可以使用更多内存并减少列表重新创建(如我的评论中所述)。但实际的性能影响完全取决于功能的使用方式。
该算法假设至少满足下列条件之一:
免责声明:语法错误很多 - 我没有尝试编译任何东西。
首先,继承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,并将事物标记为“已删除”。这个算法做出了假设:
此外,这也是未经测试的,因此存在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)
尝试在算法中实现递归。