在Java中使用另一个ArrayList循环遍历一个ArrayList

3

我有一个大的句子数组列表和另一个单词数组列表。

我的程序循环遍历数组列表,并从该数组列表中删除一个元素,如果该句子包含另一个单词中的任何一个单词。

句子数组列表可能非常大,我编写了一个快速而简单的嵌套for循环。虽然这对于句子不多的情况下可以运行,但在句子很多的情况下,完成此操作所需的时间非常长。

for (int i = 0; i < SENTENCES.size(); i++) {

        for (int k = 0; k < WORDS.size(); k++) {

            if (SENTENCES.get(i).contains(" " + WORDS.get(k) + " ") == true) {

                //Do something
            }
        }
    }

有比嵌套for循环更高效的方法吗?

你的单词列表有多长?单词中可以包含特殊字符吗? - Sergey Kalinichenko
我的单词列表可能会因外部因素而有所变化。但在最近几次检查中,我最终得到了大约200-300个单词。 - GreenGodot
" " + WORDS.get(k) + " "无法发现句子开头或结尾的单词。 - cahen
这真的取决于你在内部 if 子句中想要做什么。 - cool
1
如果您使用集合而不是列表,那么查找时间为O(1),而不是O(n)。 - vefthym
Crud,感谢你发现了这个问题,Cahen。 - GreenGodot
9个回答

6

你的代码存在一些低效性,但说到底,如果你必须搜索包含特定单词的句子,那么就无法避免使用循环。

尽管如此,还有几个可以尝试的方法。

首先,将WORDS设置为HashSet,因为它执行哈希查找来获取值,所以contains方法会比ArrayList快得多。

其次,尝试按照以下方式调整逻辑:

Iterator<String> sentenceIterator = SENTENCES.iterator();

sentenceLoop:
while (sentenceIterator.hasNext())
{
  String sentence = sentenceIterator.next();

  for (String word : sentence.replaceAll("\\p{P}", " ").toLowerCase().split("\\s+"))
  {
    if (WORDS.contains(word))
    {
      sentenceIterator.remove();
      continue sentenceLoop;
    }
  }      
}    

这段代码(假设您正在尝试删除包含特定单词的句子)使用了Iterator并避免了您原始代码中的string连接和解析逻辑(将其替换为单个正则表达式),这两者都应该更快。

但请记住,就像所有性能相关的事情一样,您需要测试这些更改以查看它们是否改善了情况。


是的。内部循环的时间复杂度为 O(句子长度),而不是 O(单词列表长度)。 - RealSkeptic
尽管这解决了问题的一部分,但由于主要问题是从该列表中删除句子,整个算法仍将保持O(N)。 - Luiggi Mendoza
这个解决方案对我真的很有用。关于删除,我只是改变了程序的工作方式,并记录要删除的单词到一个 .csv 文件中。无论如何,这对我来说都是可行的。再次感谢。 - GreenGodot

4
我会说不是问题,但您必须更改处理数据删除的方式。您问题说明中的以下部分已经指出了这一点:
句子数组列表可能非常大(...)。 当有很多句子时,虽然当句子不多时有效,但完成此操作所需的时间非常长。
原因是ArrayList中的删除时间为O(N),而且由于您正在循环内执行此操作,因此它至少需要O(N^2)。
我建议使用LinkedList而不是ArrayList存储句子,并使用Iterator而不是List#get,因为后者已在LinkedList中提供了Iterator#remove,其时间为O(1)。
如果您无法将设计更改为LinkedList,则建议在新List中存储有效的句子,并在最后用这个新List替换原始List的内容,从而节省大量时间。
除了这个大改进之外,您还可以通过使用Set来存储要查找的单词而不是使用另一个List来进一步改进算法,因为在Set中查找是O(1)。

1
我将从第二个ArrayList中创建一组单词:
Set<String> listOfWords = new HashSet<String>();
listOfWords.add("one");
listOfWords.add("two");

我将遍历集合和第一个ArrayList,使用Contains方法:
for (String word : listOfWords) {
     for(String sentence : Sentences) {
           if (sentence.contains(word)) {
                // do something
           }
     }
 }

另外,如果您可以自由使用任何开源的jar文件,请查看:

在另一个字符串中搜索字符串


2
在这种情况下,将单词转换为集合有什么好处呢? - RealSkeptic
集合包含唯一元素。ArrayList 可能包含重复。而 Cahen 提到的是什么。 - Vicky
我认为独一无二不是 OP 的问题。 - RealSkeptic
@Codebender:将句子ArrayList中的每个句子分成单词所需的时间如何? - Vicky

