【问题标题】:memory effective list of integers with fast search and slow insert/delete具有快速搜索和缓慢插入/删除的内存有效整数列表
【发布时间】:2012-07-02 18:22:38
【问题描述】:

我正在尝试为正整数(数百万个元素)的排序列表找到最佳的data structure。要求是(按重要性排序):

  1. 内存占用小

  2. 快速O(log n)搜索

  3. 插入/删除比memcpy()

我正在考虑保留两个数组:一个用于搜索,一个用于插入。每隔几个操作,我都会重新组织主要的操作并清理第二个操作。有什么想法吗?我在正确的轨道上吗?

ps。没有重复项。它不需要是线程安全的。读取会经常发生,而写入很少发生。整数在结构中的分布不均匀,这意味着一些结构将只包含几个元素,而其他结构可能有数百万个元素,位置从零到0xFFFFFFFF

【问题讨论】:

  • 内存占用要求有多严格?您需要在内存和时间之间进行权衡,几兆和几百兆之间的差异对您应该如何处理有很大的影响。
  • 内存非常关键。理想情况下,我希望这个结构的大小为X * 4,其中 X 是元素数,4 是每个元素的字节数
  • 是否允许重复?是否需要安全的并发访问?写入与读取的粗略频率?
  • 如果写入如此罕见......您是否有实际的基准表明 memcpy 太慢了?
  • 您需要存储的最大正整数是多少?如果它是“合理的”(例如,小于 800M),您也许可以使用 BitSet。

标签: java algorithm design-patterns data-structures


【解决方案1】:

我想你想使用Van Emde Boas Tree

具有以下特点:

Space   O(M)
Search  O(log log M)
Insert  O(log log M)
Delete  O(log log M)

【讨论】:

  • 嗯。暂定+1,只是因为我似乎记得VEB在内存方面相对紧凑,因为它可以使用压缩位数组。 (此外,它是排序的,并且有效地支持排序操作。)
【解决方案2】:

这实际上是一个有趣且不平凡的问题。最佳答案取决于您执行的操作的具体要求。

如果数据密集且不允许重复,那么大位图可能是最佳选择。只需设置一点以显示每个可能的整数值的存在/不存在。这种方法对于读取和写入都非常快且 O(1),但内存使用量显然取决于您拥有的范围有多大/数据的稀疏程度。

如果数据密集且允许重复/常见,则存储每个可能值的出现次数的数组可能会很好地工作。在性能上与位图方法相似,但是假设出现次数为 int,您可能需要 32 倍的内存。

如果您阅读量大且数据稀疏,那么基于排序数组的方法(使用二进制搜索进行查找)可能是最好的。如果您了解值的粗略分布,那么您可以通过使用启发式算法来猜测目标值在数组中的可能位置(例如,如果您利用这些知识,您可以显着击败 log2(N)分布大致均匀)

如果您有大量写入且数据稀疏,那么您可能需要一个基于树的结构,该结构基于整数中的位子集进行拆分(例如,32 路 trie 拆分每个节点的下一个最重要的 5 位)。 Clojure 的持久性数据结构使用这种技术效果很好。

