我正在尝试用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的初学者(供参考)。谢谢。
答案 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)
好的,首先,关于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 < B
和A <= B
可能是灾难性的。如果您选择一侧粘贴目标节点(您正在寻找的那个节点)并且始终以相同的方式进行比较,它也有助于提高可读性。
内存分配可能会失败。如果是这样,各种分配方法(malloc
,calloc
等)将返回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是一棵树,由链接节点组成,对吧?或者是吗?您还可以将其视为一个树,其中每个节点实际上都是一个子树。这使得它非常适合递归,所以让我们尽可能优雅地使用递归来表达事物。
现在,我们有几个选择。
b = bst_insert(b, 10)
因为bst_insert
等等都会返回树的新修改后的副本而不会改变旧的。)void bst_insert(bst **b, int key)
中更多地使用,称为bst_insert(&b, 10)
,我们使用额外的间接级别来修改我们的树,方法是将指针传递给指向节点的指针。 bst *b(bst *b, int key)
,它可以修改*b
(键和子指针)的内容,然后分配什么它不能。这避免了额外的间接(这有点难看),但如果您使用分配函数返回值和函数副作用的组合来实现您的目标,则有点不一致。我选择了第二选项。
假设您在BST中插入1
。也许您删除2
。你怎么知道这有效?如果你能看到你的BST正在做什么,那么调试会不会更容易?
我建议(特别是在开始编写基本数据结构时,当像gdb这样的复杂调试环境可能是一种过度杀伤和b)信息过载时,你会编写很早就打印出数据结构状态的方法。
此外,如果您正在运行* nix,valgrind
(发音为&#34; Val grinned&#34;)是您最好的朋友。它是一个内存检查器,您可以使用它来确保您始终释放您分配的内存(当然,当您完成它时),并查找其他内存错误,例如超出范围。学会使用它(它实际上非常简单)。如果您使用的是Windows,那么虽然我不认为它是免费的,但我确实认为这个环境可以推荐一些东西。<\ n
/ p>
编译器警告也很棒。打开它们就像错误一样对待它们。使用gcc时,我倾向于使用-W -Wall -ansi -pedantic
。在相关的说明中,有-g
生成GDB要使用的信息。
我打算翻阅你的代码并进行剖析,但最后我自己编写了一个类似风格的BST,然后我的代码解释了每个部分。我采用双文件方法。我们有bst.c
和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_ */
#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;
}
请记住,这基本上已经在我的咖啡休息时间组装,并且没有经过严格的测试。我希望这会有所帮助。