二叉树旋转

时间:2011-08-02 17:19:34

标签: c++ algorithm binary-search-tree avl-tree

我正在努力实现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!

3 个答案:

答案 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)标准保留带有前导下划线的标识符,并带有双下划线。据称,如果你不尊重它,你可以得到与标准和编译器提供的库的惊喜交互 - 我想这必须与成员标识符的宏相关。尾随下划线是可以的 - 我倾向于将它们用于包括警卫。

成员变量的常见约定是添加前导mm_。更常见的可能是没有任何特殊的前缀或后缀。

其次,您可能(或可能不)发现更容易实现没有存储在节点中的父链接的AVL树。我自己还没有实现AVL树,但我确实曾经实施过红黑树。许多算法需要包括递归搜索作为第一步 - 您不能只执行标准搜索来记住找到的节点,而是丢弃到该节点的路由。但是,递归实现并不算太糟糕,并且指向更少的指针。

最后,一般提示 - 尝试“干运行”这样的算法很容易让你失望,除非你严格逐步完成它,并检查所有信息来源每一步都相关(我已经修改了吗?)很容易养成跳过一些细节以获得速度的习惯。一个有用的机器辅助干运行是在调试器中逐步运行代码,并查看每个步骤的结果是否与您的纸张干运行一致。

编辑 - 还有一个注释 - 我不会称之为小费,因为我不确定在这种情况下。我通常用简单的结构实现数据结构节点 - 没有数据隐藏,很少有成员函数。大多数代码与数据结构分开,通常在“工具”类中。我知道这打破了旧的“形状自我绘制”OOP原则,但IMO在实践中效果更好。

答案 2 :(得分:1)

我发现您在代码中找到了您要查找的错误。 (正如你所说,当root被更改时你没有更新树根指针指向新根。这是列表和树插入/删除方法的常用范例,用于返回指向列表头或树根的指针,如果你记得范式不会再犯错误。)

从更高的层面来看,我用来避免AVL TreeRed-Black Tree代码问题的技术是改为使用与AA Tree具有相似性能的{{3}},使用O(n)空格和O(log n)时间进行插入,删除和搜索。但是,AA树的编码非常简单。