[Elementary-Algorithms/4]二叉搜索树

一般讲数据结构和算法的书都是先讲链表的,还好这次看的书的作者没有从链表开始,之前学数据结构就是学了链表,后面的树就没看下去了:)这次终于开始接触树这种数据结构。二叉搜索树是最简单的树结构,学完后感觉并没有比链表难理解多少。
本节主要记录下对二叉搜索树的创建、插入、删除、查找、遍历、前驱后继等操作的理解。具体的二叉搜索树定义和那些操作的定义看原书就行了,作者讲的很清楚。原书的电子版在此系列的第一篇博客有给出Github地址。
内容主要是代码,然后对代码进行一些必要的解释。代码是自己理解算法后或一时没理解算法,然后作者有给出示例代码,把示例看懂后写的,用的C语言,也可用其它语言根据算法实现。


二叉搜索树头文件:
头文件尽量简单清晰,让人一看就知道是干什么的,以及如何使用。

/*
 * File: wtlBSTree.h
 * Author: WangTaL
 * Copyright: 5/20/2018
*/
#ifndef _WTLBSTREE_H_
#define _WTLBSTREE_H_

// 使用的时候并不需要清楚树节点的结构,所以无需将节点的声明放头文件,加上下面这句就够了
// 需说明下:树作为通用容器,是可以存放任何类型的数据,为了简化下,本节点只存放整数类型
typedef struct _wtlNODE wtlNODE;
// 二叉搜索树的结构
typedef struct _wtlBSTREE {
    // 包含整棵树的根节点
    wtlNODE* root;
    // 树的高度(作者并没有说明如何计算树的高度,想了下也挺麻烦的,只是占个位,不会用到)
    int height;
} wtlBSTREE;

/* 供外部使用的函数 */
// Public functions
// 创建一颗空树
wtlBSTREE* wtlBSTree_Create(void);
// 销毁一颗树,注意是二级指针,一级指针无法改变指针内容的,销毁后要将 tree = NULL;
void wtlBSTree_Destroy(wtlBSTREE** tree);
// 向树中插入一个节点,节点中存放value。所有会改变树结构的都传入二级指针
void wtlBSTree_InsertValue(wtlBSTREE** tree, unsigned long value);
// 从树中移除存放value的那个节点
wtlBSTREE* wtlBSTree_Remove(wtlBSTREE** tree, unsigned long value);
// 整棵树的前序遍历。因为树存放的是整数,这里只是将其打印出来,如果需要对节点进行其它操作,需给函数传入一个操作节点的函数指针参数
void wtlBSTree_PreOrder(wtlBSTREE* tree);
// 整棵树的中序遍历
void wtlBSTree_InOrder(wtlBSTREE* tree);
// 整棵树的后续遍历
void wtlBSTree_PostOrder(wtlBSTREE* tree);
// 查找树中存放value的节点并返回。因为这里实现的树存的是整数,查找这个value节点似乎并没什么意义。。。
wtlNODE* wtlBSTree_LookUp(wtlBSTREE* tree, unsigned long value);
// 查找树中存放最小数据的节点。所有不会改变树结构的操作,传一级指针就行了
wtlNODE* wtlBSTree_Min(wtlBSTREE* tree);
// 查找树中存放最大数据的节点
wtlNODE* wtlBSTree_Max(wtlBSTREE* tree);
// 查找存放value节点的前驱,并将前驱节点返回
wtlNODE* wtlBSTree_Precursor(wtlBSTREE* tree, unsigned long value);
// 查找存放value节点的后继,并将后继节点返回
wtlNODE* wtlBSTree_Successor(wtlBSTREE* tree, unsigned long value);

/* 供内部使用的函数,就是上面的外部函数会调用到,使用者一般不会用到 */
// Private functions
// 创建一个存放value的节点
wtlNODE* wtlNode_Create(unsigned long value);
// 销毁一个节点,二级指针,会改变一级指针内容
void wtlNode_Destroy(wtlNODE** node);

#endif /* _WTLBSTREE_H_  */

