[英]How to efficiently (performance) remove many items from List in Java?
我有很大的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作為刪除條件(<cond>) - 僅用於演示目的。
可以提供什么更好的removeMany
版本以及為什么這個更好的版本實際上更好?
好的,現在是提出方法測試結果的時候了。 這里我測試了哪些方法(每個方法的名稱在我的源代碼中也是類名):
NaiveRemoveManyPerformer
- 帶有迭代器和刪除的ArrayList
- 在我的問題中給出的第一個和天真的實現。 BetterNaiveRemoveManyPerformer
- 具有向后迭代並從端到端移除的ArrayList
。 LinkedRemoveManyPerformer
- 天真的迭代器和刪除但在LinkedList
工作。 Disadventage:僅適用於LinkedList
。 CreateNewRemoveManyPerformer
- 將ArrayList
作為副本(僅添加保留元素),迭代器用於遍歷輸入ArrayList
。 SmartCreateNewRemoveManyPerformer
- 更好的CreateNewRemoveManyPerformer
- 結果ArrayList
初始大小(容量)設置為最終列表大小。 缺點:啟動時必須知道列表的最終大小。 FasterSmartCreateNewRemoveManyPerformer
- 甚至更好(?) SmartCreateNewRemoveManyPerformer
- 使用項索引( items.get(idx)
)而不是迭代器。 MagicRemoveManyPerformer
- 為ArrayList
工作(沒有列表副本),並從列表末尾的項目開始壓縮孔(已刪除的項目)。 Disadventage:此方法更改列表中項目的順序。 ForwardInPlaceRemoveManyPerformer
- 為ArrayList
- 移動保留項以填充空洞,最后返回subList(無最終刪除或清除)。 GuavaArrayListRemoveManyPerformer
- Google Guava Iterables.removeIf
用於ArrayList
- 幾乎與ForwardInPlaceRemoveManyPerformer
相同,但最終刪除列表末尾的項目。 完整的源代碼在本答案的最后給出。
測試使用不同列表大小(從10,000個項目到10,000,000個項目)執行的測試和不同的刪除因子(指定必須從列表中刪除多少項目)。
正如我在評論中發布的其他答案 - 我認為將項目從ArrayList
復制到第二個ArrayList
將比迭代LinkedList
更快,只是刪除項目。 Sun的Java文檔說,與LinkedList
實現相比, ArrayList
常量因子較低,但令人驚訝的是,在我的問題中並非如此。
實際上,在大多數情況下,具有簡單迭代和刪除功能的LinkedList
具有最佳性能(此方法在LinkedRemoveManyPerformer
實現)。 通常只有MagicRemoveManyPerformer
性能可與LinkedRemoveManyPerformer
相媲美,其他方法都明顯變慢。 Google Guava GuavaArrayListRemoveManyPerformer
比手工制作的類似代碼慢(因為我的代碼不會刪除列表末尾的不必要的項目)。
從1,000,000個源項目中刪除500,000個項目的示例結果:
NaiveRemoveManyPerformer
:測試未執行 - 我不是那么耐心,但它的表現比BetterNaiveRemoveManyPerformer
更差。 BetterNaiveRemoveManyPerformer
:226080毫(s) LinkedRemoveManyPerformer
:69毫(s) CreateNewRemoveManyPerformer
:246毫(s) SmartCreateNewRemoveManyPerformer
:112毫(s) FasterSmartCreateNewRemoveManyPerformer
:202 milli(s) MagicRemoveManyPerformer
:74毫(s) ForwardInPlaceRemoveManyPerformer
:69毫(s) GuavaArrayListRemoveManyPerformer
:118 milli(s) 從1,000,000個源項目中刪除1個項目的示例結果(第一個項目已刪除):
從1,000,000個源項目中刪除333,334個項目的示例結果:
從1,000,000個源項目中刪除1,000,000(全部)項目的示例結果(所有項目都被刪除但是通過逐個處理,如果您事先知道要刪除所有項目,則應該簡單地清除列表):
我的最終結論是:使用混合方法 - 如果處理LinkedList - 簡單的迭代和刪除是最好的,如果處理ArrayList - 它取決於項目順序是否重要 - 然后使用ForwardInPlaceRemoveManyPerformer,如果項目順序可能被更改 - 最佳選擇是MagicRemoveManyPerformer。 如果刪除因子是先驗已知的(您知道將刪除多少項目與保留),那么可以選擇更多條件來選擇在特定情況下表現更好的方法。 但是已知的刪除因素不是常見的情況......谷歌Guava Iterables.removeIf
是這樣一個混合解決方案,但假設略有不同(原始列表必須更改,新的不能創建,項目順序總是重要) - 這些是最常見的假設所以removeIf
是大多數現實生活中的最佳選擇。
還要注意所有好方法(天真不好!)都足夠好 - 其中任何一個方法在實際應用中做得很好,但必須避免天真的方法。
最后 - 我的測試源代碼。
package WildWezyrListRemovalTesting;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class RemoveManyFromList {
public static abstract class BaseRemoveManyPerformer {
protected String performerName() {
return getClass().getSimpleName();
}
protected void info(String msg) {
System.out.println(performerName() + ": " + msg);
}
protected void populateList(List<Integer> items, int itemCnt) {
for (int i = 0; i < itemCnt; i++) {
items.add(i);
}
}
protected boolean mustRemoveItem(Integer itemVal, int itemIdx, int removeFactor) {
if (removeFactor == 0) {
return false;
}
return itemIdx % removeFactor == 0;
}
protected abstract List<Integer> removeItems(List<Integer> items, int removeFactor);
protected abstract List<Integer> createInitialList();
public void testMe(int itemCnt, int removeFactor) {
List<Integer> items = createInitialList();
populateList(items, itemCnt);
long startMillis = System.currentTimeMillis();
items = removeItems(items, removeFactor);
long endMillis = System.currentTimeMillis();
int chksum = 0;
for (Integer item : items) {
chksum += item;
}
info("removing took " + (endMillis - startMillis)
+ " milli(s), itemCnt=" + itemCnt
+ ", removed items: " + (itemCnt - items.size())
+ ", remaining items: " + items.size()
+ ", checksum: " + chksum);
}
}
private List<BaseRemoveManyPerformer> rmps =
new ArrayList<BaseRemoveManyPerformer>();
public void addPerformer(BaseRemoveManyPerformer rmp) {
rmps.add(rmp);
}
private Runtime runtime = Runtime.getRuntime();
private void runGc() {
for (int i = 0; i < 5; i++) {
runtime.gc();
}
}
public void testAll(int itemCnt, int removeFactor) {
runGc();
for (BaseRemoveManyPerformer rmp : rmps) {
rmp.testMe(itemCnt, removeFactor);
}
runGc();
System.out.println("\n--------------------------\n");
}
public static class NaiveRemoveManyPerformer
extends BaseRemoveManyPerformer {
@Override
public List<Integer> removeItems(List<Integer> items, int removeFactor) {
if (items.size() > 300000 && items instanceof ArrayList) {
info("this removeItems is too slow, returning without processing");
return items;
}
int i = 0;
Iterator<Integer> iter = items.iterator();
while (iter.hasNext()) {
Integer item = iter.next();
if (mustRemoveItem(item, i, removeFactor)) {
iter.remove();
}
i++;
}
return items;
}
@Override
public List<Integer> createInitialList() {
return new ArrayList<Integer>();
}
}
public static class BetterNaiveRemoveManyPerformer
extends NaiveRemoveManyPerformer {
@Override
public List<Integer> removeItems(List<Integer> items, int removeFactor) {
// if (items.size() > 300000 && items instanceof ArrayList) {
// info("this removeItems is too slow, returning without processing");
// return items;
// }
for (int i = items.size(); --i >= 0;) {
Integer item = items.get(i);
if (mustRemoveItem(item, i, removeFactor)) {
items.remove(i);
}
}
return items;
}
}
public static class LinkedRemoveManyPerformer
extends NaiveRemoveManyPerformer {
@Override
public List<Integer> createInitialList() {
return new LinkedList<Integer>();
}
}
public static class CreateNewRemoveManyPerformer
extends NaiveRemoveManyPerformer {
@Override
public List<Integer> removeItems(List<Integer> items, int removeFactor) {
List<Integer> res = createResultList(items, removeFactor);
int i = 0;
for (Integer item : items) {
if (mustRemoveItem(item, i, removeFactor)) {
// no-op
} else {
res.add(item);
}
i++;
}
return res;
}
protected List<Integer> createResultList(List<Integer> items, int removeFactor) {
return new ArrayList<Integer>();
}
}
public static class SmartCreateNewRemoveManyPerformer
extends CreateNewRemoveManyPerformer {
@Override
protected List<Integer> createResultList(List<Integer> items, int removeFactor) {
int newCapacity = removeFactor == 0 ? items.size()
: (int) (items.size() * (removeFactor - 1L) / removeFactor + 1);
//System.out.println("newCapacity=" + newCapacity);
return new ArrayList<Integer>(newCapacity);
}
}
public static class FasterSmartCreateNewRemoveManyPerformer
extends SmartCreateNewRemoveManyPerformer {
@Override
public List<Integer> removeItems(List<Integer> items, int removeFactor) {
List<Integer> res = createResultList(items, removeFactor);
for (int i = 0; i < items.size(); i++) {
Integer item = items.get(i);
if (mustRemoveItem(item, i, removeFactor)) {
// no-op
} else {
res.add(item);
}
}
return res;
}
}
public static class ForwardInPlaceRemoveManyPerformer
extends NaiveRemoveManyPerformer {
@Override
public List<Integer> removeItems(List<Integer> items, int removeFactor) {
int j = 0; // destination idx
for (int i = 0; i < items.size(); i++) {
Integer item = items.get(i);
if (mustRemoveItem(item, i, removeFactor)) {
// no-op
} else {
if (j < i) {
items.set(j, item);
}
j++;
}
}
return items.subList(0, j);
}
}
public static class MagicRemoveManyPerformer
extends NaiveRemoveManyPerformer {
@Override
public List<Integer> removeItems(List<Integer> items, int removeFactor) {
for (int i = 0; i < items.size(); i++) {
if (mustRemoveItem(items.get(i), i, removeFactor)) {
Integer retainedItem = removeSomeFromEnd(items, removeFactor, i);
if (retainedItem == null) {
items.remove(i);
break;
}
items.set(i, retainedItem);
}
}
return items;
}
private Integer removeSomeFromEnd(List<Integer> items, int removeFactor, int lowerBound) {
for (int i = items.size(); --i > lowerBound;) {
Integer item = items.get(i);
items.remove(i);
if (!mustRemoveItem(item, i, removeFactor)) {
return item;
}
}
return null;
}
}
public static class GuavaArrayListRemoveManyPerformer
extends BaseRemoveManyPerformer {
@Override
protected List<Integer> removeItems(List<Integer> items, final int removeFactor) {
Iterables.removeIf(items, new Predicate<Integer>() {
public boolean apply(Integer input) {
return mustRemoveItem(input, input, removeFactor);
}
});
return items;
}
@Override
protected List<Integer> createInitialList() {
return new ArrayList<Integer>();
}
}
public void testForOneItemCnt(int itemCnt) {
testAll(itemCnt, 0);
testAll(itemCnt, itemCnt);
testAll(itemCnt, itemCnt - 1);
testAll(itemCnt, 3);
testAll(itemCnt, 2);
testAll(itemCnt, 1);
}
public static void main(String[] args) {
RemoveManyFromList t = new RemoveManyFromList();
t.addPerformer(new NaiveRemoveManyPerformer());
t.addPerformer(new BetterNaiveRemoveManyPerformer());
t.addPerformer(new LinkedRemoveManyPerformer());
t.addPerformer(new CreateNewRemoveManyPerformer());
t.addPerformer(new SmartCreateNewRemoveManyPerformer());
t.addPerformer(new FasterSmartCreateNewRemoveManyPerformer());
t.addPerformer(new MagicRemoveManyPerformer());
t.addPerformer(new ForwardInPlaceRemoveManyPerformer());
t.addPerformer(new GuavaArrayListRemoveManyPerformer());
t.testForOneItemCnt(1000);
t.testForOneItemCnt(10000);
t.testForOneItemCnt(100000);
t.testForOneItemCnt(200000);
t.testForOneItemCnt(300000);
t.testForOneItemCnt(500000);
t.testForOneItemCnt(1000000);
t.testForOneItemCnt(10000000);
}
}
正如其他人所說,你的第一個傾向應該是建立第二個清單。
但是,如果您還想嘗試就地編輯列表,那么有效的方法是使用Guava的Iterables.removeIf()
。 如果它的參數是一個列表,它會將保留的元素合並到前面,然后簡單地切掉末尾 - 比逐個刪除()內部元素要快得多。
從ArrayList
刪除大量元素是O(n^2)
操作。 我建議只使用一個更適合插入和刪除的LinkedList
(但不是隨機訪問)。 LinkedList有一點內存開銷。
如果確實需要保留ArrayList
,那么最好創建一個新列表。
更新:與創建新列表相比:
重用相同的列表,主要成本來自刪除節點和更新LinkedList中的相應指針。 這是任何節點的常量操作。
構建新列表時,主要成本來自創建列表和初始化數組條目。 兩者都是廉價的操作。 您可能還要承擔調整新列表后端陣列大小的成本; 假設最終數組大於傳入數組的一半。
因此,如果您只刪除一個元素,那么LinkedList
方法可能更快。 如果要刪除除1之外的所有節點,則新列表方法可能更快。
引入內存管理和GC時會出現更多復雜情況。 我想把它們留下來。
最佳選擇是自己實施替代方案,並在運行典型負載時對結果進行基准測試。
我會創建一個新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
會更好。
但...
ArrayList
的常量因子小於LinkedList
( Ref )的常量因子。 既然您可以合理地猜測將刪除多少元素(在您的問題中說“大約一半”),只要您不必重新添加元素到ArrayList
的末尾就是O(1) - 分配它。 所以,如果你能做出合理的猜測,我希望在大多數情況下, ArrayList
比LinkedList
略快。 (這適用於我發布的代碼。在你天真的實現中,我認為LinkedList
會更快)。
我認為構建一個新列表而不是修改現有列表會更有效 - 特別是當項目數量與您指示的一樣大時。 這假定,您的列表是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;
}
對不起,但所有這些答案都沒有提到,我想:你可能沒有,也可能不應該使用List。
如果這種“查詢”很常見,為什么不構建一個有序的數據結構,消除了遍歷所有數據節點的需要? 你沒有告訴我們關於這個問題的充分信息,但考慮到你提供一個簡單樹的例子可以解決問題。 每個項目都有一個插入開銷,但您可以非常快速地找到包含匹配節點的子樹,因此您可以避免現在正在進行的大部分比較。
此外:
根據確切的問題以及您設置的確切數據結構,您可以加快刪除速度 - 如果要殺死的節點確實減少到子樹或某種類型,您只需刪除該子樹,而不是更新整個列表節點。
每次刪除一個列表項時,你都在更新指針 - 例如lastNode.next和nextNode.prev或者其他東西 - 但是如果事實證明你也想要刪除nextNode ,那么你剛才引起的指針更新會被拋棄一個新的更新。)
您可以嘗試的一件事是使用LinkedList
而不是ArrayList
,與ArrayList
一樣,如果從列表中刪除元素,則需要復制所有其他元素。
使用Apache Commons Collections 。 特別是這個功能 。 這實現方式與人們建議您實現它的方式基本相同(即創建一個新列表然后添加到它中)。
由於速度是最重要的指標,因此可以使用更多內存並減少列表重放(如我的評論中所述)。 但實際的性能影響完全取決於功能的使用方式。
該算法假定至少滿足下列條件之一:
免責聲明:有很多語法錯誤 - 我沒有嘗試編譯任何東西。
首先,繼承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(); } }
也許List不是您的最佳數據結構? 你能改變嗎? 也許您可以使用一個樹,其中項目的排序方式是刪除一個節點刪除滿足條件的所有項目? 或者至少可以加快您的運營速度?
在你的簡單示例中,使用兩個列表(一個包含i%2!= 0的項為真,另一個包含i%2!= 0的項為false)可以很好地服務。 但這當然非常依賴於域名。
而不是混淆我的第一個答案,這已經很長了,這是第二個相關的選項:你可以創建自己的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(); } }
嘗試在您的算法中實現遞歸。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.