用于存储一长串(大部分是连续的)整数的高效数据结构

Efficient data structure for storing a long sequence of (mostly consecutive) integers

我想要一个数据结构来有效地存储一长串数字。数字应始终为整数,比方说 Longs。

我想利用的输入特征(声称 'efficiency')是多头将 大部分 连续。可能存在缺失值。并且值可以乱序交互。

我希望数据结构支持以下操作:

本质上,这是一种更具体的 Set 数据结构,可以利用数据的连续性来使用少于 O(n) 的内存,其中 n 是存储的值的数量。

需要明确的是,虽然我认为此类数据结构的有效实现需要我们在内部存储间隔,但这对用户不可见或不相关。有一些区间树分别存储多个区间,并允许操作找到与给定点或区间重叠的区间数。但从用户的角度来看,这应该完全像一个集合(除了基于范围的操作,因此可以有效地处理批量添加和删除)。

示例:

dataStructure = ...
dataStructure.addRange(1,100) // [(1, 100)]
dataStructure.addRange(200,300) // [(1, 100), (200, 300)]
dataStructure.addVal(199) // [(1, 100), (199, 300)]
dataStructure.delRange(50,250) // [(1, 49), (251, 300)]

我的假设是这最好由一些基于树的结构来实现,但我对如何实现还没有很好的印象。 我想了解是否有一些常用的数据结构已经满足这个用例,因为我不想重新发明轮子。如果没有,我想听听您认为如何最好地实施它。

如果您不关心重复项,那么您的间隔是非重叠的。您需要创建一个保持不变的结构。如果您需要像 numIntervalsContaining(n) 这样的查询,那就是另一个问题了。

