我需要编写一个C ++函数,给定范围(a,b),返回AVL树中属于该给定范围的节点数,特别是log(n)时间复杂度。 如果我需要,我可以在树的节点上添加更多字段。
我应该指出a,b不一定会出现在树上。例如,如果树的节点是:1,2,5,7,9,10,那么使用参数(3,9)运行函数应返回3.
我应该使用哪种算法来实现这一目标?
答案 0 :(得分:2)
这是一个着名的问题 - dynamic order statistcs by tree augmentation。
您基本上需要扩充节点,以便在查看子指针时,您知道在时间O(1)处子节点子树中有多少个子节点。很容易看出这可以在不影响复杂性的情况下完成。
一旦你有了这个,你可以通过从节点到根执行两次遍历来回答任何查询(在此和之间,包含/排除 - 所有可能性)。确切的遍历取决于细节(例如,检查C ++中的函数lower_bound
和upper_bound
)。
答案 1 :(得分:1)
首先,您可以通过键操作实现拆分。也就是说,给定一棵树,执行split(tree, key, ts, tg)
将键分成两棵树; ts
包含小于key
的密钥; t2
更大或更平等的。该操作可以在O(lg n)中完成。
然后,通过两个分割,第一个在a上,第二个在b上,你可以在O(lg n)中获得所需的子集范围。
拆分可以按如下方式实现(伪代码):
void split(Node * root, const Key & key, Node *& ts, Node *& tg) noexcept
{
if (root == Node::NullPtr)
return;
if (key < KEY(root))
{
Node * r = RLINK(root), * tgaux = Node::NullPtr;
split(LLINK(root), key, ts, tgaux);
insert(tgaux, root); // insert root in tgaux
tg = join_ex(tgaux, r);
}
else
{ // ket greater or equal than key to tg
Node * l = LLINK(root), *tsaux = Node::NullPtr;
split(RLINK(root), key, tsaux, tg));
insert(tsaux, root); // insert root in tsaux
ts = join_ex(l, tsaux);
}
}
join_ex(t1, t2)
加入两棵独树;也就是说,t1的所有键都比树t2的任何键都小。该连接可以用O(lg n)实现,其方式类似于Knuth在TAOCP V3 6.2.3中描述的连接。
Grosso modo 如果你想加入l
和r
,那么假设h(l)&gt; H(R)。您从r
最左边的节点(最小)中删除。让j
此加入节点和r'
生成的树(r
- j
)。现在,您向r
的右侧下降,直到到达节点p
,使h(p) - h(r')
等于0或1.此时此刻
您将p
视为已插入。
编辑:我在解释这个问题时错了。抱歉。我没有看到计算不计算一组。以下是我的答案。我不会抹掉我写的东西,因为我认为它无论如何都很有用。
Ami Tavory是对的。
如果您使用扩展树,即在每个节点中存储子树基数,那么您可以轻松计算密钥的顺序positios。我通常会调用此操作position(key)
。如果key
不在集合中,则会返回key
在树中插入时所具有的位置。
root的inorder位置是左树的基数。
现在,为了计算[a,b)设置的基数,你执行position(b) - position(a)
。如果树中没有a或b,则可能需要进行一些调整。但基本上就是这样。
position(key)
“自然”简单。假设使用COUNT(node)
:
long position(Node * root, const Key & key) noexcept
{
if (r == Node::NullPtr)
return 0;
if (key < KEY(root))
return position(LLINK(r), key, p);
else if (KEY(r) < key)
return position(RLINK(r), key) + COUNT(LLINK(r)) + 1;
else // the root contains key
return COUNT(LLINK(r));
}
由于avl树是平衡的,位置需要O(lg n)。所以两个调用需要O(lg n)。非递归版本很简单。
我希望你知道原谅我的错误