算法导论——二叉搜索树

二叉搜索树是以一个二叉树来组织的,树的每个节点都是由键值value,父节点parent,左子节点left_child,右子节点right_child组成的,而且二叉搜索树的节点间满足以下的关系:假设x是二叉搜索树中的一个节点,如果y是x左子树中的一个节点,那么y的value是比x的value小;如果y是x右子树中的一个节点,那么y的value不比x的value小。

二叉树支持的操作有:遍历、查找、插入、删除。下面是C++的实现:

首先是关于Node的定义:每个Node涉及到它本身的value,还有父节点、左子节点、右子节点的指针。然后是其构造函数为了适应后面的使用,使用了默认参数的,但是要求给出value值。然后为了使得二叉搜索树能够方便地使用Node,将BSTree定义为Node的友元类。

class Node
{
private:
    int value;
    Node* parent;
    Node* left_child;
    Node* right_child;

public:
    Node(int t_value, Node *t_parent = NULL, Node *t_left_child = NULL, Node *t_right_child = NULL):
        value(t_value), parent(t_parent), left_child(t_left_child), right_child(t_right_child){}
    friend class BSTree;
    ~Node(){}

};

然后是关于二叉树的定义BStree:其主要成员就是根节点root,然后是关于操作的前序遍历preorder_walk、中序遍历inorder_walk、后序遍历postorder_walk、最大值maximum、最小值minimum、搜索search_tree、插入insert_tree、删除delete_tree操作。其他的私有的成员函数都是辅助函数。

class BSTree
{
public:
    BSTree():root(NULL){}
    virtual ~BSTree();

    void preorder_walk();
    void inorder_walk();
    void postorder_walk();
    Node* search_tree(int t_value)const;
    Node* minimum()const;
    Node* maximum()const;
    void insert_tree(int t_value);
    void delete_tree(int t_value);

private:
    Node *root;

    void preorder_node_walk(Node* node);
    void inorder_node_walk(Node* node);
    void postorder_node_walk(Node* node);
    Node* search_node(Node *node, int t_value)const;
    Node* minimum_node(Node *node)const;
    Node* maximum_node(Node *node)const;
    void insert_node(Node* node, int t_value);
    Node* seccessor(Node* node);
    void transplant(Node *ori_node, Node *new_node);
};

前序遍历:先打印出根节点,然后再递归调用左子树,和右子树,直到子节点为空。

void BSTree::preorder_node_walk(Node *node){
    if(node == NULL)
        return;
    std::cout << node->value << " ";
    preorder_node_walk(node->left_child);
    preorder_node_walk(node->right_child);
}

void BSTree::preorder_walk(){
    std::cout << "[ ";
    preorder_node_walk(this->root);
    std::cout << "]" << std::endl;
}

中序遍历:先递归调用左子树,再打印根节点,然后递归调用右子树,直到子节点为空。这个打印出来的结果起始是从小到大排好序的。

void BSTree::inorder_node_walk(Node *node){
    if(node == NULL)
        return;
    inorder_node_walk(node->left_child);
    std::cout << node->value << " ";
    inorder_node_walk(node->right_child);
}

void BSTree::inorder_walk(){
    std::cout << "[ ";
    inorder_node_walk(this->root);
    std::cout << "]" << std::endl;
}

后序遍历:先递归调用左子树和右子树,然后打印根节点,知道子节点为空。

void BSTree::postorder_node_walk(Node *node){
    if(node == NULL)
        return;
    postorder_node_walk(node->left_child);
    std::cout << node->value << " ";
    postorder_node_walk(node->right_child);
}

void BSTree::postorder_walk(){
    std::cout << "[ ";
    postorder_node_walk(this->root);
    std::cout << "]" << std::endl;
}

搜索:从根节点出发,根据二叉树的性质,左子树是比当前节点的值小的,右子树是比当前节点大的。所以,与当前节点进行对比,如果相等则找到了。如果比当前节点的值小,那么当前节点变成左子节点,继续递归。如果比当前节点的值大,那么当前节点变成右子节点,继续递归。知道找到,否则返回的是NULL。

Node *BSTree::search_node(Node *node, int t_value)const{
    if(node == NULL)
        return NULL;

    if(t_value == node->value)
        return node;
    else if(t_value < node->value)
        return search_node(node->left_child, t_value);
    else
        return search_node(node->right_child, t_value);
}

Node* BSTree::search_tree(int t_value)const{
    return search_node(root, t_value);
}

最大值:根据二叉树的性质,二叉树的最大值是二叉树最右叶节点。

Node* BSTree::maximum_node(Node *node)const{
    Node *cur = node;

    if(cur == NULL)
        return NULL;

    while(cur->right_child != NULL)
        cur = cur->right_child;
    return cur;
}

int BSTree::maximum()const{
    return maximum_node(root).value;
}

最小值:根据二叉树的性质,二叉树的最小值是二叉树最左叶节点。

Node* BSTree::minimum_node(Node *node)const{
    Node *cur = node;

    if(cur == NULL)
        return NULL;

    while(cur->left_child != NULL)
        cur = cur->left_child;
    return cur;
}

