为什么我的BST根指针因某种未知原因而改变?

时间:2012-11-02 02:08:54

标签: c pointers binary-search-tree

我正在尝试用C实现二进制搜索树数据结构,我遇到了一个错误。我的指针值因我不明白的原因而改变。 (请参阅帖子的底部,了解奇怪的输出[删除功能和主要功能明确输出来自哪里]) 我的测试功能如下:

int main(void)
{
    Bst *bst = ( Bst* ) calloc( 1, sizeof( Bst ) );
    BstInsert( bst, 7 );
    BstInsert( bst, 8 );
    BstInsert( bst, 2 );
    BstInsert( bst, 1 );
    BstTraverse( bst );
    BstRemove( bst, 7); 
    printf("=========================\n");
    printf("Root Key: %d\n", bst->key );
    printf("Left Key: %d\n", bst->left->key );
    printf("Right Key: %d\n", bst->right->key );
    printf("Location: %p\n", &bst);
    BstTraverse( bst );

    return 0;
}

我的删除节点功能如下:

void BstRemove( Bst *root, int key ){
    //Seems like recursive algorithm would need doubly linked bst implementation
    Bst *temp_node = BstFind( root, key );
    Bst *parent_node = BstGetParent( root, key );
    Bst *replacement_node = ( Bst* ) calloc( 1, sizeof( Bst ) );
    if ( temp_node->key == root->key )
    {   
        if (root->left) replacement_node = BstMax( root->left );
        else if ( root->right ) replacement_node = BstMin( root->right );
        else replacement_node = NULL;
    }
    else if ( temp_node->left )
    {
        replacement_node = BstMax( temp_node );
        Bst *parent_replacement_node = BstGetParent( root, replacement_node->key );
        parent_replacement_node->right = NULL;
    }
    else if ( temp_node->right )
    {
        replacement_node = BstMin( temp_node );
        Bst *parent_replacement_node = BstGetParent( root, replacement_node->key );
        parent_replacement_node->left = NULL;
    }
    else
        replacement_node = NULL;

    if ( parent_node && key < parent_node->key )
        parent_node->left = replacement_node;
    else if ( parent_node )
        parent_node->right = replacement_node;

    if ( replacement_node )
    {
        if ( root->left->key != replacement_node->key ) replacement_node->left = temp_node->left;
        if ( root->right->key != replacement_node->key ) replacement_node->right = temp_node->right;
    }
    root = replacement_node;
    printf("Root Key: %d\n", root->key );
    printf("Left Key: %d\n", root->left->key );
    printf("Right Key: %d\n", root->right->key );
    printf("Location: %p\n", &root);
    free(temp_node);
}

下面的输出:

1
2
7
8
Root Key: 2
Left Key: 1
Right Key: 8
Location: 0x7fffc5cf52e8
=========================
Root Key: 0
Left Key: 2
Right Key: 8
Location: 0x7fffc5cf5338
1
2
8
0
8

这让我困惑的原因是因为我正在使用指针。我认为root-&gt;键值没有理由在delete函数中为2时进行更改,并且一旦处理完毕     root-&gt; key变为0.我很感激任何能指出我的问题或帮助我朝正确方向前进的人。如有必要,您可以在https://github.com/PuffNotes/C/blob/master/data_structures/binary_tree.c查看我当前的BST实施情况。我最近开始尝试每天编程以获得一些技能,并认为自己是C的初学者(供参考)。谢谢。

2 个答案:

答案 0 :(得分:4)

您没有更改根节点指针。它通过值传递给remove函数,因为它肯定是删除的可行目标,所以它应该通过地址传递,因为它可能会更改为不同的节点。注意:如果我在那里错过了root我道歉,但你的编译应该抓住它。)

注意:我对没有验证传递了此代码是否正确甚至有效;但是真正的暗示是错误的是底部的root =,然后是打印输出,然后是调用者(main())执行相同的打印输出并显示不同的根指针值。 / p>

