我已经实现了下面引用的CodeEval中的“在文件中查找N条最长的行”问题。

我在站点上获得了完整的100分,并且他们的数据集执行时间为182ms,因此我认为代码是有效的。我想知道的是,是否可以做些什么使它比现在更快?我想念什么吗?还有其他注释吗?在它们的长度上按
降序排列。输入示例:

您的程序应将文件路径作为第一个参数。
文件包含多行。第一行指示应输出的
行数,其他行的长度不同,
随机显示。您可以假定输入文件的格式正确,并且第一行中的数字是有效的正整数。

例如:

 2
Hello World
CodeEval
Quick Fox
A
San Francisco
 



输出示例:

打印出受指定数目限制的最长行,并按其长度按降序排列。

例如:

 San Francisco
Hello World
 



代码:



 import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class NLongestLines {

    private final static Comparator<String> CMP = new Comparator<String>() {
        @Override
        public int compare(String arg0, String arg1) {
            return arg1.length() - arg0.length();
        }
    };

    private static void insertSorted(List<String> list, String string) {
        int max = list.size();
        int min = 0;
        int pivot = min + (max - min) / 2;

        // Binary search for insertion point.
        while (min < max) {
            int c = CMP.compare(string, list.get(pivot));
            if (c <= 0) {
                max = pivot;
            } else {
                min = pivot + 1;
            }
            pivot = min + (max - min) / 2;
        }

        list.add(min, string);
    }

    public static void main(String[] args) throws Exception {
        try (FileReader fr = new FileReader(args[0]); 
             BufferedReader reader = new BufferedReader(fr)) {
            List<String> longestLines = new ArrayList<>();

            String line = reader.readLine();
            {
                int numLongestLines = Integer.parseInt(line);
                while (numLongestLines > 0 && (line = reader.readLine()) != null) {
                    numLongestLines--;
                    line = line.trim();
                    insertSorted(longestLines, line);
                }
            }

            int shortestLongLength = findShortestLongLine(longestLines);
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.length() > shortestLongLength) {
                    insertSorted(longestLines, line);
                    longestLines.remove(longestLines.size() - 1);
                    shortestLongLength = findShortestLongLine(longestLines);
                }
            }

            for (String longLine : longestLines) {
                System.out.println(longLine);
            }
        }
    }

    private static int findShortestLongLine(List<String> longestLines) {
        return longestLines.get(longestLines.size() - 1).length();
    }
}
 


编辑/附录:

我已经实现并基准化了Simon Forsberg(TreeSet)和RolfL提出的解决方案(LinkedList)。

结果可以在这里看到。执行摘要表明,在几乎所有测试用例中,原始算法的运行速度都比提议的要快得多。

可在此处找到用于测试的源。从这里需要µbench。

评论

我不太确定int shortestLongLength代码部分的用途是什么,我在这里错过了什么吗?在我看来,用一个while循环而不是两个就足够了,不是吗?

@SimonForsberg shortestLongLength用作不会影响结果的任何行的提前退出。通过两个循环,我避免在找到最初的n行之前重复进行计算。

啊好吧。您首先只读取N行,然后连续删除最短的行并添加更长的行。而不是存储所有结果,然后仅获取N个最长的结果。

@SimonForsberg确实,我尝试使结果列表尽可能短,而忽略任何比当前结果列表中最短字符串短的行。

对于Unix,这非常容易。只需将以下内容粘贴在shell脚本中:awk'{打印长度,$ 0}'“ $ 1” |排序-n -r -s | cut -d“” -f2-;)

#1 楼

我相信您已经习惯了Do-it-quick的黑客大意...以及所有主要方法;-)

您的实现应具有LongestX类,即仅跟踪最长X行数的容器。它应该具有以下构造函数:

public LongestX<T>(int size, Comparator<T> longer) {
    ....
}


和类似这样的方法:

public void evaluate(T content) {
    ....
}


和最后是一个结果方法,该方法将返回此时最长的项: br />
...但是,在您到达那里之前,请看一下新的Java8比较器构造函数: .... ;;)

最后,我建议在LongestX内使用LinkedList来存储值....并在列表上使用一个简单的“行者”,看起来像:
public List<T> longest() {
    ....
}


