用于排序数组的紧凑数据结构

时间:2012-09-13 13:43:28

标签: arrays data-structures compression

我有一个带有排序数字的表格,如:

1 320102
2 5200100
3 92010023
4 112010202
5 332020201
6 332020411
: 
5000000000 3833240522044511
5000000001 3833240522089999
5000000002 4000000000213312

鉴于记录号,我需要O(log n)时间内的值。记录号为64位长,没有丢失的记录号。这些值是64位长,它们被排序并且值(n)<1。值(N + 1)。

显而易见的解决方案是简单地执行一个数组并使用记录号作为索引。每个值的成本为64位。

但我想要一种更节省空间的方法。既然我们知道值总是在增加应该是可行的,但我不记得允许我这样做的数据结构。

解决方案是在阵列上使用deflate,但这不会给我O(log n)来访问元素 - 因此是不可接受的。

你知道一个能给我的数据结构吗?

  • O(log n)进行访问
  • 空间要求&lt; 64位/值

=编辑=

由于我们事先知道所有数字,我们可以找到每个数字之间的差异。通过取这些差异的第99个百分点,我们将获得相对适中的数字。取log2将给出表示适度数字所需的位数 - 让我们称之为适度位。

然后创建:

64-bit value of record 0
64-bit value of record 1024
64-bit value of record 2048
64-bit value of record 3072
64-bit value of record 4096

然后是所有记录的增量表:

modest-bits difference to record 0
modest-bits difference to previous record
1022 * modest-bits difference to previous record
modest-bits difference to record 1024

记录k * 1024的适度位差异始终为0,因此我们可以将其用于信令。如果它不为零,则以下64位将指向接下来的1024条记录的简单数组作为64位值。

当选择适度值作为第99百分位数时,最多会发生1%的时间,因此浪费最多1%* n *适度位+ 1%* n * 64位* 1024。

空间:O(中等位* n + 64位* n / 1024 + 1%* n *中等位+ 1%* n * 64位* 1024)

查找:O(1 + 1024)

(可能需要调整99%和1024)

= Edit2 =

基于上述想法,但浪费的空间更少。创建:

64-bit value of record 0
64-bit value of record 1024
64-bit value of record 2048
64-bit value of record 3072
64-bit value of record 4096

对于无法用modest-bits表示的所有值,可以将大值表创建为树:

64-bit position, 64-bit value
64-bit position, 64-bit value
64-bit position, 64-bit value

然后是所有记录的增量表,每1024条记录重置一次:

modest-bits difference to record 0
modest-bits difference to previous record
1022 * modest-bits difference to previous record
modest-bits difference to record 1024

但也会重置大值表中的每个值。

空间:O(中等位* n + 64位* n / 1024 + 1%* n * 2 * 64位)。

查找需要搜索大值表,然后查找第1024个值,最后总结出适度位值。

查找:O(log(大值表)+ 1 + 1024)= O(log n)

你能提高吗?或者以不同的方式做得更好?

3 个答案:

答案 0 :(得分:2)

OP提议将数字拆分为块(仅一次)。但这个过程可能会继续下去。再次拆分每个街区。而且......最后我们可能会得到一个二元特里。

enter image description here

根节点包含索引最少的数字的值。它的右后代存储表中的中间数与索引最少的数字之间的差异:d = A[N/2] - A[0] - N/2。其他右后代(图中的红色节点)继续这样做。叶节点包含前面数字的增量:d = A[i+1] - A[i] - 1