void BstRemove( Bst **root, int key )
{
    //Seems like recursive algorithm would need doubly linked bst implementation
    Bst *temp_node = BstFind( *root, key );
    Bst *parent_node = BstGetParent( *root, key );
    Bst *replacement_node = ( Bst* ) calloc( 1, sizeof( Bst ) );
    if ( temp_node->key == (*root)->key )
    {   
        if ((*root)->left) replacement_node = BstMax( (*root)->left );
        else if ( (*root)->right ) replacement_node = BstMin( (*root)->right );
        else replacement_node = NULL;
    }
    else if ( temp_node->left )
    {
        replacement_node = BstMax( temp_node );
        Bst *parent_replacement_node = BstGetParent( (*root), replacement_node->key );
        parent_replacement_node->right = NULL;
    }
    else if ( temp_node->right )
    {
        replacement_node = BstMin( temp_node );
        Bst *parent_replacement_node = BstGetParent( (*root), replacement_node->key );
        parent_replacement_node->left = NULL;
    }
    else
        replacement_node = NULL;

    if ( parent_node && key < parent_node->key )
        parent_node->left = replacement_node;
    else if ( parent_node )
        parent_node->right = replacement_node;

    if ( replacement_node )
    {
        if ( (*root)->left->key != replacement_node->key ) replacement_node->left = temp_node->left;
        if ( (*root)->right->key != replacement_node->key ) replacement_node->right = temp_node->right;
    }
    *root = replacement_node;

    printf("Root Key: %d\n", (*root)->key );
    printf("Left Key: %d\n", (*root)->left->key );
    printf("Right Key: %d\n", (*root)->right->key );
    printf("Location: %p\n", root);
    free(temp_node);
}

像这样调用它:

BstRemove( &bst, 7); 

并且习惯于通过地址传递root,因为当你开始编写平衡算法时,你会做很多

答案 1 :(得分:3)

@WhozCraig已经为问题的主旨提供了一个合适的答案,但我真的想帮助你解决一些其他问题。

第一步

好的,首先,关于code的一些非常重要的事情:

  • 撑。为了爱上帝,请在if..else语法上使用大括号。请参阅下面的BstInsert

    void BstInsert( Bst *root, int key )
    {
        if( !root->key )
            root->key = key;
        else if ( key <= root->key)
            if( root-> left )
                BstInsert( root->left, key );
            else
                root->left = NewNode( key );
        else
            if ( root -> right )
                BstInsert( root->right, key);
            else
                root->right = NewNode( key );
    }
    
  • 当您根据一个键是否小于或大于另一个键来编写走向BST的函数时,最重要的是您必须保持一致。在一个地方使用A < BA <= B可能是灾难性的。如果您选择一侧粘贴目标节点(您正在寻找的那个节点)并且始终以相同的方式进行比较,它也有助于提高可读性。

技术问题

内存分配可能会失败。如果是这样,各种分配方法(malloccalloc等)将返回NULL。你应该检查一下。请注意,calloc将内存初始化为零(清除它),而malloc则不会。对于这种情况(编写基本数据结构作为练习练习),我喜欢包装我的分配方法:

void *ecalloc(size_t n, size_t s) {
    void *o = calloc(n, s);
    if (NULL == o) {
    fprintf(stderr, "Memory allocation failed!\n");
    exit(EXIT_FAILURE);
    }
    return o;
}

这意味着a)我不必一直输入烦人的if (NULL == thing)分配检查,b)如果分配失败,程序将在打印消息后退出。后者可能并不总是令人满意的(好吧,戒烟部分,至少,尽管如果你的内存耗尽,你不会有很多选择),但在这种情况下绰绰有余。

设计问题

警告:此处松散地使用了术语Design

让我们说我们想要一个BST存储一些整数。您决定BST中的节点将存储一个整数和两个指针(到节点)。那样就好。但是,这意味着您无法合理地使用密钥作为标记值来确定是否使用了节​​点。

幸运的是,我们并不需要。当树为空时,不要使用节点作为根,而只需使用指向节点的指针。没有节点时,指针为NULL。这与您使用remove方法遇到的问题有关。

BST是一棵树,由链接节点组成,对吧?或者是吗?您还可以将其视为一个树,其中每个节点实际上都是一个子树。这使得它非常适合递归,所以让我们尽可能优雅地使用递归来表达事物。