二叉搜索树的实现文件:
代码水平当然是用自己至今所学的知识写出来的,一些函数的算法来自原文,其它就是自己想的了,难免会有未发现的错误。

/*
 * File: wtlBSTree.c
 * Author: WangTaL
 * Copyright: 5/20/2018
*/
#include <stdio.h>
#include <malloc.h>
#include <assert.h>
#include "wtlBSTree.h"

// 树节点结构
struct _wtlNODE {
    // 指向父节点的指针
    struct _wtlNODE* parent;
    // 指向左子树的指针
    struct _wtlNODE* left;
    // 指向右子树的指针
    struct _wtlNODE* right;
    // 节点存放整形数据
    unsigned long value;
};

// 创建一颗空树,没什么好解释的
wtlBSTREE* wtlBSTree_Create(void){
    wtlBSTREE* tree = malloc(sizeof(*tree));
    assert(tree != NULL);
    tree->root = NULL;
    tree->height = 0;

    return tree;
}

// 销毁一棵树
void wtlBSTree_Destroy(wtlBSTREE** tree){
    // 参数不合理的判断
    if (NULL == tree || NULL == *tree) {
        return;
    }

    // C语言其实是可以在函数内定义函数的,下面定义了一个释放节点内存的函数
    // 该函数是递归的,虽然递归效率较低,但递归实现起来简单
    // 因为销毁整棵树的函数并不需要递归,所以将需要递归部分定义为另一个函数
    // 具体步骤(自己想的,可能有更好的算法):
    // 1.如果一个节点的左右子树都为空,释放该节点
    // 2.释放左子树节点
    // 3.释放右子树节点
    // 可以看出,下面的代码几乎和算法描述的一模一样,这就是递归的魅力
    void wtlBSTree_FreeNodes(wtlNODE** root){
        while (*root) {
            if ((NULL == (*root)->left) && (NULL == (*root)->right)) {
                wtlNode_Destroy(root);
                return;
            }

            if ((*root)->left) {
                wtlBSTree_FreeNodes(&((*root)->left));
            }
            if ((*root)->right) {
                wtlBSTree_FreeNodes(&((*root)->right));
            }
        }
    }

    // 调用上面定义的函数,将树的所有节点都释放
    wtlBSTree_FreeNodes(&((*tree)->root));

    // 再释放树这个结构
    free(*tree);
    // 置为NULL,避免销毁树后再操作数据,这样可能会出错的
    *tree = NULL;
}

// 插入操作
void wtlBSTree_InsertValue(wtlBSTREE** tree, unsigned long value){
    // 同样地,将递归部分定义为另一个函数,算法来自原文
    // 具体步骤:
    // 1.如果根节点为空,将数据插入根节点
    // 2.如果数据大于根节点,将数据插入右子树
    // 3.如果数据小于根节点,将数据插入左子树
    // 我们要改变root、node的一级指针内容,所以传二级指针
    void wtlBSTree_Insert(wtlNODE** root, wtlNODE** node){
        // 如果根节点为空
        if (NULL == *root) {
            // 直接将要插入的节点赋值给根节点
            *root = *node;
            // 并返回(这是递归的结束条件)
            return;
        } else {
            // 这步是确定插入节点的父节点,算法里没这个描述
            // 具体情况,在纸上亲自按步骤插入几个节点就清楚了
            // 它是一层一层来确定父节点的,当确定了插入节点的位置,父节点也就确定了
            (*node)->parent = *root;
        }
        // 处理要插入数据在树中重复的情况
        if ((*root)->value == (*node)->value) {
            // 这里是直接跳过了
            return;
        }

        // 下面的if else就和算法描述的一样了
        if ((*root)->value > (*node)->value) {
            wtlBSTree_Insert(&((*root)->left), node);
        } else {
            wtlBSTree_Insert(&((*root)->right), node);
        }
    }

    // 注意这里是root指针的地址,而不能wtlNODE* root = (*tree)->root;再&root;
    // 如果root为NULL,&NULL在这里是不合理的
    wtlNODE** root = &((*tree)->root);
    // 创建一个节点,存放value
    wtlNODE* node = wtlNode_Create(value);
    // 调用上面那个递归函数,将节点插入到对应位置
    wtlBSTree_Insert(root, &node);
}