【讨论】:

    【解决方案3】:

    我认为@Peter Lawrey 有一个好的开始:细分。部分不同的是,我将细分为 256 个事物,每个事物跟踪 2^23 个事物。根据整数的分布,使用顶部或底部 8 位进行细分。

    至于 sub 的东西,当 int 稀疏时,从 Set (或类似的)开始。但是,一旦 Set 达到一定大小,(它开始变得密集)切换到 BitSet。我不知道你是否需要支持删除值,在这种情况下你需要从 BitSet 切换回 Set。

    附言如果一切都失败了,一个简单的所有正整数的BitSet“只有”268MB(如果我的计算是正确的......)

    【讨论】:

      【解决方案4】:

      你能用char[65536][]吗?其中顶部或底部 16 位是其他 16 位数组的索引。这可以使用少于 4 * X 每个条目。

      查找

       private final char[][] bitsArray = new char[65536][];
      
       public int countFor(int num) {
           int topBits = num >>> 16;
           int lowerBits = num & 0xFFFF;
           char[] lowerBitsArray = bitsArray[topBits];
           int count = 0;
           for(char l : lowerBitsArray)
              if(l == lowerBits)
                 count++;
           return count;
       }
      

      如果计数永远不会超过 1,那么 BitSet 可能是更好的选择。 (可能是 BitSet 数组,具体取决于数据模式)如果您要记录看到的 IP 地址,您可能无需担心 0.、10.、127.* 或 224-255.*


      int[]char[] 访问速度更快,包括转换为 int。

      public static void main(String... args) {
          char[] chars = new char[1000000];
          for (int i = 0; i < 5; i++)
              timeSum(chars);
          int[] ints = new int[1000000];
          for (int i = 0; i < 5; i++)
              timeSum(ints);
      }
      
      private static int timeSum(char[] chars) {
          long start = System.nanoTime();
          int sum = 0;
          for (char ch : chars) {
              sum += ch;
          }
          long time = System.nanoTime() - start;
          System.out.printf("Took %,d us to sum %,d chars%n", time / 1000, chars.length);
          return sum;
      }
      
      private static int timeSum(int[] ints) {
          long start = System.nanoTime();
          int sum = 0;
          for (int i : ints) {
              sum += i;
          }
          long time = System.nanoTime() - start;
          System.out.printf("Took %,d us to sum %,d ints%n", time / 1000, ints.length);
          return sum;
      }
      

      打印

      Took 5,378 us to sum 1,000,000 chars
      Took 11,551 us to sum 1,000,000 chars
      Took 437 us to sum 1,000,000 chars
      Took 407 us to sum 1,000,000 chars
      Took 407 us to sum 1,000,000 chars
      Took 5,539 us to sum 1,000,000 ints
      Took 532 us to sum 1,000,000 ints
      Took 530 us to sum 1,000,000 ints
      Took 511 us to sum 1,000,000 ints
      Took 507 us to sum 1,000,000 ints
      

      我的结论是缓存效率比转换成本更重要。

      【讨论】:

      • 这将如何使用少于 4*X?每个 int 有 4 个整数,因此仅存储整数需要 4 倍。
      • char16 bits,所以这样每个条目至少需要 2 个字节。开销很大,但可以很好地扩展。
      • 在每次比较时添加一个 char 到 int 的转换将是巨大的
      • 我愿意打赌它太小了,你甚至无法对其进行基准测试。鉴于它更有可能适合缓存,它可能会更快而不是更慢。 ;)
      • 他所说的是占用空间的大小将是 X * 2 而不是 X * 4,并且从 char 到 int 的转换可以在 Cashe 中完成,所以你甚至不会注意到它,除非你每秒做了数十亿次计算。 (在人眼看来)遍历列表所需的时间仍然与使用整数而不是字符的时间相同
      【解决方案5】:

      链表呢?记忆约束将是整数的大小+前一个和下一个指针的一点开销。至于插入和删除,时间要求只是在列表中向下查找,直到找到比您要插入的那个更小的一个并将其放在该记录之前。删除只需要更改previous和next的指针,搜索就像插入一样简单。

      【讨论】:

      • 每个项目都有一个指向另一个项目的指针,每个项目额外增加 4 个字节(至少)
      【解决方案6】:

      如果您不太担心速度,并且对内存使用感到恐惧,您可以加载一个整数数组,创建另一个数组,对数组进行排序,直到您有一个数字 X(1K 左右,以防止内存过载) 然后将数组的该部分保存为文本文件(objectOutputStream 会将整数保存为整数),清除数组,然后对数组中的下一个 X 个整数执行相同操作。只需确保将输出流标记为附加文件(true)与覆盖,这是默认值。

      【讨论】:

        【解决方案7】:

        您可以查看一些modern generation of tries(该链接未提及fusion trees)。但是,我认为它们的实现都非常复杂。如果你很幸运,你可能会发现一些大胆的人已经编写并开源了一个你可以使用的实现。

        另一个值得一看的东西是经典的B-tree

        如果您的数据集大小相对一致,您甚至可以编写一层 B-tree(因此具有单个根节点和多个子节点),这大大简化了实现(因为您可以只需存储一个int[][],然后用窥视叶子替换内部键,如果这有意义的话)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-01-06
          • 1970-01-01
          • 1970-01-01
          • 2021-09-22
          • 1970-01-01
          • 1970-01-01
          • 2010-10-27
          相关资源
          最近更新 更多