使用ListIterator是处理链接列表的绝佳解决方案,并且由于操作为O(1),因此操作速度很快(当然,线性扫描速度较慢,但​​是我们希望topx小于行的总数,因此我们希望大多数记录都小于第一个记录。因此,如果每个字符串都比前一个字符串长,那么每次都会扫描所有项目:(

评论


\ $ \ begingroup \ $
没错,我只想了解比分和快速修复信息;)和我一起唱歌“这与算法有关,与bam bam badibum bam有关”。哦,我不知道Java8比较器的问题,但是我的目标是我显然忘记提及的Java 7(因为这就是我在其中上传代码的编辑器中所说的)。
\ $ \ endgroup \ $
–艾米莉·L。
16年1月11日在20:47



\ $ \ begingroup \ $
这是我所想到的与视频最接近的代码:codeproject.com/Articles/340797/…我执行了完全相同的基准测试。这就是为什么我在O(1)插入时间内选择O(n)的原因。
\ $ \ endgroup \ $
–艾米莉·L。
16年1月11日在21:08

\ $ \ begingroup \ $
如果“前X名”少于总数的1%,则任何列表中的任何内容都没有真正的区别。您要做的只是与第一个列表成员进行比较,而不必理会。在这方面,我的插入/删除操作可能很慢。
\ $ \ endgroup \ $
–rolfl
16年1月11日在23:37

\ $ \ begingroup \ $
正如您所说,我通过使用链表对您的想法进行了基准测试,当top X很小时,使用链表非常快。但是,由于这是编程方面的挑战,因此您可以打赌,top x不会太小:)我在OP中附加了基准图和源代码。让我知道我做错了什么。在µbench上做得很好!
\ $ \ endgroup \ $
–艾米莉·L。
16年1月12日在21:30



#2 楼


其他行的长度不同并且随机显示


我理解这是因为所有行的长度都不同。因此,我将对数据结构做出不同的选择。即,一个TreeSet<String>可以使用Comparator来构造。然后,我将使用TreeSet方法对元素进行descendingIterator()降序迭代。

注意:即使行长相同,您仍然可以使用x,但是需要同时比较长度和String本身,以避免忽略插入长度与先前现有元素相同的元素。 (如果元素可以完全相同,则TreeSet将无法正常工作)

这将使我们摆脱您的TreeSet方法,摆脱掉可能慢的insertSorted调用,即\ $ O(n -索引)\ $

评论


\ $ \ begingroup \ $
我讨论了要使用树还是列表(孔阵列,而不是链接列表)。但是由于访问的位置更好,我最终选择了该列表。在缓存中很热的数组中移动指针的速度非常快。但我会尝试您的建议,然后再回来。
\ $ \ endgroup \ $
–艾米莉·L。
16年1月11日在20:11

\ $ \ begingroup \ $
@EmilyL。我不知道您如何进行基准测试,但是我可以建议一个好的基准测试库吗?
\ $ \ endgroup \ $
–西蒙·福斯伯格
16年1月11日在20:22

\ $ \ begingroup \ $
请注意,在Java 8中,比较器可以编写为Comparator.comparingInt(String :: length).thenComparing(java.util.function.Function.identity())。
\ $ \ endgroup \ $
– 200_success
16年1月11日在22:12

\ $ \ begingroup \ $
@SimonForsberg在这种情况下,列表的大小限制为N,这是一个已知的常数。 (除非它被认为是问题的变量,并且也受n约束)
\ $ \ endgroup \ $
– njzk2
16年1月11日在22:44

\ $ \ begingroup \ $
@SimonForsberg我已经修复了实现问题,不幸的是结果保持不变。在我做的测试中,ArrayList比TreeSet快一些。如果您发现实施有任何问题,请告诉我。
\ $ \ endgroup \ $
–艾米莉·L。
16年1月13日在19:14

#3 楼

代替FileReader + BufferedReader组合,使用Scanner可以简化输入处理,但可能会更慢。与@Simon在PriorityQueue(或更常见的是TreeSet)上建议的模式相同。与通常使用堆一样,要按排序的顺序提取元素,您必须一个接一个地对它们进行排序,而不是简单地进行迭代,因为通常不会对存储进行排序。)
问题描述清楚地指出,这些行具有不同的长度,因此最小堆解决方案不会带来额外的好处,并且正如@ 200_success所指出的那样,在提取后反转元素还存在额外的障碍,以使它们按长度递减。
但我认为这种方法仍然值得关注因为我发现问题陈述被不现实地操纵以允许使用SortedSet解决方案,也许是为了简单起见。我可以轻松地想象出现实的问题,您需要在无约束的输入(可能有重复项)上找到K个最大的事物。

评论


\ $ \ begingroup \ $
这并不是说有什么问题,但是最后还有一个额外的步骤:对堆的内容进行排序,因为堆没有完全排序。
\ $ \ endgroup \ $
–mleyfman
16年1月11日在20:39

\ $ \ begingroup \ $
@EmilyL。您的解决方案实际上是O(L log N),因为您通过添加/删除已排序的数据结构来处理每一行。您可以通过使堆按长度存储字符串来使此性能与堆匹配。最短的字符串将始终在顶部。每次将元素添加到堆时,都会轮询顶部以将其删除。该方法类似于我的回答。至于哪个更快...这是基准测试的问题。
\ $ \ endgroup \ $
–mleyfman
16年1月11日在21:04



\ $ \ begingroup \ $
@EmilyL。无需排序。使用最小堆。它将始终包含N个最长的时间。它具有与您相同的时间和空间复杂性。
\ $ \ endgroup \ $
– janos
16年1月11日在21:07



\ $ \ begingroup \ $
@EmilyL。就像列表一样,让最小堆增长到N个元素,然后每次添加一个元素时,都将轮询以删除最低的元素。堆的大小保持为N,因此您将获得O(L log N)时间和O(N)空间,就像您的实现一样。
\ $ \ endgroup \ $
– janos
16年1月11日在21:22