您可以使用存储端点对的 BST,就像在 C++ 中一样 std::set<std::pair<long,long>>。解释是每个条目对应区间[n,m]。您需要一个弱排序 - 它是左端点上通常的整数排序。单个 intlong n 被插入为 [n,n]。我们必须保持 属性 所有节点间隔都不重叠。下面简单评价一下你的操作顺序。由于您已经指定了 n 我使用 N 作为树的大小。

  • addVal(n): 添加单个值,n : O(log N),与 std::set<int> 相同。由于区间不重叠,需要找到n的前导,这可以在O(log n)时间内完成(按照https://www.quora.com/How-can-you-find-successors-and-predecessors-in-a-binary-search-tree-in-order中的案例分解)。查看此前身的右端点,并在必要时扩展区间或添加一个额外的节点 [n,n],按左端点排序,它始终是右子节点。请注意,如果间隔延长(将 [n+1,n+1] 插入到节点 [a,n] 形成节点 [a,n+1] 的树中),它现在可能会碰到下一个左端点,需要再次合并。所以有一些边缘情况需要考虑。比简单的 BST 稍微复杂一点,但仍然 O(log N)

  • addRange(n, m): O(log N),过程类似。如果插入的区间与另一个区间不平凡地相交,则合并区间以保持非重叠 属性。最坏情况下的性能是 O(n) 如下所述,因为我们插入的子间隔所消耗的子间隔数没有上限。

  • delVal(n): O(log N), 同样是 O(n) 最坏情况,因为我们不知道要删除的区间中包含多少区间。
  • delRange(n, m):删除 n 和 m 之间的所有值,包括:O(log N)
  • containsVal(n):return结构中是否存在单个值 n:O(log N)
  • containsRange(n, m): return n 和 m 之间的所有值是否存在于结构中:O(log N)

请注意,我们可以使用正确的 add() 和 addRange() 方法维护非重叠 属性,它已由 delete 方法维护。最坏的情况下我们需要 O(n) 个存储空间。

请注意,所有操作都是 O(log N),插入范围 [n,m]O(m-n)O(log(m-n)) 或类似的东西完全不同。

我假设您不关心重复项,只关心成员资格。否则你可能需要一个区间树或 KD 树或其他东西,但这些与浮点数据更相关......

间隔树似乎适合存储重叠间隔,而在您的情况下这没有意义。一棵区间树可以容纳数百万个小的重叠区间,它们一起只形成少数较长的非重叠区间。

如果您只想存储非重叠区间,那么添加或删除区间可能涉及删除新区间内的多个连续区间。因此,快速找到连续的间隔并有效删除可能存在的大量间隔非常重要。

这听起来像是不起眼的链表的工作。插入新间隔时,您将:

  • 搜索新区间起点的位置。
  • 如果在现有区间内,则继续寻找终点位置,同时延长现有区间,删除途中经过的所有区间。
  • 如果在现有间隔之间,请检查终点是否在下一个现有间隔之前。如果是,则创建一个新间隔。如果终点出现在下一个现有区间的开始之后,则更改下一个区间的起点,然后按照上一段中的说明继续寻找终点。

删除间隔大致相同:截断起点和终点所在的间隔,并删除其间的所有间隔。

这个的平均和最坏情况下的复杂度是N/2和N,其中N是链​​表中的区间数。您可以通过添加一种方法来改进这一点,以避免必须遍历整个列表才能找到起点;如果您知道值的范围和分布,这可能类似于散列 table;例如如果值是从 1 到 X 并且分布是均匀的,您将存储一个长度为 Y 的 table,其中每个项目都指向在值 X/Y 之前开始的间隔。添加间隔 (A,B) 时,您将查找 table[A/Y] 并从那里开始迭代链表。 Y 值的选择将取决于您要使用多少 space,以及您希望离起点的实际位置有多近。然后复杂性将下降 Y 倍。

(如果您使用的语言可以缩短链表,并且只留下您切断的对象链进行垃圾收集,您可以找到起点和终点的位置独立的,连接起来,跳过删除中间的所有区间。我不知道这样在实践中是否真的会提高速度。)


这是一个代码示例的开头,包含三个范围函数,但没有进一步优化:

function Interval(a, b, n) {
    this.start = a;
    this.end = b;
    this.next = n;
}

function IntervalList() {
    this.first = null;
}

IntervalList.prototype.addRange = function(a, b) {
    if (!this.first || b < this.first.start - 1) {
        this.first = new Interval(a, b, this.first); // insert as first element
        return;
    }
    var i = this.first;
    while (a > i.end + 1 && i.next && b >= i.next.start - 1) {
        i = i.next;                                  // locate starting point
    }
    if (a > i.end + 1) {                             // insert as new element
        i.next = new Interval(a, b, i.next);
        return;
    }
    var j = i.next;
    while (j && b >= j.start - 1) {                  // locate end point
        i.end = j.end;
        i.next = j = j.next;                         // discard overlapping interval
    }
    if (a < i.start) i.start = a;                    // update interval start
    if (b > i.end) i.end = b;                        // update interval end
}

IntervalList.prototype.delRange = function(a, b) {
    if (!this.first || b < this.first.start) return; // range before first interval
    var i = this.first;
    while (i.next && a > i.next.start) i = i.next;   // a in or after interval i
    if (a > i.start) {                               // a in interval
        if (b < i.end) {                             // range in interval -> split
            i.next = new Interval(b + 1, i.end, i.next);
            i.end = a - 1;
            return;
        }
        if (a <= i.end) i.end = a - 1;               // truncate interval
    }
    var j = a > i.start ? i.next : i;
    while (j && b >= j.end) j = j.next;              // b before or in interval j
    if (a <= this.first.start) this.first = j;       // short-circuit list
    else i.next = j;
    if (j && b >= j.start) j.start = b + 1;          // truncate interval
}

IntervalList.prototype.hasRange = function(a, b) {
    if (!this.first) return false;                   // empty list
    var i = this.first;
    while (i.next && a > i.end) i = i.next;          // a before or in interval i
    return a >= i.start && b <= i.end;               // range in interval ?
}

IntervalList.prototype.addValue = function(a) {
    this.addRange(a, a);                             // could be optimised
}

IntervalList.prototype.delValue = function(a) {
    this.delRange(a, a);                             // could be optimised
}

IntervalList.prototype.hasValue = function(a) {
    return this.hasRange(a, a);                      // could be optimised
}

IntervalList.prototype.print = function() {
    var i = this.first;
    if (i) do document.write("(" + i.start + "-" + i.end + ") "); while (i = i.next);
    document.write("<br>");
}

var intervals = new IntervalList();
intervals.addRange(100,199);
document.write("+ (100-199) &rarr; "); intervals.print();
intervals.addRange(300,399);
document.write("+ (300-399) &rarr; "); intervals.print();
intervals.addRange(200,299);
document.write("+ (200-299) &rarr; "); intervals.print();
intervals.delRange(225,275);
document.write("− (225-275) &rarr; "); intervals.print();
document.write("(150-200) ? " + intervals.hasRange(150,200) + "<br>");
document.write("(200-300) ? " + intervals.hasRange(200,300) + "<br>");

另一种选择可能是绳索数据结构 (https://en.m.wikipedia.org/wiki/Rope_(data_structure)),它似乎支持您要求的操作,在 O(log n) 时间内实现。与维基百科中的示例相反,您的示例将存储 [start,end] 而不是字符串子序列。

rope 的有趣之处在于它对区间内索引的高效查找。它通过从左到右对所有值位置进行排序来实现这一点——从低到高的定位(你的间隔将是一个简单的表示)可以是向上或向下,只要移动是向右的——以及依赖存储子树大小,它根据左侧的权重确定当前位置。通过更新和取消链接相关的树段,可以在 O(log n) 时间内用更大的包含间隔吞没部分间隔。

将每个间隔存储为一对(开始,结束)的问题在于,如果您添加一个包含 N 个先前存储的间隔的新 运行ge,则必须销毁这些间隔中的每一个,这需要N步,区间是存储在树、绳还是链表中。

(您可以将它们留给自动垃圾收集,但这也需要时间,并且仅适用于某些语言。)

一个可能的解决方案是将值(不是间隔的起点和终点)存储在 N-ary tree 中,其中每个节点代表一个 运行ge,并存储两个 N -位图,表示 N 个子运行ges 以及这些子运行ges 中的值是否全部存在、全部不存在或混合。在混合的情况下,将有一个指向表示此 rub-运行ge 的子节点的指针。

示例:(使用 branching factor 8 和高度 2 的树)

full range: 0-511 ; store interval 100-300

0-511:
  0- 63   64-127  128-191  192-255  256-319  320-383  384-447  448-511
   0      mixed      1        1      mixed      0        0        0

64-127:
 64- 71   72- 79   80- 87   88- 95   96-103  104-111  112-119  120-127
   0        0        0        0      mixed      1        1        1

96-103:
 96   97   98   99  100  101  102  103
  0    0    0    0    1    1    1    1

256-319:
256-263  264-271  272-279  280-287  288-295  296-303  304-311  312-319
   1        1        1        1        1      mixed      0        0

296-303:
296  297  298  299  300  301  302  303
  1    1    1    1    1    0    0    0

因此树将包含这五个节点:

- values: 00110000, mixed: 01001000, 2 pointers to sub-nodes  
  - values: 00000111, mixed: 00001000, 1 pointer to sub-node
    - values: 00001111, mixed: 00000000
  - values: 11111000, mixed: 00000100, 1 pointer to sub-node
    - values: 11111000, mixed: 00000000

以这种方式存储间隔的要点是您可以丢弃间隔而不必实际删除它。假设您添加了一个新的 运行ge 200-400;在这种情况下,您可以将根节点中的 运行ge 256-319 从 "mixed" 更改为“1”,而不删除或更新 256-319 和 296-303 节点本身;这些节点可以保留以供以后重新使用(或者断开连接并放入可重用子树队列中,或者在程序空闲或 运行 内存不足时在编程的垃圾收集中删除)。

查找区间时,您只需根据需要深入到树中;抬头时,例如225-275,你会发现 192-255 是无所不在的,256-319 是混合的,256-263 和 264-271 和 272-279 是无所不在的,你就会知道答案是正确的。由于这些值将存储为位图(一个用于 present/absent,一个用于 mixed/solid),因此可以仅通过几个按位比较来检查节点中的所有值。

重新使用节点:

如果一个节点有一个子节点,并且相应的值后来从mixed设置为all-absent或all-present,则子节点不再拥有相关值(但它被忽略)。当值改回 mixed 时,可以通过将其所有值设置为其在父节点中的值(在它更改为 mixed 之前)然后进行必要的更改来更新子节点。

在上面的示例中,如果我们添加 运行ge 0-200,这会将树更改为:

- values: 11110000, mixed: 00001000, 2 pointers to sub-nodes  
  - (values: 00000111, mixed: 00001000, 1 pointer to sub-node)
    - (values: 00001111, mixed: 00000000)
  - values: 11111000, mixed: 00000100, 1 pointer to sub-node
    - values: 11111000, mixed: 00000000

第二个和第三个节点现在包含过时的值,正在被忽略。如果再删除运行ge 80-95,根节点中运行ge 64-127的值又变成了mixed,运行ge 64-127的节点被重新使用。首先,我们将其中的所有值设置为全部存在(因为那是父节点的先前值),然后我们将 80-87 和 88-95 的值设置为全部不存在。 运行ge 96-103 的第三个节点仍未使用。

- values: 00110000, mixed: 01001000, 2 pointers to sub-nodes  
  - values: 11001111, mixed: 00000000, 1 pointer to sub-node
    - (values: 00001111, mixed: 00000000)
  - values: 11111000, mixed: 00000100, 1 pointer to sub-node
    - values: 11111000, mixed: 00000000

如果我们随后添加值 100,则第二个节点中 运行ge 96-103 的值将再次更改为 mixed,第三个节点将更新为 all-absent(其先前的值在第二个节点中)然后值 100 将被设置为呈现:

- values: 00110000, mixed: 01001000, 2 pointers to sub-nodes  
  - values: 11000111, mixed: 00001000, 1 pointer to sub-node
    - values: 00001000, mixed: 00000000
  - values: 11111000, mixed: 00000100, 1 pointer to sub-node
    - values: 11111000, mixed: 00000000

乍一看,与将间隔存储为 (start,end) 对的解决方案相比,此数据结构似乎使用了大量存储空间 space。但是,让我们看看(理论上的)最坏情况,在整个 64 位 运行ge 中,每个偶数都存在,每个奇数都不存在:

Total range: 0 ~ 18,446,744,073,709,551,615  
Intervals:        9,223,372,036,854,775,808  

将这些存储为(开始,结束)对的数据结构将使用:

Nodes:            9,223,372,036,854,775,808  
Size of node:                            16 bytes  
TOTAL:          147,573,952,589,676,412,928 bytes

如果数据结构使用通过(64 位)指针链接的节点,则将添加:

Data:           147,573,952,589,676,412,928 bytes
Pointers:        73,786,976,294,838,206,456 bytes
TOTAL:          221,360,928,884,514,619,384 bytes

具有 b运行ching 因子 64 的 N 叉树(最后一层为 16,得到总 运行ge 为 10×6 + 1×4 = 64 位)将使用:

Nodes (branch):         285,942,833,483,841  
Size of branch:                         528 bytes  
Nodes (leaf):        18,014,398,509,481,984  
Size of leaf:                           144 bytes  
TOTAL:            2,745,051,201,444,873,744 bytes  

比 (start,end) 对结构少 53.76 倍(或包括指针在内少 80.64 倍)。

计算是用下面的 N 叉树完成的:

Branch (9 levels):
value: 64-bit map
mixed: 64-bit map
sub-nodes: 64 pointers
TOTAL: 528 bytes
Leaf:
value: 64-bit map
mixed: 64-bit map
sub-nodes: 64 16-bit maps (more efficient than pointing to sub-node)
TOTAL: 144 bytes

这当然是最坏情况下的比较;一般情况在很大程度上取决于具体的输入。


这是我为测试这个想法而编写的第一个代码示例。节点有b运行ching factor 16,这样每层存储4位整数,无需不同的叶子和b运行ches就可以得到共同的位深度。例如,创建深度为 3 的树,表示 4×4 = 16 位的 运行ge。

function setBits(pattern, value, mask) {                 // helper function (make inline)
    return (pattern & ~mask) | (value ? mask : 0);
}

function Node(value) { // CONSTRUCTOR
    this.value = value ? 65535 : 0;                      // set all values to 1 or 0
    this.mixed = 0;                                      // set all to non-mixed
    this.sub = null;                                     // no pointer array yet
}

Node.prototype.prepareSub = function(pos, mask, value) {
    if ((this.mixed & mask) == 0) {                      // not mixed, child possibly outdated
        var prev = (this.value & mask) >> pos;
        if (value == prev) return false;                 // child doesn't require setting
        if (!this.sub) this.sub = [];                    // create array of pointers
        if (this.sub[pos]) {
            this.sub[pos].value = prev ? 65535 : 0;      // update child node values
            this.sub[pos].mixed = 0;
        }
        else this.sub[pos] = new Node(prev);             // create new child node
    }
    return true;                                         // child requires setting
}

Node.prototype.set = function(value, a, b, step) {
    var posA = Math.floor(a / step), posB = Math.floor(b / step);
    var maskA = 1 << posA, maskB = 1 << posB;
    a %= step; b %= step;

    if (step == 1) {                                     // node is leaf
        var vMask = (maskB | (maskB - 1)) ^ (maskA - 1); // bits posA to posB inclusive
        this.value = setBits(this.value, value, vMask);
    }
    else if (posA == posB) {                             // only 1 sub-range to be set
        if (a == 0 && b == step - 1)                     // a-b is full sub-range
        {
            this.value = setBits(this.value, value, maskA);
            this.mixed = setBits(this.mixed, 0, maskA);
        }
        else if (this.prepareSub(posA, maskA, value)) {  // child node requires setting
            var solid = this.sub[posA].set(value, a, b, step >> 4);     // recurse
            this.value = setBits(this.value, solid ? value : 0, maskA); // set value
            this.mixed = setBits(this.mixed, solid ? 0 : 1, maskA);     // set mixed
        }
    }
    else {                                               // multiple sub-ranges to set
        var vMask = (maskB - 1) ^ (maskA | (maskA - 1)); // bits between posA and posB
        this.value = setBits(this.value, value, vMask);  // set inbetween values
        this.mixed &= ~vMask;                            // set inbetween to solid
        var solidA = true, solidB = true;
        if (a != 0 && this.prepareSub(posA, maskA, value)) {          // child needs setting
            solidA = this.sub[posA].set(value, a, step - 1, step >> 4);
        }
        if (b != step - 1 && this.prepareSub(posB, maskB, value)) {   // child needs setting
            solidB = this.sub[posB].set(value, 0, b, step >> 4);
        }
        this.value = setBits(this.value, solidA ? value : 0, maskA);  // set value
        this.value = setBits(this.value, solidB ? value : 0, maskB);
        if (solidA) this.mixed &= ~maskA; else this.mixed |= maskA;   // set mixed
        if (solidB) this.mixed &= ~maskB; else this.mixed |= maskB;
    }
    return this.mixed == 0 && this.value == 0 || this.value == 65535; // solid or mixed
}

Node.prototype.check = function(a, b, step) {
    var posA = Math.floor(a / step), posB = Math.floor(b / step);
    var maskA = 1 << posA, maskB = 1 << posB;
    a %= step; b %= step;
    var vMask = (maskB - 1) ^ (maskA | (maskA - 1));     // bits between posA and posB

    if (step == 1) {
        vMask = posA == posB ? maskA : vMask | maskA | maskB;
        return (this.value & vMask) == vMask;
    }
    if (posA == posB) {
        var present = (this.mixed & maskA) ? this.sub[posA].check(a, b, step >> 4) : this.value & maskA;
        return !!present;
    }
    var present = (this.mixed & maskA) ? this.sub[posA].check(a, step - 1, step >> 4) : this.value & maskA;
    if (!present) return false;
    present = (this.mixed & maskB) ? this.sub[posB].check(0, b, step >> 4) : this.value & maskB;
    if (!present) return false;
    return (this.value & vMask) == vMask;
}

function NaryTree(factor, depth) { // CONSTRUCTOR
    this.root = new Node();
    this.step = Math.pow(factor, depth);
}

NaryTree.prototype.addRange = function(a, b) {
    this.root.set(1, a, b, this.step);
}

NaryTree.prototype.delRange = function(a, b) {
    this.root.set(0, a, b, this.step);
}

NaryTree.prototype.hasRange = function(a, b) {
    return this.root.check(a, b, this.step);
}

var intervals = new NaryTree(16, 3);                  // create tree for 16-bit range

// CREATE RANDOM DATA AND RUN TEST
document.write("Created N-ary tree for 16-bit range.<br>Randomly adding/deleting 100000 intervals...");
for (var test = 0; test < 100000; test++) {
    var a = Math.floor(Math.random() * 61440);
    var b = a + Math.floor(Math.random() * 4096);
    if (Math.random() > 0.5) intervals.addRange(a,b);
    else intervals.delRange(a,b);
}
document.write("<br>Checking a few random intervals:<br>");
for (var test = 0; test < 8; test++) {
    var a = Math.floor(Math.random() * 65280);
    var b = a + Math.floor(Math.random() * 256);
    document.write("Tree has interval " + a + "-" + b + " ? " + intervals.hasRange(a,b),".<br>");
}


我 运行 一个测试来检查正在创建的节点数,以及其中有多少是活动的或休眠的。我总共使用了 24 位的 运行ge(这样我可以在不 运行 内存不足的情况下测试最坏的情况),分为 6 级 4 位(因此每个节点有 16 个子- 运行ges);增加、删除或检查间隔时需要检查或更新的节点数为11或更少。该方案中的最大节点数为 1,118,481。

下图显示了保持 adding/deleting 运行dom 区间为 运行ge 1 (单个整数), 1~16, 1~256 时的活动节点数。 .. 1~16M(满运行ge)。

添加和删除单个整数(深绿色线)创建活动节点,接近最大 1,118,481 个节点,几乎没有节点处于休眠状态。在添加和删除大约 16M 个整数(= 运行ge 中的整数数)后达到最大值。

如果在更大的运行ge中添加和删除运行dom间隔,创建的节点数量大致相同,但更多节点处于休眠状态。如果在完整的1~16M 运行ge(亮黄色线)中添加运行dom间隔,无论你不断添加或删除多少间隔,任何时候都只有不到64个节点处于活动状态。

这已经给出了此数据结构相对于其他数据结构在哪里有用的想法:处于休眠状态的节点越多,在其他方案中需要删除的 intervals/nodes 就越多。

另一方面,它显示了这种数据结构对于某些 运行ges 以及输入的类型和数量来说可能过于 space- 效率低下。您可以引入休眠节点回收系统,但这会剥夺休眠节点可立即重复使用的优势。


N叉树中有很多space被指向子节点的指针占用了。如果完整的 运行ge 足够小,您可以将树存储在数组中。对于 32 位 运行ge,需要 580 MB("value" 位图需要 546 MB,"mixed" 位图需要 34 MB)。这更 space 高效,因为您只存储位图,并且不需要指向子节点的指针,因为所有内容在数组中都有固定的位置。您将拥有深度为 7 的树的优势,因此任何操作都可以通过检查 15 "nodes" 或更少来完成,并且在 add/delete/check 操作期间不需要创建或删除任何节点。

这是我用来尝试数组中的 N 叉树想法的代码示例;对于 32 位 运行ge(不幸的是,运行ge 超过 40 位左右),它使用 580MB 来存储具有 b运行ching 因子 16 和深度 7 的 N 叉树可能超出任何普通计算机的内存容量)。除了请求的函数之外,它还可以使用 notValue() 和 notRange() 检查间隔是否完全不存在。

#include <iostream>

inline uint16_t setBits(uint16_t pattern, uint16_t mask, uint16_t value) {
    return (pattern & ~mask) | (value & mask);
}

class NaryTree32 {
    uint16_t value[0x11111111], mixed[0x01111111];

    bool set(uint32_t a, uint32_t b, uint16_t value = 0xFFFF, uint8_t bits = 28, uint32_t offset = 0) {
        uint8_t posA = a >> bits, posB = b >> bits;
        uint16_t maskA = 1 << posA, maskB = 1 << posB;
        uint16_t mask = maskB ^ (maskA - 1) ^ (maskB - 1);

        // IF NODE IS LEAF: SET VALUE BITS AND RETURN WHETHER VALUES ARE MIXED
        if (bits == 0) {
            this->value[offset] = setBits(this->value[offset], mask, value);
            return this->value[offset] != 0 && this->value[offset] != 0xFFFF;
        }

        uint32_t offsetA = offset * 16 + posA + 1, offsetB = offset * 16 + posB + 1;
        uint32_t subMask = ~(0xFFFFFFFF << bits);
        a &= subMask; b &= subMask;

        // IF SUB-RANGE A IS MIXED OR HAS WRONG VALUE
        if (((this->mixed[offset] & maskA) != 0 || (this->value[offset] & maskA) != (value & maskA))
        && (a != 0 || posA == posB && b != subMask)) {

            // IF SUB-RANGE WAS PREVIOUSLY SOLID: UPDATE TO PREVIOUS VALUE
            if ((this->mixed[offset] & maskA) == 0) {
                this->value[offsetA] = (this->value[offset] & maskA) ? 0xFFFF : 0x0000;
                if (bits != 4) this->mixed[offsetA] = 0x0000;
            }
            // RECURSE AND IF SUB-NODE IS MIXED: SET MIXED BIT AND REMOVE A FROM MASK
            if (this->set(a, posA == posB ? b : subMask, value, bits - 4, offsetA)) {
                this->mixed[offset] |= maskA;
                mask ^= maskA;
            }
        }
        // IF SUB-RANGE B IS MIXED OR HAS WRONG VALUE
        if (((this->mixed[offset] & maskB) != 0 || (this->value[offset] & maskB) != (value & maskB))
        && b != subMask && posA != posB) {

            // IF SUB-RANGE WAS PREVIOUSLY SOLID: UPDATE SUB-NODE TO PREVIOUS VALUE
            if ((this->mixed[offset] & maskB) == 0) {
                this->value[offsetB] = (this->value[offset] & maskB) ? 0xFFFF : 0x0000;
                if (bits > 4) this->mixed[offsetB] = 0x0000;
            }
            // RECURSE AND IF SUB-NODE IS MIXED: SET MIXED BIT AND REMOVE A FROM MASK
            if (this->set(0, b, value, bits - 4, offsetB)) {
                this->mixed[offset] |= maskB;
                mask ^= maskB;
            }
        }
        // SET VALUE AND MIXED BITS THAT HAVEN'T BEEN SET YET AND RETURN WHETHER NODE IS MIXED
        if (mask) {
            this->value[offset] = setBits(this->value[offset], mask, value);
            this->mixed[offset] &= ~mask;
        }
        return this->mixed[offset] != 0 || this->value[offset] != 0 && this->value[offset] != 0xFFFF;
    }

    bool check(uint32_t a, uint32_t b, uint16_t value = 0xFFFF, uint8_t bits = 28, uint32_t offset = 0) {
        uint8_t posA = a >> bits, posB = b >> bits;
        uint16_t maskA = 1 << posA, maskB = 1 << posB;
        uint16_t mask = maskB ^ (maskA - 1) ^ (maskB - 1);

        // IF NODE IS LEAF: CHECK BITS A TO B INCLUSIVE AND RETURN
        if (bits == 0) {
            return (this->value[offset] & mask) == (value & mask);
        }

        uint32_t subMask = ~(0xFFFFFFFF << bits);
        a &= subMask; b &= subMask;

        // IF SUB-RANGE A IS MIXED AND PART OF IT NEEDS CHECKING: RECURSE AND RETURN IF FALSE
        if ((this->mixed[offset] & maskA) && (a != 0 || posA == posB && b != subMask)) {
            if (this->check(a, posA == posB ? b : subMask, value, bits - 4, offset * 16 + posA + 1)) {
                mask ^= maskA;
            }
            else return false;
        }
        // IF SUB-RANGE B IS MIXED AND PART OF IT NEEDS CHECKING: RECURSE AND RETURN IF FALSE
        if (posA != posB && (this->mixed[offset] & maskB) && b != subMask) {
            if (this->check(0, b, value, bits - 4, offset * 16 + posB + 1)) {
                mask ^= maskB;
            }
            else return false;
        }
        // CHECK INBETWEEN BITS (AND A AND/OR B IF NOT YET CHECKED) WHETHER SOLID AND CORRECT
        return (this->mixed[offset] & mask) == 0 && (this->value[offset] & mask) == (value & mask);
    }

    public:

    NaryTree32() { // CONSTRUCTOR: INITIALISES ROOT NODE
        this->value[0] = 0x0000;
        this->mixed[0] = 0x0000;
    }

    void addValue(uint32_t a)             {this->set(a, a);}
    void addRange(uint32_t a, uint32_t b) {this->set(a, b);}
    void delValue(uint32_t a)             {this->set(a, a, 0);}
    void delRange(uint32_t a, uint32_t b) {this->set(a, b, 0);}
    bool hasValue(uint32_t a)             {return this->check(a, a);}
    bool hasRange(uint32_t a, uint32_t b) {return this->check(a, b);}
    bool notValue(uint32_t a)             {return this->check(a, a, 0);}
    bool notRange(uint32_t a, uint32_t b) {return this->check(a, b, 0);}
};

int main() {
    NaryTree32 *tree = new NaryTree32();

    tree->addRange(4294967280, 4294967295);
    std::cout << tree->hasRange(4294967280, 4294967295) << "\n";
    tree->delValue(4294967290);
    std::cout << tree->hasRange(4294967280, 4294967295) << "\n";
    tree->addRange(1000000000, 4294967295);
    std::cout << tree->hasRange(4294967280, 4294967295) << "\n";
    tree->delRange(2000000000, 4294967280);
    std::cout << tree->hasRange(4294967280, 4294967295) << "\n";

    return 0;
}

我很惊讶没有人建议 segment trees 存储值的整数域。 (当用于 2d 和 3d 图形等几何应用程序时,它们分别称为四叉树和八叉树。)插入、删除和查找的时间和 space 复杂度与 (maxval - minval) 中的位数成正比),即log_2(maxval - minval),整数数据域的最大值和最小值。

