Binary Search Tree 作为Chapter17章中的一个重难点,作者Stephen Prata 花了很大篇幅来讲述二叉查找树的具体原理以及执行,英文原版本身就增加一定的理解难度,读到这里想能够完全理解更是难上加难,总结一些阅读过程中作者提到的一些重难点,防止自己忘记的基础上,希望也能够帮到正在阅读英文原版的同道中人。
Binary Search Tree
作者在为你说明二叉查找树这一名词的具体概念的时候,已经提到了不少要点:
1.二叉查找树是一种链接结构体,它集成了链接结构体和二分查找两者的优点于一身,具体内容可以另外详细说明.
2.二叉查找树之所以有此名,是因为它时有着二分查找的策略在里面的,这个思想就是每个节点都有着两个子节点——一个左节点一个右节点。
3.这些节点的排列顺序是左节点是要先于(数值上小于)父节点的,右节点是要后于(数值上大于)父节点的,这个关系对树中的每一个节点都有效。
4.树的顶部被叫做根节点,树本身是一种等级制组织,这就意味着树种的数据被分成不同的等级,有在它上面的等级,也有在它下面的等级。
5.如果一个树是满的(满二叉树的定义各有不同,这里不做详细解释),那么每一层的的节点数是它上一层节点数的两倍。
6.每一个节点都可以看作属于它下面沿溯节点组成的树的根节点,这种树被称作子树。
A Binary Tree ADT
这里作者运用了和前面ADT类型一样的声明格式:
类型名称 : 二叉查找树
类型属性 :
一个要么是一个空树,要么由一个根节点领导的树;
每一个节点自上而下实际上都有两个树:左子树和右子树;
每一个子树本身也是个二叉树,包括成为一个空树的可能性;
一个二叉查找树是一个排好序的二叉树,在他其中的每一个节
点都包含一个项目,所有左子树节点中的项目都在根节点项目之前,所有右子
树节点中的项目都在根节点项目之后;
类型操作 :
初始化树为空
确定树是否为空
确定树是否是满的
确定树中的项目的个数
加一个项目到树中
拿走一个项目从树中
在树中寻找一个项目
遍历树中的每一个项目
清空树中的内容
The Binary Search Tree Interface
在这里作者定义了三个ADT类型的变量:
typedef SOMETHING Item /*第一个ADT,其中Item可以是某种类型*/
typedef struct trnode /*第二个ADT,可以用Trn表示trnode类型的结构体*/
{
Item item;
struct trnode * left;
struct trnode * right;
}Trn;
typedef struct tree /*第三个ADT,可以用Tree表示tree类型的结构体*/
{
Trnode * root;
int size;
}Tree;
这里需要注意的是:Tree是一个包含有指向Trnode类型的指针的结构体,意味着Tree根据指针指向的地址可以调用它的成员Trnode;Trnode又是一个包含有Item类型的变量。
The Binary Tree Implementation
Adding an Item
要增加一个项目到节点中,首先你需要确保的是两点:
1.有足够的空间为一个节点;
2.树中没有和你要加入的节点内容相同的节点
满足以上两个条件,这个节点可以被加在树上,你创造一个新的树,并把他的左右指针初始化为空。这里设置这样一个函数AddItem()(函数中的SeekItem(),MakeNode(),AddNode()在之后会有定义)
bool AddItem(const Item *pi,Tree * ptree)
{
Trnode * new_node;
if(TreeIsFull(ptree))
{
fprintf(stderr,"Tree is full\n");
return false;
} /*判断树是否满了*/
if(SeekItem(pi,ptree).child !=NULL)
{
fprintf(stderr,"Attempted to add dumplicate item\n");
return false;
}/*发现树中有将要添加的项目,无法添加*/
new_node=MakeNode(pi);/*point to new node*/
if(new_node == NULL)
{
fprintf(stderr,"Couldn't creat node");
return false;/*新节点为空无法添加*/
}
ptree->size++;
if(ptree->root==NULL)/*case1: tree is empty*/
ptree->root = new_node;/*new node is tree root*/
else /*case2: not empty*/
AddNode(new_node,ptree->root);/*add node to tree*/
return true; /*successful return*/
}
需要注意的是:这里面的SeekItem(),MakeNode()和AddNode()函数并不属于公共交互界面的一部分。相反的他们是被隐藏在tree.c文件中的静态函数,他们解决执行操作的各种细节,比如节点,指针,结构体,这些并不属于公共交互界面。
之后是关于MakeNode()函数的定义:
static Trnode * MakeNode(const iten=m *pi)
{
Trnode * new_node;
new_node = (Trnode*)malloc(sizeof(Trnode));
if(new_node !=NULL)
{
new_node->item=*pi;
new_node->left=NULL;
new_node->right=NULL;
}
return new_node;
}
接着是AddNode()函数的定义:
static void AddNode(Trnode * new_node,Trnode * root)
{
if(ToLeft(&new_node->item,&root->item))
{
if(root->left ==NULL)/*empty subtree*/
root->left = new_node;/*so add node here*/
else
AddNode(new_node,root->left);/*else process subtree*/
}
else if(ToRight(&new_node->item,&root->item))
{
if(root->right ==NULL)
root->right = new_node;
else
AddNode(new_node,root->right);
}
else /*should be no duplicates*/
{
fprintf(stderr,"locantion error in AddNode()\n");
exict(1);
}
}
接着作者在定义Toleft()和ToRight()函数之前对函数strcmp()做了一些说明:strcmp()返回一个负值如果第一个字符串先于第二个字符串,如果两者相等返回0,如果第二个字符串先于第一个返回正值。
接着作者定义了ToLeft()和ToRight()函数:
static bool ToLeft(const Item * i1,const Item * i2)
{
int comp1;
if((comp1 = strcmp(i1->petname,i2->petname)) < 0)
return true;
else if(comp1==0 &&
strcmp(i1->kind,i2->kind) < 0)
return true;
else
return false;
}
Finding an Item
这里对SeekItem()函数做了一些说明,该函数的返回值是一个结构体,这个结构体包括该节点所包含的项目,以及指向该节点的父节点,我们将返回的结构体定义成pair类型:
typedef struct pair
{
Trnode * parent;
Trnode * child;
}Pair;
有了这些函数,我们就可以在函数SeekItem()中对ToLeft(),ToRight()实现调用:
static Pair SeekItem(const Item * pi,const Tree * ptree)
{
Pair look;
look.parent = NULL;
look.child = ptree->root;
if(look.child == NULL)
return look; /*没有子树,空树*/
while(look.child != NULL)
{
if(ToLeft(pi,&(look.child->item)))
{
look.parent = look.child;
look.child = look.child->left;
}
else if (ToRight(pi, &(look.child->item)))
{
look.parent = look.child;
look.child = look.child->right;
}
else /*must be same if not to left or right*/
break;/*look.child is address of node with item*/
}
return look;/*successful return*/
}
这里作者提醒因为该函数返回值是一个结构体类型,所以当其他函数调用时需要考虑到结构体操作,比如在AddItem()函数中调用时有段代码:
if(SeekItem(pi,ptree).child !=NUll)
使用完SeekItem后,InTree()函数就变得简单了:
bool InTree(const Item * pi,const Tree * ptree)
{
return (SeekItem(pi,ptree).child == NULL) ? false:true;
}
Considerrations In Deleting an Item
接下来就是作者本人认为“most difficult”的部分,这里作者分以下几种情况:
1.当需要删除的节点没有子节点时:所有需要做的是重置父节点里面的指针为空指针并且使用free()函数来释放被要删除的节点使用过的函数。
2.当需要删除的节点有一个子节点时:删除这个节点使得子节点与树剩下的部分分离了。这时候需要把子节点的地址赋给先前在父结点中被要删除的节点占据的位置上
3.最后,也是最难的一种情况是:删除有两个子树的节点。这里的重点是:由于右子树曾经是被删除节点的子树的一部分,所以右子树中的每一个项目都比要删除节点的父节点要小,考虑要把右子树放在什么位置:因为右子树的项目都比父节点要小,所以你要从父节点的左子树开始往下寻找,然后,由于右子树的项目都比左子树的小,所以你需要看一下左子树中的右子树是否有足够的空间留给需要放置的节点;如果没有,你需要继续沿着节点的右子树往下走去寻找,知道你找到能够仿制的位置为止。自此,作者将删除分成了两个部分:
Deleting a Node
这里作者提到有两点年需要注意:
1.程序需要去确认需要被删除的节点的父节点
2.去修真这个指针,这个的代码必须传递指向删除函数的指针的地址
考虑到该函数的参数是指针的地址,这个参数就会是Trnode **类型,也就是指向Trnode类型的指针的指针。具体代码如下:
static void DeleteNode(Trnode **ptr)
{
Trnode * temp;
if( (*ptr)->left == NULL)
{
temp = *ptr;
*ptr = (*ptr)->right;
free(temp);
}
else if( (*ptr)->right == NULL)
{
temp = *ptr;
*ptr =(*ptr)->left;
free(temp);
}
else
{
for(temp = (*ptr)->left; temp->right != NULL;
temp = temp->right)
continue;
temp->right = (*ptr)->right;
temp = *ptr;
*ptr =(*ptr)->left;
free(temp);
}
}
这里需要注意的是:代码使用了一个临时的指针在父节点的指针被重置之后去追踪这个需要删除的项目的地址,而且你需要为free()函数提供相关信息。
Deleting an Item
删除一个节点你需要删除节点内的项目,要找到你想删除的节点你需要根据节点中的项目寻找,具体代码如下:
bool DeleteItem(const Item * pi,Tree * ptree)
{
Pair look;
look = SeekItem(pi,ptree);
if(look.child == NULL)
DeleteNode(&ptree->root);
else if(look.parent->left == look.child)
DeleteNode(&look.parent->right);
ptree->size--;
return true;
}
参考文献:Stephen Prata《C Primer Plus 6th》英文版