1
首先,你的程序有一个漏洞:它无法计算句子开头和结尾的单词。
你当前的程序具有O(s*w)的运行时间复杂度,其中s是所有句子的字符长度,w是所有单词的字符长度。
如果“words”相对较小(大约几百个项目),你可以使用正则表达式来显着加快速度:构建像这样的模式,并在循环中使用它:
StringBuilder regex = new StringBuilder();
boolean first = true;
// Let's say WORDS={"quick", "brown", "fox"}
regex.append("\\b(?:");
for (String w : WORDS) {
    if (!first) {
        regex.append('|');
    } else {
        first = false;
    }
    regex.append(w);
}
regex.append(")\\b");
// Now regex is "\b(?:quick|brown|fox)\b", i.e. your list of words
// separated by OR signs, enclosed in non-capturing groups
// anchored to word boundaries by '\b's on both sides.
Pattern p = Pattern.compile(regex.toString());
for (int i = 0; i < SENTENCES.size(); i++) {
    if (p.matcher(SENTENCES.get(i)).find()) {
        // Do something
    }
}

由于正则表达式被预编译成适合快速搜索的结构,因此您的程序将以O(s * max(w))的时间运行,其中 s 是所有句子中字符的长度,而w是最长单词的长度。鉴于您的集合中单词的数量约为200或300,这可能会使运行时间减少一个数量级。

Pattern p部分之后,你能详细说明一下吗?我看不出来如何匹配WORDS中的任何单词。我可以查阅API文档,但这将为未来的读者节省时间。 - vefthym
@vefthym 我将表达式简化为 "\b(?:quick|brown|fox)\b",使其尽可能基础。即使只是对正则表达式库有粗略的了解,也足以理解它的工作原理。 - Sergey Kalinichenko
请原谅我的无知,也许并非每个人都对正则表达式有一定的了解。感谢您的编辑。 - vefthym

1
你可以将所有单词放入 HashSet 中。这样可以快速检查一个单词是否在集合中。请参阅 https://docs.oracle.com/javase/8/docs/api/java/util/HashSet.html 了解更多信息。
HashSet<String> wordSet = new HashSet();
for (String word : WORDS) {
    wordSet.add(word);
}

然后只需要将每个句子分成组成它的单词,并检查这些单词中是否有任何一个在集合中。

for (String sentence : SENTENCES) {
    String[] sentenceWords = sentence.split(" "); // You probably want to use a regex here instead of just splitting on a " ", but this is just an example.
    for (String word : sentenceWords) {
        if (wordSet.contains(word)) {
            // The sentence contains one of the special words.
            // DO SOMETHING
            break;
        }
    }
}

0
如果您有足够的内存,可以将句子进行分词并将它们放入一个Set中。这样做不仅可以提高性能,而且比当前的实现更加正确。

0

看了你的代码,我建议两个可以提高每次迭代性能的事情:

  1. 去掉“== true”。contains操作已经返回一个布尔值,所以在if中比较它与true会增加不必要的额外操作。
  2. 不要在循环内拼接字符串(" " + WORDS.get(k) + " "),因为+运算符会创建新对象,这是一种相当昂贵的操作。最好使用字符串缓冲区/构建器,并在每次迭代后清除它,使用stringBuffer.setLength(0);

除此之外,对于这种情况,我不知道其他的方法,也许如果你可以将想要删除的单词抽象成一个模式并且只有一个循环,那么你可以使用正则表达式。

希望能帮到你!


3
不与“true”进行比较更多是好的编码风格而非效率问题。这并不会对操作的数量级产生影响。至于你的第二个建议,自Java 1.6以来,“+”运算符已经内部实现为StringBuilder,因此没有区别。 - RealSkeptic
我猜用 == true 编译器最终可能会进行优化,但使用 StringBuffer 肯定会有所帮助,因为每个 + 都会创建一个新对象。在循环外只创建一个对象,并在迭代后清除它,可以消除实例化开销。 - Borja Clemente

0

如果你关心效率,我认为最有效的方法是使用Aho-Corasick算法。虽然这里有2个嵌套循环和一个contains()方法(我认为它需要最好的句子长度+单词长度时间),但Aho-Corasick只需要一次循环来检查包含的单词,它只需要句子长度,比起原来的方法快单词长度倍(加上创建有限状态机的预处理时间,这个时间相对较小)。


0

我会从更理论的角度来解释这个问题。如果你没有内存限制,可以尝试模仿计数排序的逻辑。

假设M1 = 句子数量,M2 = 每个句子的单词数,N = 单词数量。为了简单起见,假设所有句子都有相同的单词数。你当前的方法的复杂度是O(M1.M2.N)。

我们可以创建一个单词-位置在句子中的映射。遍历你的句子arraylist,并将它们转换成二维的嵌套单词数组。遍历新数组,创建一个HashMap,其中键值对为单词和单词位置的arraylist(长度为X)。这是O(2M1.M2.X) = O(M1.M2.X)。

然后遍历你的单词arraylist,访问你的单词hashmap,循环遍历单词位置列表。删除每一个。这是O(N.X)。

假设你需要将结果以字符串arraylist的形式返回,我们需要另一个循环并连接所有内容。这是O(M1.M2)。

总复杂度为O(M1.M2.X) + O(N.X) + O(M1.M2),假设X远小于N,你可能会获得更好的性能。


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