内存有效的快速搜索和慢速插入/删除列表

时间:2012-07-02 18:22:39

标签: java algorithm design-patterns data-structures

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

  1. 内存占用量小

  2. 快速O(log n)搜索

  3. 插入/删除速度超过memcpy()

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

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

7 个答案:

答案 0 :(得分:2)

我认为你想使用Van Emde Boas Tree

它具有以下特征:

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

答案 1 :(得分:1)

你能使用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

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

答案 2 :(得分:1)

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

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

如果数据密集且允许重复/共同,则存储每个可能值的出现次数的数组可能效果很好。性能与位图方法类似,但是你可能需要32倍的内存,假设出现次数为。

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

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

答案 3 :(得分:1)

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

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

P.S。如果所有其他方法都失败了,那么全部正整数的简单BitSet就是“仅”268MB(如果我的计算是正确的......)

答案 4 :(得分:0)

链接列表怎么样?对于以前的&amp;和/或下一个指针。就插入和删除而言,时间要求只是列在列表中,直到找到一个小于你正在进行的那个并将其放在该记录之前。删除只需要改变上一个和下一个指针,搜索就像插入一样简单。

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

要看的另一件事是经典的B-tree

如果您的数据集大小相对一致,您甚至可以编写一个单层B树(所以只有一个根节点和多个子节点),这样可以简化实现(因为你可以只需存储int[][],并将内部密钥替换为叶子,如果有意义的话,将其替换为。)