现在,我们有几个选择。

  1. 我们可以创建一个不可变的bst(所有修改调用看起来都像b = bst_insert(b, 10)因为bst_insert等等都会返回树的新修改后的副本而不会改变旧的。)
  2. 我们可以在void bst_insert(bst **b, int key)中更多地使用,称为bst_insert(&b, 10),我们使用额外的间接级别来修改我们的树,方法是将指针传递给指向节点的指针。
  3. 或者我们可以在前两个选项之间找到一些东西,我们有bst *b(bst *b, int key),它可以修改*b(键和子指针)的内容,然后分配什么它不能。这避免了额外的间接(这有点难看),但如果您使用分配函数返回值和函数副作用的组合来实现您的目标,则有点不一致。
  4. 我选择了第二选项。

    调试

    假设您在BST中插入1。也许您删除2。你怎么知道这有效?如果你能看到你的BST正在做什么,那么调试会不会更容易?

    我建议(特别是在开始编写基本数据结构时,当像gdb这样的复杂调试环境可能是一种过度杀伤和b)信息过载时,你会编写很早就打印出数据结构状态的方法。

    此外,如果您正在运行* nix,valgrind(发音为&#34; Val grinned&#34;)是您最好的朋友。它是一个内存检查器,您可以使用它来确保您始终释放您分配的内存(当然,当您完成它时),并查找其他内存错误,例如超出范围。学会使用它(它实际上非常简单)。如果您使用的是Windows,那么虽然我不认为它是免费的,但我确实认为这个环境可以推荐一些东西。<\ n

    / p>

    编译器警告也很棒。打开它们就像错误一样对待它们。使用gcc时,我倾向于使用-W -Wall -ansi -pedantic。在相关的说明中,有-g生成GDB要使用的信息。

    写一个BST

    我打算翻阅你的代码并进行剖析,但最后我自己编写了一个类似风格的BST,然后我的代码解释了每个部分。我采用双文件方法。我们有bst.cbst.h

    bst.h

    这个位是这样的,如果在一个更大的系统中多次包含标题,我们不会偶然地尝试定义或声明相同的事情,并且我们也不会意外地如果我们有循环标头引用,则会导致无限的预处理器循环。

    #ifndef BST_H_
    #define BST_H_
    

    这里有一个typedef,它们可以让我们一直避免输入struct bstnode,并且只会隐藏使用您的BST的任何人struct bstnode类型的内容。

    typedef struct bstnode bst;
    

    extern说这些功能基本上是在其他地方实现的。

    extern bst *bst_new(int k);
    extern void bst_insert(bst **b, int k);
    extern bst *bst_search(bst  *b, int k);
    extern void bst_remove(bst **b, int k);
    extern void bst_delete(bst **b);
    extern void bst_newick(const bst  *b);
    
    #endif /* BST_H_ */
    

    bst.c

    #include <stdlib.h>
    #include <stdio.h>
    #include "bst.h"
    

    这里是完整的struct bstnode。我们可以访问bst typedef,因为我们已添加bst.h

    struct bstnode {
        int key;
        bst *left, *right;
    };
    

    在此上下文中,static表示这些函数具有文件范围。

    static void bst_swap_keys(bst *a, bst *b);
    static void bst_newick_rec(const bst *b);
    static void *ecalloc(size_t n, size_t s); /* Here for compactness - normally I would put it in a utility file somewhere else. */
    

    现在,我们可以轻松地将bst.h包含在另一个文件main.c中,并将主要方法放在那里,但为了紧凑,我没有。

    int main(void)
    {
        bst* b = bst_new(5);
    
        bst_newick(b);
        bst_insert(&b, 7);
    
        bst_newick(b);
        bst_insert(&b, 3);
    
        bst_insert(&b, 8);
        bst_insert(&b, 2);
        bst_insert(&b, 1);
        bst_newick(b);
    
        bst_remove(&b, 7);
        bst_newick(b);
    
        bst_delete(&b);
        printf("%p\n", (void*) b);
    
        return EXIT_SUCCESS;
    }
    

    以这种方式执行bst_new的缺点是,在创建第一个有效节点之前,您需要一个密钥。我们可以废弃bst_new并在bst_insert中完成分配,但我希望在此处保留new / delete范例。

    bst *bst_new(int k) {
    bst *b = ecalloc(1, sizeof *b);
    b->key = k;
    return b;
    

    }

    这是我们的插入方法。请记住,我尽可能地避免使用快捷方式,并且有很多方法可以使这些代码更紧凑。请注意对括号的强迫使用 - 这可能是额外的工作,但我建议它避免意外行为,尤其是在以后修改代码时。

    void bst_insert(bst **b, int k) {
        if (NULL == b) { /* I wanted to avoid additional levels of nesting so I did this instead of NULL != b */
        return;
        }
    
        if (NULL == *b) {
        *b = bst_new(k);
        } else if ((*b)->key > k) {
            bst_insert(&(*b)->left, k);
        } else if ((*b)->key < k) {
        bst_insert(&(*b)->right, k);
        }
    }
    

    尽可能找到一个节点。我本可以让b const表示我们不修改它,但是我也不得不改变返回类型,然后把它扔掉以修改我搜索的任何东西,这有点调皮。

    bst *bst_search(bst *b, int k) {
        if (NULL == b) {
        return NULL;
        } else if (b->key == k) {
        return b;
        } else if (b->key > k) {
        return bst_search(b->left, k);
        } else {
        return bst_search(b->right, k);
        }
    }
    

    这仅适用于bst_remove方法,但它也可以在此文件之外使用,因此也可以通过标题使用。

    bst *bst_min(bst *b) {
        if (NULL != b && NULL != b->left) {
        return bst_min(b->left);
        } else {
        return b;
        }
    }
    

    请注意,我们交换目标节点的密钥(正在删除的密钥)和应该替换它的密钥,而不是交换节点本身,然后再次递归删除目标值。如果键是字符串或在堆上分配的其他东西,您还需要在释放节点之前释放密钥。

    void bst_remove(bst **b, int k) {
        bst *temp;
        if (NULL == b || NULL == *b) { /* Doing it like this avoids extra indentation, which is harder to read*/
        return;
        }
        temp = *b;
    
        if ((*b)->key > k) {
        bst_remove(&(*b)->left, k);
        } else if ((*b)->key < k) {
        bst_remove(&(*b)->right, k);
        } else {
        if (NULL != (*b)->left && NULL != (*b)->right)
        {
            temp = bst_min((*b)->right);
            bst_swap_keys((*b), temp);
            bst_remove(&(*b)->right, k);
        }
        else if (NULL != (*b)->left)
        {
            *b = (*b)->left;
        }
        else if (NULL != (*b)->right)
        {
            *b = (*b)->right;
        }
        else
        {
            (*b) = NULL;
        }
        free(temp);
        }
    }
    

    bst_delete非常重要。它释放了您分配给传递给它的bst的所有内存。请记住,对于每个分配呼叫,还应该有一个免费呼叫。如果键是字符串或在堆上分配的其他东西,您还需要在释放节点之前释放密钥。

    void bst_delete(bst **b) {
        if (NULL == b) {
        return;
        }
        if (NULL != *b) {
        bst_delete(&(*b)->left);
        bst_delete(&(*b)->right);
        free(*b);
            *b = NULL;
        }
    }
    

    Newick format中打印BST并读取这些值对我来说总是感觉有点像黑客(因为L-> R和R-> L之间没有区别Newick ...),但我对它有一个情有独钟,我也习惯阅读它,并且发现它在过去的调试中很方便。除非你疯了,否则进行打印的方法应该在其排序中保持一致。这还演示了通过将递归工作拆分为单独的方法来包装递归任务,然后从公开的方法中调用该方法。后者处理其他不太适合递归的任务(例如,只打印一次分号和换行以及最高级别。)

    void bst_newick(const bst *b)
    {
        if (NULL != b)
        {
        bst_newick_rec(b);
        printf(";\n");
        }
        else
        {
        printf("NULL!\n");
        }
    }
    
    static void bst_newick_rec(const bst *b)
    {
        if (NULL == b) {
        return;
        }
    
        if (NULL != b->left || NULL != b->right) {
        printf("(");
        if (NULL != b->left && NULL != b->right) {
            bst_newick_rec(b->left);
            printf(",");
            bst_newick_rec(b->right);
        } else if (NULL != b->left) {
            bst_newick_rec(b->left);
        } else {
            bst_newick_rec(b->right);
        }
        printf(")");
        }
        printf("%d", b->key);
    }
    

    制作密钥交换方法实际上只是一个小小的便利。

    static void bst_swap_keys(bst *a, bst *b)
    {
        int temp;
        if (NULL != a && NULL != b && a != b)
        {
        temp = a->key;
        a->key = b->key;
        b->key = temp;
        }
    }
    
    static void *ecalloc(size_t n, size_t s) {
        void *o = calloc(n, s);
        if (NULL == o) {
        fprintf(stderr, "Memory allocation failed!\n");
        exit(EXIT_FAILURE);
        }
        return o;
    }
    

    请记住,这基本上已经在我的咖啡休息时间组装,并且没有经过严格的测试。我希望这会有所帮助。