// 前序遍历整棵树
void wtlBSTree_PreOrder(wtlBSTREE* tree){
    if (NULL == tree) {
        return;
    }

    // 其实遍历的整个过程都是递归的,但是为了统一操作函数的参数,就这样了
    // 算法来自原文,具体步骤:
    // 1.访问根节点
    // 2.访问左节点
    // 3.访问右节点
    void wtlBSTree_PreOrderRaw(wtlNODE* root){
        // 递归的退出条件,当遍历的节点为空,结束整个函数
        if (NULL == root) {
            return;
        }

        // 访问操作,在这里其实就是将节点的数据打印出来
        printf("%ld ", root->value);
        // 递归遍历左子树(打印左节点数据)
        wtlBSTree_PreOrderRaw(root->left);
        // 递归遍历右子树(打印右节点数据)
        wtlBSTree_PreOrderRaw(root->right);
    }

    // 调用上面的递归函数,就可以得到前序遍历结果了
    wtlBSTree_PreOrderRaw(tree->root);
}

// 中序遍历整棵树
void wtlBSTree_InOrder(wtlBSTREE* tree){
    if (NULL == tree) {
        return;
    }

    // 算法来自原文,步骤和前序遍历差不多,只不过:
    // 1.先访问左节点
    // 2.再访问跟节点
    // 3.最后访问右节点
    void wtlBSTree_InOrderRaw(wtlNODE* root){
        if (NULL == root) {
            return;
        }

        wtlBSTree_InOrderRaw(root->left);
        printf("%ld ", root->value);
        wtlBSTree_InOrderRaw(root->right);
    }

    wtlBSTree_InOrderRaw(tree->root);
}

// 后序遍历整棵树
void wtlBSTree_PostOrder(wtlBSTREE* tree){
    if (NULL == tree) {
        return;
    }

    // 算法来自原文:
    // 1.先访问左节点
    // 2.再访问右节点
    // 3.最后访问根节点
    void wtlBSTree_PostOrderRaw(wtlNODE* root){
        if (NULL == root) {
            return;
        }

        wtlBSTree_PostOrderRaw(root->left);
        wtlBSTree_PostOrderRaw(root->right);
        printf("%ld ", root->value);
    }

    wtlBSTree_PostOrderRaw(tree->root);
}

// 查找操作
wtlNODE* wtlBSTree_LookUp(wtlBSTREE* tree, unsigned long value){
    if (NULL == tree) {
        return NULL;
    }

    // 同样,为了统一接口,另定义一个递归函数
    // 算法来自原文:
    // 1.如果要查找的值不存在,放回空
    // 2.如果查找的值等于该节点,返回该节点
    // 3.如果要查找的值大于该节点,继续在右子树中查找
    // 4.如果要查找的值小于该节点,继续在左子树中查找
    wtlNODE* wtlBSTree_LookUpRaw(wtlNODE* root, unsigned long value){
        if (NULL == root) {
            return NULL;
        }
        if (value == root->value) {
            return root;
        }

        if (value > root->value) {
            wtlBSTree_LookUpRaw(root->right, value);
        } else {
            wtlBSTree_LookUpRaw(root->left, value);
        }
    }

    // 调用上面的函数,放回查找到的节点
    return wtlBSTree_LookUpRaw(tree->root, value);
}

// 查找树中最小数据的节点。因为其它地方需要用到,所以不能定义在函数中
// 算法来自原文,就是递归地找到最左边那个节点
wtlNODE* wtlBSTree_MinRaw(wtlNODE* root){
    // 根节点为空,当然返回空了
    if (NULL == root) {
        return NULL;
    }
    // 根节点不为空,递归地在左子树中查找
    if (root->left) {
        return wtlBSTree_MinRaw(root->left);
    }
    // 当递归到最后一个左节点时,该节点的左节点就为空了,就不会再递归了
    // 此时返回最后那个节点就是最小节点了
    return root;
}
// 统一参数的接口,直接调用上面那个函数
wtlNODE* wtlBSTree_Min(wtlBSTREE* tree){
    if (NULL == tree) {
        return NULL;
    }

    return wtlBSTree_MinRaw(tree->root);
}

