如何在Java中高效地(提高性能)从List中删除多个项目?

31

我有一个非常大的列表,名为items(>= 1,000,000项),并且有一些条件用表示,该条件选择要删除的项目,并且对于我的列表中许多项目(可能是一半)都为true。

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

这是我的测试代码:

    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 版本可以提供,并且为什么这个更好的版本实际上更好?


哪些性能指标很重要——只有速度,还是内存使用也很重要?列表的生命周期短吗?缩短后(删除后)列表的每个条目是否保证被访问?我想知道是否创建一个新的列表迭代器,将删除条件存储为返回条件,可能是某些问题的有效解决方案。不是从列表中删除,而是使迭代器的next()方法跳过不匹配条件的项。这将具有仅测试您操作的条目的好处,但浪费大量内存的惩罚。 - atk
输入是一个列表,输出也是一个列表(删除了选定的项目或保留了选定的项目)。速度是我最重要的指标。 - WildWezyr
感谢您的回答!我刚刚给出了我的答案,它汇编了不同的提议方法并在实践中进行了测试。我希望我的代码没有错误,并且我的最终结论是有帮助的。 - WildWezyr
12个回答

40

好的,现在是测试不同方法的结果。以下是我测试过的各种方法(每个方法的名称也是我的源代码中的类名):

  1. NaiveRemoveManyPerformer - 使用迭代器和remove方法的ArrayList - 这是我在问题中提出的第一个简单实现。
  2. BetterNaiveRemoveManyPerformer - 使用反向迭代和从后往前删除的ArrayList
  3. LinkedRemoveManyPerformer - 使用迭代器和remove方法但只适用于LinkedList。缺点:仅适用于LinkedList
  4. CreateNewRemoveManyPerformer - 将ArrayList复制为新ArrayList(仅添加保留的元素),使用迭代器遍历输入的ArrayList
  5. SmartCreateNewRemoveManyPerformer - 更好的CreateNewRemoveManyPerformer - 将结果ArrayList的初始大小(容量)设置为最终列表的大小。缺点:必须在开始时知道列表的最终大小。
  6. FasterSmartCreateNewRemoveManyPerformer - 更好的SmartCreateNewRemoveManyPerformer - 使用项目索引(items.get(idx))而不是迭代器。
  7. MagicRemoveManyPerformer - 在ArrayList中原地工作(不进行列表复制),并将空洞(已删除的项)从列表开头压缩到列表末尾的项目中。缺点:此方法更改了列表中的项目顺序。
  8. ForwardInPlaceRemoveManyPerformer - 在ArrayList中原地工作 - 将保留的项目移动以填补空洞,最后返回子列表(没有最终的删除或清除)。
  9. 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 个项的示例结果:
  1. NaiveRemoveManyPerformer:未执行测试-我没有那么耐心,但它的表现比 BetterNaiveRemoveManyPerformer 差。
  2. BetterNaiveRemoveManyPerformer: 226080 毫秒
  3. LinkedRemoveManyPerformer: 69 毫秒
  4. CreateNewRemoveManyPerformer: 246 毫秒
  5. SmartCreateNewRemoveManyPerformer: 112 毫秒
  6. FasterSmartCreateNewRemoveManyPerformer: 202 毫秒
  7. MagicRemoveManyPerformer: 74 毫秒
  8. ForwardInPlaceRemoveManyPerformer: 69 毫秒
  9. GuavaArrayListRemoveManyPerformer: 118 毫秒
从 1,000,000 个源项中删除 1 个项的示例结果(第一项已被删除):
  1. BetterNaiveRemoveManyPerformer: 34 毫秒
  2. LinkedRemoveManyPerformer: 41 毫秒
  3. CreateNewRemoveManyPerformer: 253 毫秒
  4. SmartCreateNewRemoveManyPerformer: 108 毫秒
  5. FasterSmartCreateNewRemoveManyPerformer: 71 毫秒
  6. MagicRemoveManyPerformer: 43 毫秒
  7. ForwardInPlaceRemoveManyPerformer: 73 毫秒
  8. GuavaArrayListRemoveManyPerformer: 78 毫秒