int BSTree::minimum()const{
    return minimum_node(root).value;
}

插入:如果树是空的,那么插入的节点就是根节点,然后返回。如果树不是空的,那么根据二叉树的性质,找到插入的值应该放置的位置,然后将其加入到树中来。

void BSTree::insert_node(Node* node, int t_value){

    if(t_value < node->value){
        if(node->left_child == NULL){
            node->left_child = new Node(t_value, node);
        } else{
            insert_node(node->left_child, t_value);
        }
    } else{
        if(node->right_child == NULL){
            node->right_child = new Node(t_value, node);
        } else{
            insert_node(node->right_child, t_value);
        }
    }
}

void BSTree::insert_tree(int t_value){
    if(root == NULL){
        root = new Node(t_value);
        return;
    }

    insert_node(root, t_value);
}

删除:删除的过程就比较复杂,为了保持二叉树的性质,其变化可能比较大。为了辅助删除函数,首先实现了一个在二叉树中的一个替换函数,就是将new_node取代ori_node。注意这不是节点的替换,这是以new_node为根节点的子树替换以ori_node为根节点的子树。所以只用修改节点与父节点之间的联系就可以了。还有一个就是寻找节点后驱的函数,首先检查当前节点的右子树是否为空,不为空的话,那么这个右子树的最大值就是当前节点的后驱节点。如果右子树为空的话,那么找到第一个包含当前节点的子树且该子树是某个节点的左子节点,那么这个节点就是当前节点的后驱。我们再来分析删除的过程:为了保持二叉树的性质,我们分下面几种情况进行讨论:

1..左子树为空:这种情况直接将其右子树替代掉删除的节点就行了。

2.左子树不为空,但是右子树为空:这种情况直接将其左子树替代掉删除的节点就行了。

3.左子树和右子树都不为空:这种情况一般是确定其后驱节点,根据上面的后驱的查找办法,而且这里的右子树不为空,所以其后驱肯定是在其右子树中的的最左叶节点,也就是它是没有左子节点的。那么我们就将后驱节点的右子节点替代后驱节点,然后将删除的节点换成后驱节点就行了。在这里按照算法导论这本书里面在这里分成了两种情况:后驱节点是不是当前节点的右子节点。其实我这里分析是不用区分的,因为这里代码在后驱节点不是当前节点的子节点的情况下,先是后驱节点的右子节点的一个替换,然后是后面后驱节点对当前节点的一个替换。后驱节点是当前节点的子节点的情况下,这两部合起来一个替换就完成了。

Node* BSTree::seccessor(Node* node){
    if(node->right_child != NULL)
        return minimum_node(node->right_child);

    Node *snode = node->parent;
    Node *cnode = node;

    while(snode != NULL && snode->right_child == cnode){
        cnode = snode;
        snode = snode->parent;
    }

    return snode;
}

void BSTree::transplant(Node *ori_node, Node *new_node){
    if(ori_node->parent == NULL)
        root = new_node;
    else if(ori_node->parent->left_child == ori_node)
        ori_node->parent->left_child = new_node;
    else
        ori_node->parent->right_child = new_node;

    if(new_node != NULL)
        new_node->parent = ori_node->parent;
}

void BSTree::delete_tree(int t_value){
    Node *dnode = search_tree(t_value);

    if(dnode == NULL)
        return;

    if(dnode->left_child == NULL)
        transplant(dnode, dnode->right_child);
    else if(dnode->right_child == NULL)
        transplant(dnode, dnode->left_child);
    else{
        Node *snode = seccessor(dnode);
        std::cout <<snode->value << std::endl;
        if(snode->parent != dnode){
            transplant(snode, snode->right_child);
            snode->right_child = dnode->right_child;
            dnode->right_child->parent = snode;
        }

        transplant(dnode, snode);
        snode->left_child = dnode->left_child;
        dnode->left_child->parent = snode;
    }

    dnode->parent = NULL;
    dnode->left_child = NULL;
    dnode->right_child = NULL;

    delete dnode;
}

至此,完成了二叉树的一些基本操作。这里总结一下:因为这里主要注重代码的实现,有些细节的东西并没有讲清楚,例如什么后驱节点的查找为什么结果是那样的,删除的时候为什么这样做,这样做到底对不对啊?起始这里有一个书上没有提到的,但是我们应该能够分析到的结论:对于二叉树中的每个子树,如果我们将二叉树按照中序(实际从小到大排列)展开,那么任何一个子树的元素在这个展开的序列中都是连续的一个片段。那么我们根据这个就可以知道后驱应该怎么去找。然后还有一个怎么确认你的操作能够保持二叉树的性质不变:对你改变的地方进行局部分析,如果与父节点,子节点之间的关系能够保持,俺么你的变化基本是正确的(这个是在某些情况下才成立,至少在这次的实现当中是成立的,因为这个过程需要保持一颗子树是谁的子节点,那么变化后仍然是它的左子节点或者右子节点)。

猜你喜欢

转载自blog.csdn.net/WaterWin/article/details/84025941