【问题标题】:Why is my BST root pointer changing for some unknown reason?为什么我的 BST 根指针由于某种未知原因而发生变化?
【发布时间】:2012-10-22 16:53:24
【问题描述】:

我正在尝试在 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-> 键值在删除函数中为 2 时没有理由改变,并且一旦被处理
root->key 变为 0。我感谢任何能指出我的问题或帮助我朝正确方向前进的人。如有必要,您可以在 https://github.com/PuffNotes/C/blob/master/data_structures/binary_tree.c 查看我当前的 BST 实现。我最近开始尝试每天编程以获取一些技能,并认为自己是 C 的初学者(供参考)。谢谢。

【问题讨论】:

  • 老实说,我不确定有什么区别,除了 calloc 明确表示我只为一个元素存储内存。只为一个元素分配堆空间并为多个元素分配 calloc 时使用 malloc 是否约定?编辑:对不起,我认为我覆盖了某人的评论而不是评论,他的问题是,你为什么使用 calloc 而不是 malloc?
  • fwiw malloc 和 calloc 的区别在于 calloc 将所有分配的空间设置为 0,而 malloc 不提供任何关于分配内存内容的保证
  • 这听起来很奇怪,但请忍耐一下:您是否在您的 Bst 树中初始化 insert 是利用您在程序的第一行分配的空根节点,还是根据比较结果,它会立即开始向左或向右插入。如果你不明白我为什么要问,我会在后面给出猜测的答案。
  • 我正在利用根节点来初始化它。我的 BstInsert 函数的前两行是 if( !root->key ) root->key = key;如果有人喜欢的话,我也会在 pastebin 上发布产生这个问题的代码。 pastebin.com/CjCmyBWK
  • 老实说,除了您所看到的之外,还有很多其他问题。首先在if... else 逻辑中使用大括号,尤其是在BstInsert 中,并且考虑到0 是一个神奇的无效整数值(!root-key)也是你应该重新考虑的事情。

标签: c pointers binary-search-tree


【解决方案1】:

你没有改变你的根节点指针。它按值传递给删除函数,并且由于它肯定是删除的可行目标,因此应该按地址传递,因为它可能会更改到不同的节点。注意:如果我在某处遗漏了root,我深表歉意,但您的编译器应该会捕捉到它)。

注意:我 no 验证此代码是否正确或是否有效;但真正的提示是底部的root =,然后是打印输出,然后调用者 (main()) 执行相同的打印输出并显示不同的根指针值。

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); 

并习惯于通过地址传递根,当你开始编写平衡算法时,你会大量这样做。

【讨论】:

  • 是的,你是对的,谢谢你的洞察力。这就是我在实现堆栈和队列数据结构以消除使用赋值来更新数据结构时所做的,(尽管我没有使用 & 通过引用传递,而是为指向我的结构的指针创建了一个单独的数据类型,是否有约定?),我没有尝试这个,因为我的 BstInsert 函数工作正常。我仍然很困惑为什么 BstInsert 有效(因为我仍然更改了 root 的属性),但不是 BstRemove,我将继续分析问题,但任何进一步的反馈总是值得赞赏的。非常感谢。
  • @Miles 约定是,如果它是一个指针,则使其对代码阅读器显而易见,它就是它的本质,通过名称,通过声明,你可以命名它。从长远来看,typedef Bst *BstPtr; 等类型可以说是代码清晰度的障碍。我更喜欢只看到func(Bst *bstp),但没有固定的正确方法来做到这一点。使用最适合您那些需要阅读您的代码的人。最后,一旦你开始平衡你的树,你的 BstInsert 将无法工作。您可能一直在更改根节点中的 data,而不是在插入代码中滑动新的根节点指针。
【解决方案2】:

@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 &lt; BA &lt;= 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(&amp;b, 10),我们使用额外的间接级别通过传入指向节点的指针来修改我们的树。
  3. 或者我们可以在前两个选项之间选择一些东西,我们有bst *b(bst *b, int key),它修改了*b(键和子指针)的功能,并分配回它不能.这避免了额外的间接性(这有点难看),但如果您使用分配函数返回值和函数副作用的组合来实现您的目标,则会有点不一致。

我选择了选项二。

调试

假设您将 1 插入 BST。也许您删除了2。你怎么知道这行得通?如果你能看到你的 BST 在做什么,是不是更容易调试?

我建议(尤其是在开始编写基本数据结构时,当像 gdb 这样的复杂调试环境可能 a) 过度杀伤和 b) 信息过载时)您尽早编写代码来打印出数据结构的状态。

另外,如果您正在运行 *nix,valgrind(发音为“Val grinned”)是您最好的朋友。它是一个内存检查器,您可以使用它来确保始终释放您分配的内存(当然,一旦您完成了它),并查找其他内存错误,例如超出范围。学习使用它(它实际上非常简单)。如果你在 Windows 上,有 Purify 虽然我不认为它是免费的......无论如何,我相信更熟悉该环境的人可以推荐一些东西。

编译器警告也是一件很棒的事情。打开它们并将它们视为错误。使用 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 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 中,并将我们的 main 方法放在那里,但为了紧凑,我没有。

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 并读取值对我来说总是有点像黑客(因为在 Newick 中 L->R 和 R->L 之间没有区别......),但我对它情有独钟,也习惯于阅读它,并且发现它在过去调试时很方便。而且你的打印方法无论如何都应该在它的顺序上保持一致,除非你疯了。这还演示了通过将递归工作拆分为单独的方法来包装递归任务,然后从公开可用的方法中调用该方法。后者处理不太适合递归的其他任务(例如,在顶层只打印一次分号和换行符。)

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;
}

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

【讨论】:

  • @IsakarJarak,哇!非常感谢您为此付出时间和精力。我真的很感激你,你太棒了,我会在今天晚些时候有更多时间的时候完成它:)
  • 我觉得自己绝对是个懒鬼。我所做的只是找到他的错误 =P
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-17
  • 2019-06-19
相关资源
最近更新 更多