五一回家还真是没什么事做,一直在床上躺着有种罪恶感。。就想顺手接着写写数据结构,二叉查找树。虽然平时没事的时候也会经常思考它的递归与非递归实现,觉得没什么问题,可是写起来还是各种错误一堆,最后还是翻着书勉强搞定。悲催啊。。
Def:二叉查找树顾名思义是二叉树的一种,但是它除了具有二叉树一般的结构特性之外,还有一个自身的查找树性质。对每个结点N的某个数据Key来说,都满足key[left] < key[N] < key[right]. 即结点大于它的左子树但小于右子树(等于也可以,根据实现不同),这里的小于可以是对于结点记录域中某一个用于排序的数据大小或者其他形式的比较。
上面是算法导论上的图。
二叉查找树上的操作一般而言有:
查找与特定关键字匹配的结点,成功则返回结点位置否则返回NULL。
插入某一关键字的结点。
删除某一关键字的结点。
查找具有最小关键字/最大关键字的结点。
因为二叉查找树的特殊性质,以上的所有操作都是以O(log n)的时间运行。
下面是操作的实现,参考Data Structures and Algorithm Analasis in C这本书。上面的例程基本上都是递归实现,我觉得无论是用来理解树结构本身还是理解递归过程都是很好的方法,因为树的定义就带有递归性质,所以并不晦涩。
首先是ADT的一些类型声明,用于描述和理解方便。
typedef int S_ElemType; // 通用关键字数据类型
struct BiTreeNode;
typedef struct BiTreeNode* PtrToBiTreeNode;
/* 根据二叉树的递归定义,它和它的左右儿子本质上是相同的
* 这里都用指向结点的指针来表示
*/
typedef PtrToBiTreeNode SearchTree;
typedef PtrToBiTreeNode Position;
/* 二叉树结点结构 */
typedef struct BiTreeNode{
S_ElemType data;
SearchTree left; // 左子树
SearchTree right; // 右子树
}BiTreeNode;
根据二叉树的性质,FIND操作的想法很简单,假设待查找的关键字用X表示,从某一个结点T开始搜索,该结点关键字为K[T], 如果有 X < K[T] 则我们知道该结点只可能在它的左子树,于是查找它的左子树,若X > K[T], 则查找右子树,否则该结点即为目标结点,直接返回指向它的指针。在过程中,若查找到NULL, 则意味查找失败。
很显然,上面的描述是一个递归过程,因此递归的实现是很自然的事情。首先找递归的两个基本要素:基准情形和推进。
这里的基准的情形有两个:
如果查找的结点不存在,即NULL, 则以为查找失败,因此返回NULL
查找的结点就是目标结点,因此返回它本身的位置
推进:递归查找左子树或者右子树。
代码如下:
SearchTree S_find(SearchTree T, S_ElemType e)
{
if(T == NULL)
return NULL;
if(e < T->data)
return S_find(T->left, e);
else if(e > T->data)
return S_find(T->right, e);
else
return T;
}
非递归的版本:
SearchTree S_find(SearchTree T, S_ElemType e)
{
while(T != NULL){
if(e < T->data)
T = T->left;
else if(e > T-data)
T = T->right;
else
return T;
}
return NULL;
}
用于查找最小值和最大值的过程 FINDMIN, FINDMAX 想法与FIND差不多。一个递归查找左子树,一个递归查找右子树。不同在基准情形,以FINDMIN为例,当查找的结点没有左子树时,则当前结点就是最小的结点,FINDMAX类似。代码如下:
SearchTree S_findMin(SearchTree T)
{
if(T == NULL)
return NULL;
else if(T->left == NULL)
return T;
else
return S_findMin(T->left);
}
下面是插入的方法:
考虑到当待插入的树为NULL的特殊情形, insert返回一个指向新树根的指针。若待插入的数值小于当前结点的关键字,则递归地插入左子树,否则右子树。考虑基准情形,一个是提到过的NULL情况,我们用malloc函数分配一个新的结点,把关键字设置为参数,左右子树设置为NULL,然后返回这个新结点作为新的树根。另一个是当前结点不为空,则在递归查找左右子树之后返回自身,以维持一致性。
SearchTree S_insert(SearchTree T, S_ElemType e)
{
if(T == NULL){
T = malloc(sizeof(struct BiTreeNode));
assert(T != NULL);
T->data = e;
T->left = T->right = NULL;
}
else if(e < T->data)
T->left = S_insert(T->left, e);
else if(e > T->data)
T->right = S_insert(T->right, e);
return T;
}
这里的实现有可以修改的地方,忽略了关键字已经存在的情况,也可以修改以插入重复关键字或者增加一个重复域。
剩下的是删除一个特定关键字结点的方法,根据要被删除结点的儿子数有不同的操作。
有一个或没有:可以用类似单链表的删除方法。
有两个儿子:用右子树中最小的元素代替当前结点(这样不会破坏查找树的性质),然后递归调用删除函数,删除那个结点。因为右子树最小的结点一定没有左子树,因此可以用前面叙述的方法很容易删除。
贴个算法导论上的图:
实现如下:
SearchTree S_delete(SearchTree T, S_ElemType e)
{
Position tmp;
if(T == NULL)
return NULL;
else if(e < T->data)
T->left = S_delete(T->left, e);
else if(e > T->data)
T->right = S_delete(T->right, e);
else{
// Two children
if(T->left != NULL && T->right != NULL){
tmp = S_findMin(T->right);
T->data = tmp->data;
T->right = S_delete(T->right, T->data);
}
// 0 or 1 children
else{
tmp = T;
if(T->left == NULL)
T = T->right;
else
T = T->left;
free(tmp);
}
}
return T;
}
先写这么多吧。还有关于遍历的内容。
下面贴下完整的ADT声明:
#ifndef BITREE_H_INCLUDED #define BITREE_H_INCLUDED typedef int S_ElemType; struct BiTreeNode; typedef struct BiTreeNode* PtrToBiTreeNode; typedef PtrToBiTreeNode SearchTree; typedef PtrToBiTreeNode Position; SearchTree S_makeEmpty(SearchTree T); Position S_findMin(SearchTree T); Position S_findMax(SearchTree T); Position S_find(SearchTree T, S_ElemType e); SearchTree S_insert(SearchTree T, S_ElemType e); SearchTree S_delete(SearchTree T, S_ElemType e); void S_preOrder(SearchTree T); void S_midOrder(SearchTree T); void S_postOrder(SearchTree T); void S_seqOrder(SearchTree T); #endif // BITREE_H_INCLUDED


浙公网安备 33010602011771号