2

C で Binary Search Tree データ構造を実装しようとしていますが、バグに遭遇しました。理解できない理由でポインタ値が変化します。(奇妙な出力については、投稿の下部を参照してください[削除機能と主な機能は、出力がどこから来るのかを明確にします])以下の私のテスト機能:

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->key の値が 2 の場合に変更する理由がわかりません。処理が完了すると、
root->key は 0 になります。私の問題を指摘したり、正しい方向。必要に応じて、 https://github.com/PuffNotes/C/blob/master/data_structures/binary_tree.cで現在の BST 実装を確認できます。私は最近、スキルを習得するために毎日プログラミングを試み始めました。自分は C の初心者であると考えています (参考までに)。ありがとうございました。

4

2 に答える 2

4

ルートノードポインタを変更していません。これは値によって削除関数に渡されます。これは確かに削除の実行可能なターゲットであるため、別のノードに変更される可能性があるため、アドレスによって渡される必要があります。注:rootどこかで見逃してしまった場合はお詫びしますが、コンパイルで問題が解決するはずです)。

注:このコードが正しいかどうか、または機能するかどうかについて、検証パスを作成しませんでした。しかし、何かがおかしいという本当のヒントは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); 

そして、バランスアルゴリズムを書き始めるときにたくさんのことをするので、アドレスでルートを渡すことに慣れてください。

于 2012-11-02T02:51:34.327 に答える
3

@WhozCraig は、質問の主旨に対して適切な回答を提供してくれましたが、その他の問題についてはもう少しお役に立ちたいと思います。

最初のステップ

OK、最初に、コードに関する非常に重要なことがいくつかあります。

  • 中かっこ。神への愛のために、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 < B1 か所で使用するA <= 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 のノードが整数と (ノードへの) 2 つのポインターを格納することを決定します。それは結構です。ただし、ノードが使用されているかどうかを判断するために、キーをセンチネル値として賢明に使用できないことを意味します。

幸いなことに、その必要はありません。ツリーが空のときにルートとしてノードを使用する代わりに、ノードへのポインターを使用します。ノードがない場合、ポインターはNULLです。これは、remove メソッドで発生した問題に関連しています。

BST は、リンクされたノードで構成されるツリーですよね? またはそれは?各ノードが実際にはサブツリーであるツリーと考えることもできます。そのため、再帰に適しているので、可能な場合は再帰を使用してエレガントに表現してみましょう。

ここで、いくつかのオプションがあります。

  1. 不変の bst を作成できます (すべての変更呼び出しは、古いものを変更せずにツリーの新しい変更されたコピーを返すため、b = bst_insert(b, 10)すべての変更呼び出しは次のようになります)。bst_insert
  2. void bst_insert(bst **b, int key)as と呼ばれるの行に沿ってさらに進むことができますbst_insert(&b, 10)。ここでは、ノードへのポインターへのポインターを渡すことにより、追加レベルの間接化を使用してツリーを変更します。
  3. または、前の 2 つのオプションの間の何かを選択することもできます。ここではbst *b(bst *b, int key)、できるもの*b(キーと子ポインター) を変更し、できないものを代入します。これにより、余分な間接化 (少し醜い) が回避されますが、目的を達成するために関数の戻り値と関数の副作用の割り当てを組み合わせて使用​​している場合、少し一貫性がなくなります。

使用するオプション 2 を選択しました。

デバッグ

1BST にa を挿入するとします。おそらく、を削除し2ます。これが機能したことをどのように知っていますか?BST が何をしているかを確認できれば、デバッグが容易になると思いませんか?

(特に、基本的なデータ構造を書き始めるとき、gdb のような複雑なデバッグ環境が a) 過剰で b) 情報が過負荷になる可能性がある場合)、データ構造の状態を非常に早い段階で出力する方法をコーディングすることをお勧めします。

また、*nix を実行している場合、valgrind("Val grinned" のように発音) はあなたの親友です。これはメモリ チェッカーであり、割り当てたメモリが常に解放されていることを確認したり (もちろん、メモリの使用が終わったら)、範囲外などの他のメモリ エラーを探すために使用できます。使い方を学びましょう (実際にはとても簡単です)。Windows を使用している場合は、Purify がありますが、無料ではないと思います...とにかく、その環境に詳しい人が何かを推奨できると確信しています。

コンパイラの警告も素晴らしいものです。それらをオンにして、エラーとして扱います。gcc を使用するときは、 を使用する傾向があります-W -Wall -ansi -pedantic。関連して-g、GDB が使用する情報を生成する必要があります。

BST の作成

私はあなたのコードを調べて分析するつもりでしたが、同様のスタイルの BST を自分で書くことになり、各部分を説明するコードを調べました。私は2ファイルのアプローチを採用しました。と がbst.cありbst.hます。

bst.h

このビットは、ヘッダーが大規模なシステムに複数回含まれている場合に、誤って同じものを複数回定義または宣言しようとしないようにするため、また、誤って無限のプリプロセッサ ループを引き起こさないようにするためです。循環ヘッダー参照があります。

#ifndef BST_H_
#define BST_H_

これは typedef であり、常に入力する必要がなく、BST を使用しているだけの人から型struct bstnodeの内容を隠すことができます。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、そこにメイン メソッドを配置することもできましたが、コンパクトにするためにそうしませんでした。

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を維持したかったのです。newdelete

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

可能であればノードを見つけます。const変更しないことを示すためにb を作成することもできましたが、その場合は戻り値の型も変更してから、検索したものを変更するためにキャストする必要がありましたが、これは少しいたずらです。

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 形式で BST を出力して値を読み取るのは、私にとっては常にちょっとしたハックのように感じます (Newick では L->R と R->L の区別がないため...)。そのため、私もそれを読むことに慣れており、過去にデバッグに便利であることがわかりました. そして、あなたが頭がおかしくない限り、印刷を行う方法はとにかく順序が一貫している必要があります。これは、再帰的な作業を別のメソッドに分割して再帰的なタスクをラップする方法も示しています。このメソッドは、公開されているメソッドから呼び出されます。後者は、再帰にはあまり適していない他のタスクを処理します (たとえば、セミコロンと改行をトップレベルで 1 回だけ出力するなど)。

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

これは基本的に私のコーヒーブレイクで組み立てられたものであり、厳密にテストされていないことに注意してください. これが役立つことを願っています。

于 2012-11-05T03:40:19.827 に答える