从 1,000,000 个源项中删除 333,334 个项的示例结果:
  1. BetterNaiveRemoveManyPerformer: 253206 毫秒
  2. LinkedRemoveManyPerformer: 69 毫秒
  3. CreateNewRemoveManyPerformer: 245 毫秒
  4. SmartCreateNewRemoveManyPerformer: 111 毫秒
  5. FasterSmartCreateNewRemoveManyPerformer: 203 毫秒
  6. MagicRemoveManyPerformer: 69 毫秒
  7. ForwardInPlaceRemoveManyPerformer: 72 毫秒
  8. GuavaArrayListRemoveManyPerformer: 102 毫秒

移除100万个元素的操作结果(全部元素都被移除,但是需要一个接一个地处理。如果您预先知道要删除所有元素,则应该简单清空列表):

  1. BetterNaiveRemoveManyPerformer: 58毫秒
  2. LinkedRemoveManyPerformer: 88毫秒
  3. CreateNewRemoveManyPerformer: 95毫秒
  4. SmartCreateNewRemoveManyPerformer: 91毫秒
  5. FasterSmartCreateNewRemoveManyPerformer: 48毫秒
  6. MagicRemoveManyPerformer: 61毫秒
  7. ForwardInPlaceRemoveManyPerformer: 49毫秒
  8. GuavaArrayListRemoveManyPerformer: 133毫秒

我的最终结论:使用混合方法-如果处理LinkedList,则简单迭代和移除是最好的,如果处理ArrayList--它取决于项目顺序是否重要——然后使用ForwardInPlaceRemoveManyPerformer,如果项目顺序可以改变,则最佳选择是MagicRemoveManyPerformer。如果事先知道要删除的因素(您知道将删除多少项与保留多少项),则可以添加更多条件以选择在特定情况下表现更好的方法。但是已知删除因子不是常见情况... Google Guava Iterables.removeIf 是这样一种混合解决方案,但具有稍微不同的假设(原始列表必须更改,不能创建新列表,并且项目顺序始终很重要) - 这些是最常见的假设,因此removeIf 在大多数实际情况下都是最佳选择。

还要注意,所有良好的方法(naive不好!)都足够好-它们中的任何一个都应该在实际应用程序中表现出色,但必须避免naive方法。

最后-我的测试源代码。

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

2
很高兴看到你投入了这么多精力来实证测试这些方法,但是不幸的是我必须告诉你这些测量结果并不具有说服力。获取Java代码的实质微基准测试非常困难。每次正确操作可能会有一百种错误操作方式,这些错误会极大地扭曲结果。例如,你需要在开始测量之前多次运行测试约10秒钟左右。每个测量应该测量“许多”次重复。继续测量直到结果稳定...... - Kevin Bourrillion
2
每次VM调用只测量一个东西(这很重要)。多次运行每个测量 - 你会惊讶于结果的不一致性。这只是冰山一角;如果你做了所有这些事情并遵循其他几十条建议,你的基准测试结果很可能像我的一样仍然具有可疑的意义。这就是Java中微基准测试的现状。 - Kevin Bourrillion
3
@Kevin:很棒,你也知道 Java 微基准测试困难的理论考虑。但是针对我的特定情况,你的观点是什么?我的代码或结论有什么问题吗?请给出更好的代码或更好的结论。你已经知道我的结论是错误的了吗?你看到我的代码中的预热阶段了吗(它相当隐蔽,但是确实存在)?请提供更多细节、代码修复或者你自己更好的测试结论... - WildWezyr
1
我没有时间教授微基准测试101,特别是因为如果世界上有人教微基准测试101,我将是第一个报名参加该课程的人。我不知道什么有效,但我已经看到了足够多的无效的东西,以对任何Java微基准测试持怀疑态度。我不是要贬低你——用George Costanza的话来说,“这不是你的问题,而是JVM/等等的问题。” - Kevin Bourrillion
4
@Kevin: 我之前就已经很“愉快”地阅读了你提供链接的一些文章。由于我已经了解了那里所说的内容,所以这种愉悦感更加强烈。我进行程序优化已经有约10年(使用不同的编程语言:Java、SQL语句等),因此我必须熟悉如何发现瓶颈、基准测试优化等方面。再次感谢你提供的大量理论信息……但请给出建设性的意见——指出我的代码有什么问题,为什么我的结论是错误的,如何修复它们。 - WildWezyr
显示剩余2条评论