简而言之,我们在 [minval, maxval] 中编码一组整数。最顶层 0 的节点代表整个范围。每个连续级别的节点代表 sub-ranges 的近似大小 (maxval - minval) / 2^k。当包含一个节点时,它对应值的某些子集是表示集的一部分。当它是一片叶子时,它的 all 个值都在集合中。当它不存在时,none 是。

例如如果minval=0且maxval=7,则k=0节点的k=1children表示[0..3]和[4..7]。它们在k=2层的children是[0..1][2..3][4..5]和[6..7],k=3个节点代表单个元素。集合 {[1..3], [6..7]} 将是树(从左到右的级别):

[0..7] -- [0..3] -- [0..1]
       |         |        `-[1] 
       |         `- [2..3]
        ` [4..7]
                 `- [6..7]

不难看出树的 space 将是 O(m log (maxval - minval)),其中 m 是存储在树中的间隔数。

使用带动态插入和删除的线段树并不常见,但事实证明算法相当简单。需要注意确保节点数量最少。

这里是一些经过简单测试的 java 代码。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class SegmentTree {
  // Shouldn't differ by more than Long.MAX_VALUE to prevent overflow.
  static final long MIN_VAL = 0;
  static final long MAX_VAL = Long.MAX_VALUE;
  Node root;

  static class Node {
    Node left;
    Node right;
    Node(Node left, Node right) {
      this.left = left;
      this.right = right;
    }
  }

  private static boolean isLeaf(Node node) {
    return node != null && node.left == null && node.right == null;
  }

  private static Node reset(Node node, Node left, Node right) {
    if (node == null) {
      return new Node(left, right);
    }
    node.left = left;
    node.right = right;
    return node;
  }

  /**
   * Accept an arbitrary subtree rooted at a node representing a subset S of the range [lo,hi] and
   * transform it into a subtree representing S + [a,b]. It's assumed a >= lo and b <= hi.
   */
  private static Node add(Node node, long lo, long hi, long a, long b) {
    // If the range is empty, the interval tree is always null.
    if (lo > hi) return null;
    // If this is a leaf or insertion is outside the range, there's no change.
    if (isLeaf(node) || a > b || b < lo || a > hi) return node;
    // If insertion fills the range, return a leaf.
    if (a == lo && b == hi) return reset(node, null, null);
    // Insertion doesn't cover the range. Get the children, if any.
    Node left = null, right = null;
    if (node != null) {
      left = node.left;
      right = node.right;
    }
    // Split the range and recur to insert in halves.
    long mid = lo + (hi - lo) / 2;
    left = add(left, lo, mid, a, Math.min(b, mid));
    right = add(right, mid + 1, hi, Math.max(a, mid + 1), b);
    // Build a new node, coallescing to leaf if both children are leaves.
    return isLeaf(left) && isLeaf(right) ? reset(node, null, null) : reset(node, left, right);
  }

  /**
   * Accept an arbitrary subtree rooted at a node representing a subset S of the range [lo,hi] and
   * transform it into a subtree representing range(s) S - [a,b].  It's assumed a >= lo and b <= hi.
   */
  private static Node del(Node node, long lo, long hi, long a, long b) {
    // If the tree is null, we can't remove anything, so it's still null
    // or if the range is empty, the tree is null.
    if (node == null || lo > hi) return null;
    // If the deletion is outside the range, there's no change.
    if (a > b || b < lo || a > hi) return node; 
    // If deletion fills the range, return an empty tree.
    if (a == lo && b == hi) return null;
    // Deletion doesn't fill the range. 
    // Replace a leaf with a tree that has the deleted portion removed. 
    if (isLeaf(node)) {
      return add(add(null, lo, hi, b + 1, hi), lo, hi, lo, a - 1);
    }
    // Not a leaf. Get children, if any.
    Node left = node.left, right = node.right;
    long mid = lo + (hi - lo) / 2;
    // Recur to delete in child ranges.
    left = del(left, lo, mid, a, Math.min(b, mid));
    right = del(right, mid + 1, hi, Math.max(a, mid + 1), b);
    // Build a new node, coallescing to empty tree if both children are empty.
    return left == null && right == null ? null : reset(node, left, right);
  }

  private static class NotContainedException extends Exception {};

  private static void verifyContains(Node node, long lo, long hi, long a, long b)
      throws NotContainedException {
    // If this is a leaf or query is empty, it's always contained.
    if (isLeaf(node) || a > b) return;
    // If tree or search range is empty, the query is never contained.
    if (node == null || lo > hi) throw new NotContainedException();
    long mid = lo + (hi - lo) / 2;
    verifyContains(node.left, lo, mid, a, Math.min(b, mid));
    verifyContains(node.right, mid + 1, hi, Math.max(a, mid + 1), b);
  }

  SegmentTree addRange(long a, long b) {
    root = add(root, MIN_VAL, MAX_VAL, Math.max(a, MIN_VAL), Math.min(b, MAX_VAL));
    return this;
  }

  SegmentTree addVal(long a) {
    return addRange(a, a);
  }

  SegmentTree delRange(long a, long b) {
    root = del(root, MIN_VAL, MAX_VAL, Math.max(a, MIN_VAL), Math.min(b, MAX_VAL));
    return this;
  }

  SegmentTree delVal(long a) {
    return delRange(a, a);
  }

  boolean containsVal(long a) {
    return containsRange(a, a);
  }

  boolean containsRange(long a, long b) {
    try {
      verifyContains(root, MIN_VAL, MAX_VAL, Math.max(a, MIN_VAL), Math.min(b, MAX_VAL));
      return true;
    } catch (NotContainedException expected) {
      return false;
    }
  }

  private static final boolean PRINT_SEGS_COALESCED = true;

  /** Gather a list of possibly coalesced segments for printing. */
  private static void gatherSegs(List<Long> segs, Node node, long lo, long hi) {
    if (node == null) {
      return;
    }
    if (node.left == null && node.right == null) {
      if (PRINT_SEGS_COALESCED && !segs.isEmpty() && segs.get(segs.size() - 1) == lo - 1) {
        segs.remove(segs.size() - 1);
      } else {
        segs.add(lo);
      }
      segs.add(hi);
    } else {
      long mid = lo + (hi - lo) / 2;
      gatherSegs(segs, node.left, lo, mid);
      gatherSegs(segs, node.right, mid + 1, hi);
    }
  }

  SegmentTree print() {
    List<Long> segs = new ArrayList<>();
    gatherSegs(segs, root, MIN_VAL, MAX_VAL);
    Iterator<Long> it = segs.iterator();
    while (it.hasNext()) {
      long a = it.next();
      long b = it.next();
      System.out.print("[" + a + "," + b + "]");
    }
    System.out.println();
    return this;
  }

  public static void main(String [] args) {
    SegmentTree tree = new SegmentTree()
        .addRange(0, 4).print()
        .addRange(6, 7).print()
        .delVal(2).print()
        .addVal(5).print()
        .addRange(0,1000).print()
        .addVal(5).print()
        .delRange(22, 777).print();
    System.out.println(tree.containsRange(3, 20));
  }
}