计算给定范围内 AVL 树中的节点数
Posted
技术标签:
【中文标题】计算给定范围内 AVL 树中的节点数【英文标题】:Count the number of nodes in an AVL tree in a given range 【发布时间】:2015-06-14 21:06:55 【问题描述】:我需要编写一个 C++ 函数,在给定范围 (a,b] 的情况下,它返回 AVL 树中在给定范围内的节点数,特别是 log(n) 时间复杂度。 如果需要,我可以向树的节点添加更多字段。
我应该指出 a,b 不一定会出现在树中。例如,如果树的节点是:1,2,5,7,9,10,那么使用参数 (3,9] 运行函数应该返回 3。
我应该使用哪种算法来实现这一点?
【问题讨论】:
How can I write this function?
请提供一个代码示例,说明您到目前为止所做的工作。我们不会为你写的。
我澄清了我的问题
@mittelmania 我误解了最初的问题。我编辑了我的答案。基本上,Ami Tavory 是对的
更有趣的是:给定一个标准的 AVL 树,在另外两个键之间的半开间隔内给出树中键的计数的严格下限和上限。
【参考方案1】:
这是一个著名的问题 - dynamic order statistcs by tree augmentation。
您基本上需要扩充您的节点,以便当您查看子指针时,您可以知道在 O(1) 时子树的子树中有多少子节点。很容易看出,这可以在不影响复杂性的情况下完成。
一旦你有了它,你就可以通过执行从节点到根的两次遍历来回答任何查询(在这个和那个之间,包含/排除 - 所有可能性)。确切的遍历取决于细节(例如检查 C++ 中的函数 lower_bound
和 upper_bound
)。
【讨论】:
我认为如果你不需要中序位置那么你不需要扩展 如果操作是(真的)const
,那么我不知道如何避免它。
我误解了这个问题。我解释为计算一个集合,而不是计算它的基数。所以,你是对的【参考方案2】:
首先,您可以实现按键拆分操作。也就是说,给定一棵树,执行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) > h(r)。您从r
中删除最左边的节点(最小值)。让j
这个加入节点 和r'
结果树(r
- j
)。现在你从r
的右侧下降直到到达一个节点p
使得h(p) - h(r')
等于0 或1。此时你这样做了
而您将p
视为已插入。
编辑:我对问题的解释是错误的。对不起。没看出算不算一套。以下将是我的答案。我不会抹掉我写的东西,因为我觉得它还是有用的。
Ami Tavory 是对的。
如果您使用扩展树,即在每个节点中存储子树基数,那么您可以轻松计算键的中序位置。我通常会调用此操作position(key)
。如果key
不在集合中,则返回key
在树中插入时的位置。
根的中序位置是左树的基数。
现在,为了计算 [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)。非递归版本很简单。
希望你知道原谅我的错误
【讨论】:
它当然有它的优点。不过,据推测,这样的操作应该适用于const
操作。您的解决方案至少必须 const_cast
的东西。我也看不出它如何并行工作。那么利弊。不过这个主意不错。
当然。你应该玩const_cast
,提取结果然后重构原始树(这可以在 O(lg n) 中完成。关于缺点,当然,我发现 avl 树很难获得并发利润。当然这个算法非常复杂。相反,红黑树有利于并发,并且拆分更简单-是的,如果您在每个节点中添加基数,那么您很可能可以更轻松地解决问题,尽管空间更大,也许更多时间(但仍然是 O(lg n))以上是关于计算给定范围内 AVL 树中的节点数的主要内容,如果未能解决你的问题,请参考以下文章