// 找最大节点,就是最右边那个,步骤和找最小节点相反,不细讲
wtlNODE* wtlBSTree_MaxRaw(wtlNODE* root){
    if (NULL == root) {
        return NULL;
    }
    if (root->right) {
        return wtlBSTree_MaxRaw(root->right);
    }
    return root;
}
wtlNODE* wtlBSTree_Max(wtlBSTREE* tree){
    if (NULL == tree) {
        return NULL;
    }

    return wtlBSTree_MaxRaw(tree->root);
}

// 查找value所在节点的前驱节点。算法来自原文,步骤在代码中解释
wtlNODE* wtlBSTree_Precursor(wtlBSTREE* tree, unsigned long value){
    assert(tree != NULL);

    // 查找value所在节点
    wtlNODE* current = wtlBSTree_LookUp(tree, value);

    assert(current != NULL);

    // 如果该节点的左子树不为空
    if (current->left) {
        // 则左子树中的最大节点就是前驱节点
        return wtlBSTree_MaxRaw(current->left);
    } else { // 否则就根据父节点指针向上查找
        wtlNODE* parent = current->parent;
        // 下面的循环处理了两种情况,如果current节点为右节点,则它的父节点就是前驱节点
        // 如果current为左节点,则它的父节点的父节点就是前驱节点
        while (parent && parent->right != current) {
            current = parent;
            parent = parent->parent;
        }

        return parent;
    }
}

// 查找value所在节点的后继节点。算法来自原文,步骤在代码中解释
wtlNODE* wtlBSTree_Successor(wtlBSTREE* tree, unsigned long value){
    assert(tree != NULL);

    // 查找value所在节点
    wtlNODE* current = wtlBSTree_LookUp(tree, value);

    assert(current != NULL);

    // 如果该节点的右子树不为空
    if (current->right) {
        // 则右子树中的最大节点就是后继节点
        return wtlBSTree_MinRaw(current->right);
    } else { // 否则就根据父节点指针向上查找
        wtlNODE* parent = current->parent;
        // 下面的循环处理了两种情况,如果current节点为左节点,则它的父节点就是后继节点
        // 如果current为右节点,则它的父节点的父节点就是后继节点
        while (parent && parent->left != current) {
            current = parent;
            parent = parent->parent;
        }

        return parent;
    }
}

// 从树中移除value所在的那个节点
wtlBSTREE* wtlBSTree_Remove(wtlBSTREE** tree, unsigned long value){
    assert(*tree != NULL);

    wtlBSTREE* ptree = *tree;
    // 先找到value所在的节点
    wtlNODE* node = wtlBSTree_LookUp(ptree, value);

    if (NULL == node) {
        return ptree;
    }

    // 获取该节点的父节点和子节点
    wtlNODE* parent = node->parent;
    wtlNODE* left = node->left;
    wtlNODE* right = node->right;
    // 如果该节点的子节点都为空,直接移除
    if (NULL == left && NULL == right) {
        // 下面的if else是为了将parent的对应子节点置为NULL
        if (parent->left == node) {
            parent->left = NULL;
        } else {
            parent->right = NULL;
        }
        wtlNode_Destroy(&node);
        return ptree;
    }

    // node的左子节点不为空,右子节点为空的情况
    if (left != NULL && NULL == right) {
        // 如果node为左节点
        if (parent->left == node) {
            // 将parent的左子树指向node的左子树就行了
            parent->left = left;
            // 设置node的左子树的父节点
            left->parent = parent;
        } else { // node为右节点
            parent->right = left;
            left->parent = parent;
        }
        wtlNode_Destroy(&node);
        return ptree;
    }

    // node的右子节点不为空,左子节点为空的情况
    if (right != NULL && NULL == left) {
        // node为左节点
        if (parent->left == node) {
            // 将parent的左子树指向node的右节点
            parent->left = right;
            right->parent = parent;
        } else { // node为右节点
            // 将parent的右子树指向node的右节点
            parent->right = right;
            right->parent = parent;
        }
        wtlNode_Destroy(&node);
        return ptree;
    }

    // node的左右节点都不为空
    if (left && right) {
        // 找到node的右子树中的最小节点,用来替换移除的node节点
        wtlNODE* right_min = wtlBSTree_MinRaw(right);
        // 替换其实就是将数据替换。。。
        node->value = right_min->value;

        // 实际上要移除的是node右子树中那个最小节点,释放它后将其赋值为NULL就行了
        wtlNODE* min_parent = right_min->parent;
        if (min_parent->left == right_min) {
            min_parent->left = NULL;
        } else {
            min_parent->right = NULL;
        }
        wtlNode_Destroy(&right_min);
        return ptree;
    }
}

