我想要一个有效存储长序列数据的数据结构。数字应该总是整数,让我们说Longs。
我想利用的输入功能(声称“效率”)是多数主要连续。可能缺少值。这些值可以无序地进行交互。
我希望数据结构支持以下操作:
本质上,这是一种更具体的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)]
我的假设是最好通过一些基于树的结构来实现,但我对如何做到这一点没有很好的印象。 我想知道是否有一些常用的数据结构已经满足了这个用例,因为我宁愿不重新发明轮子。如果没有,我想听听您认为最好如何实施。
答案 0 :(得分:6)
如果您不关心重复项,那么您的间隔时间不重叠。您需要创建一个保持不变的结构。如果您需要像numIntervalsContaining(n)这样的查询,那么这是一个不同的问题。
您可以使用存储端点对的BST,就像在C ++ std::set<std::pair<long,long>>
中一样。解释是每个条目对应于区间[n,m]
。您需要一个弱排序 - 它是左端点上通常的整数排序。单int
或long
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)
如下所述,因为我们插入的子区间数没有上限。
O(log N)
,再次O(n)
最糟糕的情况,因为我们不知道我们要删除的时间间隔中包含多少个时间间隔。O(log N)
O(log N)
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) → "); intervals.print();
intervals.addRange(300,399);
document.write("+ (300-399) → "); intervals.print();
intervals.addRange(200,299);
document.write("+ (200-299) → "); intervals.print();
intervals.delRange(225,275);
document.write("− (225-275) → "); 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));
}
}