用于存储长序列(大多数是连续的)整数的高效数据结构

时间:2016-09-16 00:25:58

标签: algorithm data-structures language-agnostic

我想要一个有效存储长序列数据的数据结构。数字应该总是整数,让我们说Longs。

我想利用的输入功能(声称“效率”)是多数主要连续。可能缺少值。这些值可以无序地进行交互。

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

  • addVal(n):添加单个值n
  • addRange(n,m):添加n和m之间的所有值,包括
  • delVal(n):删除单个值n
  • delRange(n,m):删除n和m之间的所有值,包括
  • containsVal(n):返回结构
  • 中是否存在单个值n
  • containsRange(n,m):返回n和m之间的所有值,包括,是否存在于结构中

本质上,这是一种更具体的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)]

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

4 个答案:

答案 0 :(得分:6)

如果您不关心重复项,那么您的间隔时间不重叠。您需要创建一个保持不变的结构。如果您需要像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):返回结构中是否存在单个值n:O(log N)
  • containsRange(n,m):返回结构中是否存在n和m之间的所有值,包括:O(log N)

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

请注意,所有操作均为O(log N),插入范围[n,m]O(m-n)O(log(m-n))或类似内容完全相同。

我认为你不关心重复,只关心会员资格。否则,您可能需要间隔树或KD树或其他东西,但这些与浮点数据更相关...

答案 1 :(得分:3)

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

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

答案 2 :(得分:1)

间隔树似乎是为了存储重叠的间隔,而在你的情况下则没有意义。间隔树可以容纳数百万个小的重叠间隔,这些间隔一起仅形成少数较长的非重叠间隔。

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

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

  • 搜索新间隔起点的位置。
  • 如果它在现有间隔内,则继续查找终点的位置,同时扩展现有间隔并删除在途中传递的所有间隔。
  • 如果它位于现有间隔之间,请检查终点是否在下一个现有间隔之前。如果是,请创建一个新间隔。如果终点位于下一个现有间隔开始之后,则更改下一个间隔的起点,然后继续查找结束点,如上一段所述。

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

平均和最坏情况的复杂性是N / 2和N,其中N是链​​表中的间隔数。你可以通过添加一个方法来改进这一点,以避免迭代整个列表来找到起点;如果您知道值的范围和分布,这可能类似于哈希表;例如如果值从1到X且分布均匀,则存储长度为Y的表,其中每个项指向在值X / Y之前开始的间隔。添加间隔(A,B)时,您将查找表[A / Y]并从那里开始迭代链表。 Y值的选择取决于您想要使用多少空间,以及您想要达到起点实际位置的距离。然后复杂性将下降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>");

答案 3 :(得分:1)

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

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

E.g。如果minval = 0且maxval = 7,那么k = 0节点的k = 1个子节点代表[0..3]和[4..7]。他们在k = 2级的孩子是[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]

不难看出树的空间是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));
  }
}