\ $ \ begingroup \ $
我自己尝试了PriorityQueue,在每次插入后都对队列进行了修剪,以防止它超出N个元素。但是,事实证明TreeSet解决方案仍然更好,因为如果使用min-heap来促进修剪,那么最终会得到一个数据结构,该结构可以按O(N log N)的时间顺序提取N个结果,即增加长度,因此您仍然必须反转长度为N的列表。
\ $ \ endgroup \ $
– 200_success
16年1月11日22:00



#4 楼

扩展西蒙·福斯伯格(Simon Forsberg)的解决方案:

继续向TreeSet添加行,直到有N(输出所需的行数)行为止。
在N行之后,每次添加一个,删除最短的一个。这可以用myTreeSet.pollFirst()来完成。
之所以有用,是因为您得到的是O(L + L log(N))。差别是微小的,但是如果N L(比L小得多),则节省的时间加起来。


评论


\ $ \ begingroup \ $
@EmilyL。,它是O(L + L log N),而不是O(L + N log N),您执行L次插入,每个插入都花费N个时间。关于您的辩论,找出渐近复杂性的常数是基准测试的工作。
\ $ \ endgroup \ $
–mleyfman
16年1月11日在21:06



\ $ \ begingroup \ $
不要使用myTreeSet.remove(myTreeSet.first())-现在是O(log | S |)时间来查找条目,然后是O(log | S |)时间来再次找到并删除它。而是使用myTreeSet.pollFirst()。
\ $ \ endgroup \ $
– 200_success
16年1月11日在22:06



\ $ \ begingroup \ $
@ 200_success虽然最好使用1方法而不是2方法,但复杂度是相同的。 TreeSet是引擎盖下的红黑树,因此在任何情况下维护结构都需要O(log | S |)时间。我确实编辑了我的答案以切换到该答案。
\ $ \ endgroup \ $
–mleyfman
16年1月12日,0:33



#5 楼

除非我被“开箱即用”的Java所限制,否则我至少会考虑使用Guava。 Guava的Ordering类通过greatestOfonResultOf直接支持您想要的内容,这使得选择长度最大的N相当容易。特别是,您是否主要是在最大程度地减少开发时间,执行时间,内存使用等方面感兴趣。例如您希望将N与总输入大小进行比较的大小,以及是否需要支持交互式处理(在任何时候都提供N个最大的读取)还是仅支持批处理(在提供任何输出之前始终读取所有数据) 。

所以,让我们考虑一下这些因素如何影响代码。其中的N个“最大”(基于行长的比较)可能是最好。它几乎是简单的,尽管它可能不是最快的,但是在大多数情况下,对于大多数用途来说,它仍然可能足够快(例如,瓶颈可能是几乎所有合理机器上的I / O)。 >如果您主要关心最小化内存使用,那么jano使用堆的解决方案几乎肯定会更好,尤其是如果我们希望N行占输入文件的比例很小的话。特别是,它在任何给定时间最多存储N + 1行输入(并且+1部分也不会持续很长时间)。至少从理论上讲,这可能会至少提高一点执行速度,因为它是O(L log N)而不是O(L log L)(其中L是输入行数,N是输出行数) 。如果输入文件足够大,无法容纳在物理RAM中,但可以容纳N + 1行,那么减少的内存使用量可能会帮助它获得更大的优势(相对于涉及读取整个文件,然后进行排序或类似的东西。

就交互式与批处理而言,如果只执行批处理(而不关心内存使用),则最好阅读并存储所有内容。线,不考虑顺序,然后进行(部分)排序以获得所需的线。交互使用更有可能受益于在读取数据时维护数据的数据结构,因此,迄今为止最长的N个数据总是可用而无需任何额外的工作。 Java开箱即用,我可能会使用优先级队列(堆),然后将行从堆复制到堆栈,最后从那里复制到输出。但是,我比Java程序员更多地是C ++程序员,所以我倾向于在速度和内存使用都可能很重要的假设下工作。如果我不关心内存使用情况,则C ++中的选择可能是将std::partial_sort与比较器按长度降序使用。我猜Java可能提供了等效的功能,但是我不确定使用Java的程度。

评论


\ $ \ begingroup \ $
如果您忽略了树条目和树指针的开销,那么基于TreeSet的解决方案不应比基于PriorityQueue的解决方案需要更多的内存。
\ $ \ endgroup \ $
– 200_success
16年1月11日在23:38

\ $ \ begingroup \ $
@ 200_success:是的-通常不会大幅增加,但通常至少会更多。
\ $ \ endgroup \ $
–杰里·科芬(Jerry Coffin)
16年1月11日在23:41

\ $ \ begingroup \ $
由于这是一个编程挑战类型的站点,因此很遗憾,我们只能使用Java中的标准库。
\ $ \ endgroup \ $
–艾米莉·L。
16年1月12日在19:34