12

正如其他人所说,你的第一反应应该是建立一个第二个列表。

但是,如果你想尝试对列表进行原地编辑,那么高效的方法是使用Guava中的Iterables.removeIf()。如果它的参数是一个列表,它将保留元素合并到前面,然后简单地截断末尾--比逐个删除内部元素要快得多。


我本来是要来推荐Guava(或者google-collections,如果你需要一个已经以二进制形式提供并且可以在公共maven仓库中找到的东西... Kevin,轻轻地推一下)但是你已经抢先了。 - Cowan
是的,不幸的是,在google-collections中不存在Iterables.removeIf();自那时以来,它是Guava中的新功能! - Kevin Bourrillion

6
ArrayList中删除大量元素是一个O(n^2)的操作。我建议直接使用LinkedList,它更适用于插入和删除(但不适用于随机访问)。LinkedList会有一些内存开销。
如果你确实需要保留ArrayList,那么最好创建一个新列表。
更新:与创建新列表进行比较:
重用同一列表,主要成本来自于删除节点并在LinkedList中更新适当的指针。这对于任何节点都是一个恒定的操作。
当构造一个新列表时,主要成本来自于创建列表和初始化数组条目。两者都是廉价的操作。您可能还需要扩展新列表后端数组的成本;假设最终数组大于传入数组的一半。
因此,如果只删除一个元素,则LinkedList方法可能更快。如果要删除除一个之外的所有节点,则可能更快的是新列表方法。
当涉及到内存管理和GC时,还有更多的复杂性。我想把这些留出来。
最好的选择是自己实现替代方案,并在运行典型负载时对结果进行基准测试。

在我的情况下,为什么使用LinkedList比LBushkin提供的解决方案(创建新列表)更好?或者它和新列表一样好(考虑性能)? - WildWezyr
这取决于你的使用情况,如果你经常需要从列表中删除大量项目,或者你的函数只是“给定此列表,返回一个删除了N个项目的列表”。 - matt b
可能还取决于您要删除多少项与列表中有多少项,例如,如果您有一个包含10,000个项目的列表,您只需要删除2个项目,而不是一个包含10,000个项目的列表,您需要删除其中9,999个项目。 - matt b
2
如果列表中元素的顺序不重要,实际上可以使LinkedList和ArrayList在删除方面同样高效。例如,而不是在ArrayList的元素上调用remove(),您将替换当前索引处的值为列表尾部的项目。然后调用remove()在最后一个项目上。这不会导致任何项目被移动,但确实改变了列表中项目的顺序。 - LBushkin
确实需要在两个列表中遍历整个列表。但是,其中一个涉及额外的指针修改,而另一个则需要分配和大量的指针赋值。在渐近意义下,O(n)操作是“等价”的,但在实践中它们有很大的区别。 - notnoop
显示剩余2条评论

5

我会创建一个新的List将项目添加到其中,因为从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小(参考文献)。由于你可以合理地猜测将删除多少元素(在你的问题中说了“大约一半”),只要你不必重新分配它,向ArrayList的末尾添加一个元素就是O(1)。因此,如果你能做出合理的猜测,我预计在大多数情况下ArrayList会比LinkedList稍微快一些。(这适用于我发布的代码。在你的天真实现中,我认为LinkedList会更快)。


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

你应该否定条件,并且不需要i++ - notnoop
我认为您忘记对移除条件进行否定以获得保留条件了;-)但我理解您的观点,这是我目前知道的最佳解决方案(在我看来)... - WildWezyr
@notnoop 我只是好奇:为什么你建议将 i++ 作为单独的语句删除?这样做有什么好处吗?我故意让它保持不变,因为它不是移除条件(cond)的一部分,而且代码似乎更长但更清晰...微小的关注点分离;-)。 - WildWezyr
1
在书面和口语中广泛使用的词汇,如果被认为不是一个单词,那么这种说法对我来说似乎很愚蠢。语言是活生生的实体,随着新概念和思想的出现而不断发展,并需要高效地进行交流。词典的认可并不能使其成为一个单词——只有人们的使用和理解才能如此。 - LBushkin
我不明白为什么你使用泛型,却没有消除三个多余的迭代器行,而你可以用 for (T item : items) 来替换它。在我看来,只有当你可能会调用 itr.remove() 时,迭代器才有用。 - Jherico
显示剩余2条评论

