一般讲数据结构和算法的书都是先讲链表的,还好这次看的书的作者没有从链表开始,之前学数据结构就是学了链表,后面的树就没看下去了:)这次终于开始接触树这种数据结构。二叉搜索树是最简单的树结构,学完后感觉并没有比链表难理解多少。
本节主要记录下对二叉搜索树的创建、插入、删除、查找、遍历、前驱后继等操作的理解。具体的二叉搜索树定义和那些操作的定义看原书就行了,作者讲的很清楚。原书的电子版在此系列的第一篇博客有给出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编译测试通过:)