因此,存储在trie中的大多数值都是delta值。它们中的每一个都占用少于64位。并且为了紧凑,它们可以作为可变比特长度的数字存储在比特流中。要获得每个数字的长度并在O(log N)时间内在此结构中导航,比特流还应包含(某些)数字和(某些)子树的长度:

  1. 每个节点包含其左子树的长度(以位为单位)(如果有的话)。
  2. 除叶子节点外,每个右后代(图中的红色节点)包含其值的长度(以位为单位)。叶节点的长度可以从从根到该节点的路径上的其他长度计算。
  3. 每个右后代(图中的红色节点)包含对应值的差异和路径上最近的“红色”节点的值。
  4. 所有节点都打包在比特流中,从根节点开始按顺序排列:左后代始终跟随其祖先;右后裔跟随子树,以左后裔为根。
  5. 要访问给定索引的元素,请使用索引的二进制表示来跟踪trie中的路径。遍历此路径时,将“红色”节点的所有值相加。当索引中不再有非零位时停止。

    有几种方法可以存储N / 2值的长度:

    1. 根据需要为每个长度分配尽可能多的位,以表示从最大长度到低于平均长度的所有值(不包括一些非常短的异常值)。
    2. 还要排除一些长异常值(将它们保存在单独的地图中)。
    3. 由于长度可能不均匀分布,因此将霍夫曼编码用于值长度是合理的。
    4. 对于每个trie深度,固定长度或霍夫曼编码应该是不同的。

      N / 4子树长度实际上是值长度,因为N / 4个最小子树包含单个值。

      其他N / 4子树长度可以存储在固定(预定义)长度的单词中,因此对于大型子树,我们只知道近似(向上舍入)长度。

      对于2 30 全范围64位数字,我们必须打包大约34位值,对于3/4节点,大约。 4位值长度,对于每第四个节点,10位子树长度。这节省了34%的空间。


      示例值:

      0 320102
      1 5200100
      2 92010023
      3 112010202
      4 332020201
      5 332020411
      6 3833240522044511
      7 3833240522089999
      8 4000000000213312
      

      尝试这些价值观:

      root               d=320102           vl=19    tl=84+8+105+4+5=206
         +-l                                         tl=75+4+5=84
         | +-l                                       tl=23
         | | +-l
         | | | +-r       d=4879997          (vl=23)
         | | +-r         d=91689919         vl=27
         | |   +-r       d=20000178         (vl=25)
         | +-r           d=331700095        vl=29    tl=8
         |   +-l
         |   | +-r       d=209              (vl=8)
         |   +-r         d=3833240190024308 vl=52
         |     +-r       d=45487            (vl=16)
         +-r             d=3999999999893202 vl=52
      

      值长度编码:

                 bits start end
      Root       0    19    19
      depth 1    0    52    52
      depth 2    0    29    29
      depth 3    5    27    52
      depth 4    4    8     23
      

      子树长度各需要8位。

      这是编码流(为便于阅读,二进制值仍以十进制显示):

      bits value                      comment
      19   320102                     root value
      8    206                        left subtree length of the root
      8    84                         left subtree length
      4    15                         smallest left subtree length (with base value 8)
      23   4879997                    value for index 1
      5    0                          value length for index 2 (with base value 27)
      27   91689919                   value for index 2
      25   20000178                   value for index 3
      29   331700095                  value for index 4
      4    0                          smallest left subtree length (with base value 8)
      8    209                        value for index 5
      5    25                         value length for index 6 (with base value 27)
      52   3833240190024308           value for index 6
      16   45487                      value for index 7
      52   3999999999893202           value for index 8
      

      总共285位或5个64位字。我们还需要存储来自值长度编码表(350位)的位/起始值。要存储635位,我们需要10个64位字,这意味着无法压缩这么小的数字表。对于较大的数字表,值长度编码表的大小可以忽略不计。

      要搜索索引7的值,请读取根值(320102),跳过206位,为索引4添加值(331700095),跳过8位,为索引6添加值(3833240190024308),为索引7添加值( 45487),并添加索引(7)。结果是3 833 240 522 089 999,如预期的那样。

答案 1 :(得分:1)

我会在你的问题中概括出来。选择一个块大小 k ,您可以接受必须在平均 k / 2 值之前解码,然后再到达您之后的值。对于 n 总值,您将拥有 n / k 块。具有 n / k 条目的表将指向数据流以找到每个块的起始点。找到该表中的位置将是O(log( n / k ))用于二进制搜索,或者如果表足够小并且如果重要,则可以将其设为O(1 )使用辅助哈希表。

每个块都以一个起始的64位值开始。之后的所有值都将存储为前一个值的增量。我的建议是将这些增量存储为霍夫曼代码,该代码表示​​下一个值中有多少位,后跟那么多位。霍夫曼代码将针对每个块进行优化,并且该代码的描述将存储在块的开头。

您可以通过在每个值之前加上具有位数的六位来简化,在1..64的范围内,实际上是平坦的霍夫曼代码。根据比特长度的直方图,与扁平代码相比,优化的霍夫曼代码可以剔除大量的比特。

完成此设置后,您可以尝试 k ,看看您可以做多少,并且对压缩的影响仍然有限。

答案 2 :(得分:0)

我不知道这样做的数据结构。

获得空间而不是过于宽松的速度的明显解决方案是根据您存储的不同int大小创建具有不同数组大小的自己的结构。

<强>伪代码

class memoryAwareArray {
    array16 = Int16[] //2 bytes
    array32 = Int32[] //4 bytes
    array64 = Int64[] //8 bytes

    max16Index = 0;
    max32Index = 0;

    addObjectAtIndex(index, value) {
      if (value < 65535) {
        array16[max16Index] = value;
        max16Index++;
        return;
      }
      if (value < 2147483647) {
        array32[max32Index] = value;
        max32Index++;
        return;
      }

      array64[max64Index] = value;
      max64Index++;
    }

    getObject(index) {
      if (index < max16Index) return(array16[index]);
      if (index < max32Index) return(array32[index-max16Index]);
      return(array64[index-max16Index-max32Index]);
    }
}

沿着这些线条的东西不应该改变到很大的速度,如果你填满了整个结构,你将节省大约7千兆。你不会节省太多,因为你的价值当然会有差距。