我正在努力实现AVL搜索树。到目前为止,我已经完成了编码部分,我已经开始测试它的bug。我发现我的节点旋转方法被窃听了,为了上帝的缘故,我无法理解问题是什么。
该算法在纸上应用,但在机器上执行时它很好......泄漏树节点。
这是用于将节点向左旋转的方法:http://pastebin.com/mPHj29Af
bool avl_search_tree::avl_tree_node::rotate_left()
{
if (_right_child != NULL) {
avl_tree_node *new_root = _right_child;
if (_parent != NULL) {
if (_parent->_left_child == this) {
_parent->_left_child = new_root;
} else {
_parent->_right_child = new_root;
}
}
new_root->_parent = _parent;
_parent = new_root;
_right_child = new_root->_left_child;
new_root->_left_child = this;
if (_right_child != NULL) {
_right_child->_parent = this;
}
//update heights
update_height();
new_root->update_height();
return true;
}
return false;
}
在我的插入方法中,我评论了AVL平衡部分,而我只是试图将新插入的节点向左旋转。以升序插入整数的结果:我的树只包含初始根(插入的第一个节点),所有其他节点都被泄露。
在我开始发疯的时候,非常感谢任何帮助确定问题的方法。
对于记录:如果我不使用任何旋转,树将不会泄漏节点,它可以作为普通的非平衡二叉搜索树(用于插入和查找)。
编辑:由于AJG85的评论,我将添加观察结果:
我在avl_search_tree :: avl_tree_node的析构函数方法中添加了printf'检查',它将在清理之前打印键值(在我的情况下是32位整数),并且是avl_search_tree的insert方法,它将打印刚刚插入的键。
然后在程序的入口点,我在堆上分配一个avl_search_tree,并按升序添加键,然后将其删除。
启用AVL Balancing后,我在终端中获得以下输出:
bool avl_search_tree::insert(const int&) : 1
bool avl_search_tree::insert(const int&) : 2
bool avl_search_tree::insert(const int&) : 3
bool avl_search_tree::insert(const int&) : 4
bool avl_search_tree::insert(const int&) : 5
bool avl_search_tree::insert(const int&) : 6
bool avl_search_tree::insert(const int&) : 7
bool avl_search_tree::insert(const int&) : 8
avl_search_tree::avl_tree_node::~avl_tree_node() : 1
这意味着所有插入都成功,但只删除了根。
AVL Balancing注释掉它就像普通的二叉搜索树一样工作。终端输出为:
bool avl_search_tree::insert(const int&) : 1
bool avl_search_tree::insert(const int&) : 2
bool avl_search_tree::insert(const int&) : 3
bool avl_search_tree::insert(const int&) : 4
bool avl_search_tree::insert(const int&) : 5
bool avl_search_tree::insert(const int&) : 6
bool avl_search_tree::insert(const int&) : 7
bool avl_search_tree::insert(const int&) : 8
avl_search_tree::avl_tree_node::~avl_tree_node() : 1
avl_search_tree::avl_tree_node::~avl_tree_node() : 2
avl_search_tree::avl_tree_node::~avl_tree_node() : 3
avl_search_tree::avl_tree_node::~avl_tree_node() : 4
avl_search_tree::avl_tree_node::~avl_tree_node() : 5
avl_search_tree::avl_tree_node::~avl_tree_node() : 6
avl_search_tree::avl_tree_node::~avl_tree_node() : 7
avl_search_tree::avl_tree_node::~avl_tree_node() : 8
这意味着一切都得到了适当的清理。
现在......我怎么得出旋转方法是问题的结论?在注释的AVL平衡子程序下,我添加了一条线,将每个新插入的节点向左旋转。结果?与启用AVL Balancing子例程的情况相同。
关于update_height()方法,它不会以任何方式改变树的结构。
我希望这会澄清它。
编辑2:
为了澄清更多内容,他是如何实现avl_tree_node析构函数的:
avl_search_tree::avl_tree_node::~avl_tree_node()
{
printf("%s : %d\n", __PRETTY_FUNCTION__, *_key);
if (_left_child != NULL) {
delete _left_child;
}
if (_right_child != NULL) {
delete _right_child;
}
if (_key != NULL) {
delete _key;
}
}
_left_child和_right_child是指向堆上分配的avl_tree_node对象的指针。
编辑3:
感谢AGJ85的第2条评论,我发现了这个问题。在我的旋转方法中,我忘记了每当根移动时我实际上必须更新树的根指针到新的根。
基本上,树的根始终指向第一个插入的节点,并且在需要时不更新指针,我的旋转方法会泄漏实际配置正确的新树的根。 :)
谢谢AGJ85!
答案 0 :(得分:3)
感谢AGJ85的第2条评论,我发现了这个问题。在我的旋转方法中,我忘记了每当根移动时我实际上必须更新树的根指针到新的根。
基本上,树的根始终指向第一个插入的节点,并且在需要时不更新指针,我的旋转方法会泄漏实际配置正确的新树的根。 :)
答案 1 :(得分:2)
编辑 - 该死的 - 我没有看到问题已经解决(回答问题)。不过,也许有一些非答案提示值得打捞。
我没有彻底检查,但我认为你在这条线上出错...
_right_child = new_root->_left_child;
问题是您可能已经在行中覆盖了new_root->_left_child
...
_parent->_left_child = new_root;
我认为你应该做的是,一开始就有一大堆本地定义,比如......
avl_tree_node *orig_parent = _parent;
avl_tree_node *orig_this = this;
avl_tree_node *orig_left_child = _left_child;
avl_tree_node *orig_right_child = _right_child;
然后使用orig_
局部变量作为以后分配的来源。这节省了一定程度的担心在旋转期间通过各种指针的数据流。优化者应该摆脱任何值得担心的冗余工作,而且无论如何都没有多少。
几点加分......
首先,C ++(和C)标准保留带有前导下划线的标识符,并带有双下划线。据称,如果你不尊重它,你可以得到与标准和编译器提供的库的惊喜交互 - 我想这必须与成员标识符的宏相关。尾随下划线是可以的 - 我倾向于将它们用于包括警卫。
成员变量的常见约定是添加前导m
或m_
。更常见的可能是没有任何特殊的前缀或后缀。
其次,您可能(或可能不)发现更容易实现没有存储在节点中的父链接的AVL树。我自己还没有实现AVL树,但我确实曾经实施过红黑树。许多算法需要包括递归搜索作为第一步 - 您不能只执行标准搜索来记住找到的节点,而是丢弃到该节点的路由。但是,递归实现并不算太糟糕,并且指向更少的指针。
最后,一般提示 - 尝试“干运行”这样的算法很容易让你失望,除非你严格逐步完成它,并检查所有信息来源每一步都相关(我已经修改了吗?)很容易养成跳过一些细节以获得速度的习惯。一个有用的机器辅助干运行是在调试器中逐步运行代码,并查看每个步骤的结果是否与您的纸张干运行一致。
编辑 - 还有一个注释 - 我不会称之为小费,因为我不确定在这种情况下。我通常用简单的结构实现数据结构节点 - 没有数据隐藏,很少有成员函数。大多数代码与数据结构分开,通常在“工具”类中。我知道这打破了旧的“形状自我绘制”OOP原则,但IMO在实践中效果更好。
答案 2 :(得分:1)
我发现您在代码中找到了您要查找的错误。 (正如你所说,当root被更改时你没有更新树根指针指向新根。这是列表和树插入/删除方法的常用范例,用于返回指向列表头或树根的指针,如果你记得范式不会再犯错误。)
从更高的层面来看,我用来避免AVL Tree或Red-Black Tree代码问题的技术是改为使用与AA Tree具有相似性能的{{3}},使用O(n)空格和O(log n)时间进行插入,删除和搜索。但是,AA树的编码非常简单。