2
抱歉,但我认为所有这些答案都没有抓住重点:您可能不需要,也可能不应该使用列表。
如果这种“查询”很常见,为什么不构建一个有序的数据结构,以消除遍历所有数据节点的需要?您没有告诉我们足够关于问题的信息,但是考虑到您提供的示例,一个简单的树可以完成任务。每个项目都有插入开销,但是您可以非常快速地找到包含匹配节点的子树,因此避免了大部分比较。
此外:
- 根据确切的问题和您设置的确切数据结构,您可以加速删除--如果要删除的节点确实缩小为子树或类似物,则只需丢弃该子树,而不是更新整个列表节点。 - 每次删除列表项时,您都在更新指针--例如lastNode.next和nextNode.prev之类的内容--但是如果您发现还想删除nextNode,则刚刚引起的指针更新将被新更新所取代。

你说得很可能是对的:在某些情况下,更好的源结构应该表现更好。但是如果你不知道移除条件会是什么,那么选择专用结构就很困难了。如果移除条件是固定的,那么最好的数据结构可以在一开始就被选出来。然而,在我的问题中并非如此,所以解决方案必须在列表上操作,这就是我所询问的——关于从列表中高效地移除的方法。 - WildWezyr

1
你可以尝试使用 LinkedList 而不是 ArrayList,因为如果从列表中删除元素,则需要复制所有其他元素,而 LinkedList 则不需要。

在我的情况下,为什么使用LinkedList比LBushkin提供的解决方案(创建新列表)更好?或者它和新列表一样好(考虑性能)? - WildWezyr
我认为对于你的情况,添加到新的LinkedList或从现有的LinkedList中删除应该是相同的,因为两者都是O(1),并且你会删除一半的元素。如果你删除的元素比留下的元素多,那么添加到新列表应该更快,反之亦然。 - Fabian Steeg

1

使用Apache Commons Collections。具体来说,使用这个函数。它的实现方式基本上与人们建议你实现的方式相同(即创建一个新列表,然后添加到其中)。


1

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

该算法假定以下至少一个条件为真:

  • 原始列表的所有元素都不需要进行测试。如果我们真的正在寻找与我们的条件匹配的前N个元素,而不是所有与我们的条件匹配的元素,则可能会发生这种情况。
  • 将列表复制到新内存中更加昂贵。如果原始列表使用了超过50%的分配内存,则在原地工作可能更好,或者如果内存操作变得更慢(这将是意外的结果)。
  • 从列表中删除元素的速度惩罚太大,无法一次接受所有惩罚,但是将该惩罚分散到多个操作中是可以接受的,即使总体惩罚大于一次性承受所有惩罚的情况。这就像申请20万美元的抵押贷款:每月支付30年1000美元是可承受的,并且具有拥有房屋和资产的好处,即使整体付款在贷款期限内达到36万美元。

免责声明:可能存在语法错误-我没有尝试编译任何内容。

首先,子类化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("您必须为迭代器指定条件"); } }

