【问题标题】:Most frequently repeated numbers in a huge list of numbers大量数字中最常重复的数字
【发布时间】:2009-09-10 00:28:46
【问题描述】:

我有一个文件,其中包含许多随机整数(大约一百万),每个整数由空格分隔。我需要找到该文件中出现频率最高的 10 个数字。在java中这样做最有效的方法是什么? 我能想到 1. 创建一个哈希映射,键是文件中的整数,值是计数。对于文件中的每个数字,检查该键是否已经存在于哈希映射中,如果是,则 value++,否则在哈希中创建一个新条目 2.制作一个BST,每个节点都是文件中的整数。对于文件中的每个整数,查看 BST 中是否存在节点,如果是,则执行 value++,value 是节点的一部分。

如果我能想出好的散列函数,我觉得散列映射是更好的选择, 有人可以建议我这样做最好吗?我可以使用其他任何有效的算法吗?

【问题讨论】:

    标签: java data-structures performance


    【解决方案1】:

    编辑#2:

    好吧,我搞砸了自己的第一条规则——永远不要过早地优化。最坏的情况可能是使用范围广泛的股票 HashMap——所以我就这样做了。它仍然像一秒钟一样运行,所以忘记这里的所有其他内容,然后就这样做吧。

    在担心棘手的实现之前,我会提醒自己始终测试速度。

    (以下是较旧的过时帖子,如果某人的积分超过一百万,它仍然有效)

    HashSet 可以工作,但如果你的整数有一个合理的范围(比如 1-1000),那么创建一个包含 1000 个整数的数组会更有效,并且对于你的百万个整数中的每一个,递增大批。 (与 HashMap 的想法几乎相同,但优化了 Hash 必须考虑的一些未知数应该使其速度提高几倍)。

    您也可以创建一棵树。树中的每个节点都将包含(值,计数),并且树将按值组织(左侧的值较低,右侧的值较高)。遍历你的节点,如果它不存在——插入它——如果存在,那么就增加计数。

    您的值的范围和分布将决定这两个(或常规哈希)中的哪一个会表现更好。我认为常规哈希不会有很多“获胜”案例(它必须是广泛的“分组”数据,即使这样树也可能获胜。

    因为这很简单——我建议您实施多个解决方案并根据实际数据集测试速度。

    编辑:回复评论

    TreeMap 可以工作,但仍会添加一个间接层(而且自己实现非常简单和有趣)。如果您使用 stock 实现,则必须使用 Integers 并在每次增加时不断地与 int 相互转换。存在指向 Integer 的指针的间接性,以及您存储的对象数量至少是 2 倍的事实。这甚至不计算方法调用的任何开销,因为它们应该内联。

    通常这将是一种优化(邪恶),但是当您开始接近数十万个节点时,您有时必须确保效率,因此内置的 TreeMap 将因为与内置的相同原因而变得低效-in HashSet 会。

    【讨论】:

    • 不需要从scrath实现树,因为java已经有了java.util.TreeMap,它使用了红黑树。
    【解决方案2】:

    Java 处理散列。您不需要编写散列函数。只需开始在哈希映射中推送内容即可。

    另外,如果这是只需要运行一次(或只是偶尔)的东西,那么不要同时优化。它会足够快。仅当它是要在应用程序中运行的东西时才打扰。

    【讨论】:

    • 我需要让它尽可能高效。它将作为更大应用程序的一部分运行。
    【解决方案3】:

    哈希映射

    一百万个整数并不是很多,即使对于解释型语言,尤其是对于像 Java 这样的快速语言。您可能几乎不会注意到执行时间。如果您认为这太慢,我会先尝试一下,然后再转向更复杂的东西。

    与使用 HashMap 查找频率的最简单算法相比,进行字符串拆分和解析以转换为整数可能需要更长的时间。

    【讨论】:

      【解决方案4】:

      为什么要使用哈希表?只需使用与数字范围相同大小的数组即可。这样您就不会浪费时间执行散列函数。然后在完成后对值进行排序。 O(N log N)

      【讨论】:

      • 数字过大可能会降低效率
      【解决方案5】:
      1. 分配与您拥有的输入项数量相同大小的数组/向量
      2. 用数字填充文件中的数组,每个元素一个数字
      3. 按顺序排列列表
      4. 遍历列表并跟踪您遇到的前 10 位数字。
      5. 最后输出前十名。

      作为第 4 步的改进,您只需要以相当于您的第 10 次最长跑步的步数向前穿过阵列。任何超过此时间的运行都将与您的采样重叠。如果第十个最长的运行是 100 个元素,您只需要对元素 100、200、300 进行采样,并在每个点计算您在那里找到的整数的运行(向前和向后)。任何超过第 10 长的运行肯定会与您的采样重叠。

      您应该在第 10 次运行长度与数组中的其他运行相比非常长之后应用此优化。

      除非你有很少的唯一数字,每个都有大量重复,否则地图对于这个问题来说是多余的。

      注意:类似于 gshauger 的回答,但充实

      【讨论】:

        【解决方案6】:

        如果您必须使其尽可能高效,请使用整数数组,其中位置表示值,内容表示计数。这样可以避免自动装箱和拆箱,这是标准 Java 集合最有可能的杀手。

        如果数字范围太大,请查看 PJC 及其 IntKeyIntMap 实现。它也将避免自动装箱。不过,我不知道它对你来说是否足够快。

        【讨论】:

          【解决方案7】:

          如果数字的范围很小(例如 0-1000),请使用数组。否则,使用HashMap<Integer, int[]>,其中的值都是长度为 1 的数组。在基元数组中增加一个值应该比每次要增加一个值时创建一个新的 Integer 快得多。您仍在为键创建 Integer 对象,但这很难避免。毕竟,创建一个 2^31-1 个整数的数组是不可行的。

          如果所有输入都已标准化,因此您没有像 01 而不是 1 这样的值,请使用字符串作为映射中的键,这样您就不必创建整数键。

          【讨论】:

            【解决方案8】:

            在遍历文件时,使用 HashMap 在内存中创建数据集(值-计数对)。 HashMap 应该让您在创建数据集时接近 O(1) 访问元素(从技术上讲,在最坏的情况下,HashMap 是 O(n))。完成文件搜索后,对 HashMap.values() 返回的值 Collection 使用 Collections.sort() 来创建值-计数对的排序列表。使用 Collections.sort() 保证 O(nLogn)。 例如:

            public static class Count implements Comparable<Count> {
                int value;
                int count;
                public Count(int value) {
                    this.value = value;
                    this.count = 1;
                }
                public void increment() {
                    count++;
                }
                public int compareTo(Count other) {
                    return other.count - count;
                }
            }
            
            public static void main(String args[]) throws Exception {
                Scanner input = new Scanner(new FileInputStream(new File("...")));
                HashMap<Integer, Count> dataset = new HashMap<Integer, Count>();
                while (input.hasNextInt()) {
                    int tempInt = input.nextInt();
                    Count tempCount = dataset.get(tempInt);
                    if (tempCount != null) {
                        tempCount.increment();
                    } else {
                        dataset.put(tempInt, new Count(tempInt));
                    }
                }
            
                List<Count> counts = new ArrayList<Count>(dataset.values());
                Collections.sort(counts);
            

            【讨论】:

              【解决方案9】:

              实际上,有一个 O(n) 算法可以准确地完成您想做的事情。您的用例类似于 LFU 缓存,其中元素的访问计数决定它是在缓存中还是被逐出缓存。

              http://dhruvbird.blogspot.com/2009/11/o1-approach-to-lfu-page-replacement.html

              【讨论】:

                【解决方案10】:

                这是java.lang.Integer.hashCode() 的来源,如果您将条目存储为HashMap&lt;Integer, Integer&gt;,将使用该散列函数:

                public int hashCode() {
                return value;
                }
                

                换句话说,java.lang.Integer 的(默认)哈希值就是整数本身。

                还有什么比这更有效的?

                【讨论】:

                  【解决方案11】:

                  正确的做法是使用链表。当你插入一个元素时,你沿着链表向下,如果它在那里你增加节点计数,否则创建一个计数为 1 的新节点。插入每个元素后,你将在 O(n *log(n))。

                  对于您的方法,您正在执行 n 次插入,然后在 O(n*log(n)) 中进行排序,因此您的复杂度系数更高。

                  【讨论】:

                  • 每次查找值时都可能需要遍历整个列表,除非您知道输入已排序。
                  • 您是在建议本质上是 O(n^2) 的插入排序。我不知道你从哪里得到日志,但你通常需要一种“分而治之”的方法来获得对数的执行时间。
                  • 好吧,我把log(n) 放在那里是因为我认为数字的分布很不平衡,但你是对的,在最坏的情况下是O(n^2)。如果数字的分布真的有偏差,你甚至可以做得比O(n*log(n)) 更好。
                  • 使用链表存储计数的最坏情况复杂度大约为 O(n^2)。最好的情况是 O(n)。这比哈希表或树建议更糟糕。
                  • 你刚才说最好的情况是 O(n) (这是正确的)。 O(n) 比 O(n*log(n)) 好(其他解决方案的复杂性),所以应该考虑,严格来说并不差。
                  猜你喜欢
                  • 2015-05-10
                  • 1970-01-01
                  • 2021-06-24
                  • 2019-09-03
                  • 1970-01-01
                  • 2022-11-19
                  • 1970-01-01
                  • 2016-08-24
                  • 1970-01-01
                  相关资源
                  最近更新 更多