// 内部函数,没什么好讲的了
// Private functions
wtlNODE* wtlNode_Create(unsigned long value){
    wtlNODE* node = malloc(sizeof(*node));

    assert(node != NULL);

    node->parent = NULL;
    node->left = NULL;
    node->right = NULL;
    node->value = value;

    return node;
}

void wtlNode_Destroy(wtlNODE** node){
    if (NULL == *node) {
        return;
    }

    free(*node);
    *node = NULL;
}

以上就是我看了作者对二叉搜索树的介绍后实现的代码了,每个人的理解不一样,可能最后的实现也不一样。所以最好还是看看原文的介绍。


再提供下测试代码吧:

// test.c
#include "wtlBSTree.h"

int main(int argc, char* argv[])
{
    unsigned long arr[] = {4, 3, 1, 2, 8, 7, 16, 10, 9, 14};

    wtlBSTREE* tree = wtlBSTree_Create();

    for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
        wtlBSTree_InsertValue(&tree, arr[i]);
    }

    printf("\nRemove key 4\n");
    tree = wtlBSTree_Remove(&tree, 4);
    printf("Remove key 16\n");
    tree = wtlBSTree_Remove(&tree, 16);
    printf("Remove key 1\n");
    tree = wtlBSTree_Remove(&tree, 1);
    printf("Remove key 14\n\n");
    tree = wtlBSTree_Remove(&tree, 14);


    wtlNODE* min = wtlBSTree_Min(tree);
    if (min) printf("min:%ld\n", min->value);
    wtlNODE* max = wtlBSTree_Max(tree);
    if (max) printf("max:%ld\n", max->value);

    wtlNODE* precursor = wtlBSTree_Precursor(tree, 14);
    if (precursor) printf("\nprecursor of 14:%ld\n", precursor->value);
    wtlNODE* successor = wtlBSTree_Successor(tree, 14);
    if (successor) printf("successor of 14:%ld\n\n", successor->value);

    precursor = wtlBSTree_Precursor(tree, 8);
    if (precursor) printf("\nprecursor of 8:%ld\n", precursor->value);
    successor = wtlBSTree_Successor(tree, 8);
    if (successor) printf("successor of 8:%ld\n\n", successor->value);

    precursor = wtlBSTree_Precursor(tree, 3);
    if (precursor) printf("\nprecursor of 3:%ld\n", precursor->value);
    successor = wtlBSTree_Successor(tree, 3);
    if (successor) printf("successor of 3:%ld\n\n", successor->value);

    printf("pre-order:");
    wtlBSTree_PreOrder(tree);
    printf("\n");
    printf("in-order:");
    wtlBSTree_InOrder(tree);
    printf("\n");
    printf("post-order:");
    wtlBSTree_PostOrder(tree);
    printf("\n");

    wtlBSTree_Destroy(&tree);

    return 0;
}

测试代码没什么好说的。在Ubuntu下GCC7编译测试通过:)

猜你喜欢

转载自blog.csdn.net/alonwol/article/details/80382636
今日推荐