然后我们需要帮助类:

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); }
/** * 注意,当两个匹配元素之间有一堆不匹配的元素时, * 交替调用hasNext()和hasPrev()非常低效。 */ public boolean hasNext() { if( nextKnownGood ) return true;
/* 找到列表中与我们的条件匹配的下一个对象(如果有)。 */ while( ! listIterator.hasNext() ) { Object next = listIterator.next(); if( condition.matches(next) ) { listIterator.set(next); nextKnownGood = true; return true; } }
nextKnownGood = false; // 没有找到匹配的元素。 return false; }
/** * 请参见hasNext()以获取效率说明。 * 复制并粘贴hasNext()。 */ public boolean hasPrevious() { if( prevKnownGood ) return true;
/* 找到列表中与我们的条件匹配的上一个对象(如果有)。 */ while( ! listIterator.hasPrevious() ) { Object prev = listIterator.next(); if( condition.matches(prev) ) { prevKnownGood = true; listIterator.set(prev); return true; } }
// 没有找到匹配的元素。 prevKnownGood = false; return false; }
/** 请参见hasNext()以获取效率说明 **/ public Object next() { if( nextKnownGood || hasNext() ) { prevKnownGood = nextKnownGood; nextKnownGood = false; return listIterator.next(); }
throw NoSuchElementException("没有更多匹配的元素"); }
/** 请参见hasNext()以获取效率说明;复制并粘贴next() **/ public Object previous() { if( prevKnownGood || hasPrevious() ) { nextKnownGood = prevKnownGood; prevKnownGood = false; return listIterator.previous(); } throw NoSuchElementException("没有更多匹配的元素"); }
/** * 注意,nextIndex()和previousIndex()返回值的是数组索引, * 而不是此类返回的结果数。如果这对您不好,请维护自己的当前索引, * 并在next()和previous()中递增或递减。 */ public int nextIndex(){ return listIterator.previousIndex(); } public int previousIndex(){ return listIterator.previousIndex(); }
public remove(){ listIterator.remove(); } public set(Object o) { listIterator.set(o); } }

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

/** 类似于比较器... **/
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("准备项目"); 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("准备列表所需时间为 " + (endmillis-startmillis) + " 毫秒。");
System.out.println("删除项目"); startMillis = System.currentTimeMillis(); // 我们实际上从这个列表中没有删除任何内容,因此 removeMany 实际上是“瞬间完成”的 // items = removeMany(items); endMillis = System.currentTimeMillis(); System.out.println("删除后:items.size=" + items.size() + " 所需时间为 " + (endMillis - startMillis) + " 毫秒。"); System.out.println("--> 注意:实际上没有删除任何内容。该算法使用额外的内存来避免修改或复制原始列表。");
System.out.println("即将遍历列表"); startMillis = System.currentTimeMillis(); int count = iterate(items, condition); endMillis = System.currentTimeMillis(); System.out.println("迭代后:items.size=" + items.size() + " count=" + count + " 所需时间为 " + (endMillis - startMillis) + " 毫秒。"); System.out.println("--> 注意:这应该是相当低效的。主要是由于多个类的开销。该算法被设计(希望)比使用列表的所有元素的算法更快。");
System.out.println("即将遍历列表"); startMillis = System.currentTimeMillis(); int total = addFirst(30, items, condition); endMillis = System.currentTimeMillis(); System.out.println("总计前 30 个元素后:total=" + total + " 所需时间为 " + (endMillis - startMillis) + " 毫秒。");
... private int iterate(List<Integer> items, Condition condition) { // i++ 和返回值实际上是为了防止 JVM 优化 // - 只是为了安全起见。 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(); } }

这似乎过于复杂了。我不知道你最初的假设是什么。我已经清楚地说明了我的问题(在我看来),甚至为此提供了测试代码。最重要的是,我的测试代码在不同的removeMany实现下的运行时间。越快越好 - 就这么简单。 - WildWezyr
@WildWezyr:这很可能是对你的问题过于复杂的解决方案。这就是为什么我在评论中问了额外的问题(超出“速度是唯一因素吗”)。 - atk
此外,问题可能仅在于实现过于复杂。如果您不需要通用解决方案,并且在删除项目后仅执行一次数据操作,则仍可以使用基础算法。 - atk

0

也许列表不是最适合您的数据结构?您能改变它吗?也许您可以使用树,其中项目按照删除一个节点会删除满足条件的所有项目的方式进行排序?或者至少加快您的操作速度?

在您的简单示例中,使用两个列表(一个包含 i % 2 != 0 为 true 的项目,另一个包含 i % 2 != 0 为 false 的项目)可能很好。但这当然非常依赖于领域。


删除条件可能会有所不同,它们可以预先确定,因此无法按特定顺序/结构等准备项目,因为不知道源列表(命名项目)何时被填充。 - WildWezyr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接