线索二叉树
在学习了基本的二叉树操作之后,数据结构就来到了图结构的学习,以及相关的基本算法。 完成之后,就到了二叉树的进阶学习,有序二叉树,平衡二叉树,哈夫曼树,到后面的B树,B+树,B*树。 今天,就先简单地说一瞎有序二叉树。
关于有序二叉树,是二叉树的一种特殊的状态,以树根为起始点,左子树的值比树根的值小,而右子树的值比树根的值大。
图形来说的话,相当于这样的一个二叉树:
在这个二叉树中根结点的值为15,而根节点的左子树有两个叶子结点,左叶子的值为5,右叶子的值为13. 根节点的右子树只有根节点值为20.
从上图中我们可以看出,根节点的值相当于一个“界限”,比根节点数据小的值被分配在左子树上,反之分配在右子树上。
这样我们就会得出这个基本的数据结构:
typedef struct Tree* pTree; /* 结构指针 */
typedef struct Tree eTree;
struct Tree{
int data; /* 数据域 */
pTree left; /* 左子树 */
pTree right; /* 右子树 */
};
同很多数据结构一样,线索二叉树也有建立,插入,查找,删除,清除等基本的功能。
pTree CreatTree( pTree tree ,int RootNum); /* 创造一个树 */
int InsertElem( pTree tree , int data ); /* 向树中增添子树 */
int SearchElem( pTree tree , int data ); /* 在树中查找一个值 */
void DeleteElem( pTree tree ,int data ); /* 删除一个特定的结点 */
void DestroyTree( pTree tree ); /* 销毁一个树 */
创建线索二叉树
创建二叉树的方法一般来说有两种: 先序中序创建,#法创建 。 但是因为线索二叉树中结构相对比较简单,因此我们通过一般方法来创建即可。 我们先创建一个根节点,其次再根据具体的需求来添加需要的结点。 因此:
pTree CreatTree( pTree tree ,RootNum ){
tree = (pTree)malloc( sizeof(eTree) );
assert( tree != NULL); /* 需 #include<assert.h>
tree->left = tree->right = NULL; /* 树初始化 */
tree->data = RootNum;
return tree;
}
刚开始,我们需要将left 和 right 指针置空,在需要的时候再使用。
向树中插入结点
在二叉树中,插入一个结点的条件是树中有空节点,然后动态分配内存插入即可。 但在线索二叉树中,不但要考虑以上步骤,还要考虑待插结点的数值域是否满足树的情况。综合以上情况,我们可以写出:
int InsertElem( pTree tree , int InsertData ){
pTree tmp = tree; /* 分配临时结点 */
while( (tmp->letf != NULL) && ( tmp->right != NULL) ){
if( tmp->data < InsertData ){ /* 当左右子树都不为空,判断被插数值与结点数值大小,小于则进入左子树 */
tmp = tmp->left;
continue;
}
else if( tree->data > InsertData ){
tmp = tmp->right; /* 进入右子树 */
continue;
}
else{
printf("The number is not unique in the tree.\n");
return -1; /* 被插数值与树中存在的值相同,则返回-1,插入失败 */
}
}
if( (tmp->left == NULL) && (tmp->data < InsertData) ){
eTree *NewNode = (eTree*)malloc( sizeof(eTree) );
assert( NewNode != NULL ); /* 左结点为空,可以使用 */
NewNode->left = NewNode->right = NULL; /* 置空 */
NewNode->data = InsertData; /* 赋值 */
tmp->left = NewNode;
return 1;
}
else if( (tmp->right == NULL) && (tmp->right >InsertData) ){
eTree *NewNode = (eTree*)malloc( sizeof(eTree) );
assert( NewNode != NULL ); /* 右结点为空,可以使用 */
NewNode->right = NewNode->left = NULL; /* 置空 */
NewNode->data = InsertData; /* 赋值 */
tmp->right = NewNode;
return 1;
}
else{
retunr -1; /* 插入失败 */
}
}
我们可以清晰地看出,有序树的插入是要满足两个条件的,而且在树中,一个值对应一个结点,这样使得树的查找和遍历算法变得容易。
在树中查找一个值
在学习二叉树的遍历算法中,有先序,中序,后序遍历算法。 三种算法的时间复杂度都相同,只是输出语句的顺序不同。 当然,这三种算法都是通过递归来实现的,那么有没有 不通过递归的方法 进行二叉树遍历的算法?
在线索二叉树中,这种算法很容易实现,而且时间复杂度也是O(n)。
pTree SearchElem( pTree tree ,int data ){
pTree tmp = tree;
while( tmp != NULL ){
if( tree->data == data ){
return data; /* 找到寻求的值 */
}
else if( (tree->data < data) && (tmp->left != NULL) ){
tmp = tmp->left ; /* 进入左子树 */
continue;
}
else if( (tree->data > data) && (tmp->right != NULL) ){
tmp = tmp->right; /* 进入右子树 */
continue;
}
else{
printf("Cannot find the element in the tree .\n");
return tree;
}
}
printf("The tree is empty.\n"); /* 树为空 */
return tree;
}
刚开始自然晦涩难懂,但是只要理解了有序树的层次关系,那么一切就会变得简单易懂。其实,寻找响应的值和插入一个值是相同的过程,只是略微的目的不同。
在树中删除一个结点
在树中删除一个结点的过程较为复杂,因为删除的结点分为三种情况:
-
结点时叶子,left 和 right 为NULL, 此时直接删除此结点就可以。
-
结点有一个子树时,将子树与结点的上一个结点相连。 就相当于用唯一的子树来弥补删除此结点时出现的“空位”。
-
结点有两个子树,这种情况最麻烦,因为两个树都不为NULL,因此要让结点删除后不影响子树的信息。 通常的方法有两种。
一种是,假设要删除的结点时p找到中序遍历的前驱结点pre,用pre代替结点p,然后将pre删除。
另一种是找到结点p的中序遍历后继结点next,用next替换p,再将next删除,确保二叉树的性质不会变化。
而通过我们前面定义的二叉树结构来实现删除操作,比较困难,因此我们引进一个前驱结点parent,用来指示前驱结点。
struct Tree{
int data;
pTree left; /* 左子树 */
pTree right; /* 右子树 */
pTree parent; /* 前驱结点 */
};
void DeleteElem( pTree tree , data ){
eTree *q , *s ;
eTree *node = SearchElem( tree, data ); /* 查找这个结点 */
if( eTree == NULL ){
printf("The node is not belonged to the tree.\n");
return;
}
if( (node->left == NULL) && (node->right == NULL) ){
printf("The node is a leaf.\n");
node->parent->left = node->parent->right=NULL; /* 结点为叶子 */
free(node);
}
else if( node->right == NULL ){
/* 右子树为空 */
q = node;
node = node->left;
if( q == q->parent->right ){
q->parent->right = node; /* 结点时其父结点的右孩子 */
}
else{
q->parent->left = node; /* 结点时去其父节点的左孩子 */
}
free(q);
}
else if( node->left == NULL ){
/* 左子树为空 */
q = node;
node = node->right;
if( q == q->parent->right ){
q->parent->right = node; /* 结点是其父节点的右孩子 */
}
else{
q->parent->left = node; /* 结点是其父节点的左孩子 */
}
}
else{
/* 左右子树都不为空 */
q = node;
s = node->left;
/* 寻找其中序前驱结点,其中中序前驱在其左子树的右下角 */
while( s->right ){
q = s;
s = s->right;
}
/* 循环结束,s就指向要删除结点的中序前驱 */
node->data = s->data;
if( q != node ){
q->right = s->left;
}
else{
q->left = s->left;
}
free(s);
}
}
在删除同时具有左孩子和有孩子的结点时,务必使之后的结点的不会出现断点,不会使有序二叉树的结构被破坏。
销毁一个二叉树
因为二叉树是具有层次结构的数据结构,因此使用递归来销毁一个二叉树就自然省时省力。
我们来使用上面的例子:
现在要删除左边的树,使用递归的思想来看,我们从根节点开始进入二叉树,此时进入有两条路,我们一般选择左边的优先:
- 判断根节点8是否为NULL,不是,则判断3是否为NULL,不是则进入3中,接着判断1是否为NULL,不是,则进入1中; 判断1的左子树是否为NULL,是。 则接着判断1的右子树是否为NULL,是,则删除1结点。 接着将3的left赋为NULL。
- 接着判断3的右孩子是否为NULL,否,进入6; 判断6的左孩子是否为NULL,否,进入4; 判断4的左孩子为NULL,有孩子也为NULL,删除4,返回到6; 判断6的右孩子,不为NULL,进入7,7为叶子结点,删除。 此时6也是叶子节点,删除6结点。
- 回到3结点,左孩子右孩子都为NULL,删除3结点,返回到根节点8中。
- 此时根节点的左孩子为NULL,判断右孩子是否为NULL,否,进入到10中; 10的左孩子为NULL,进入14中; 14为叶子节点,删除。 此时10也为叶子节点,删除。 此时根节点8也为叶子节点,删除。
- 这样就完成对二叉树的销毁。
void DestroyTree( pTree tree ){
if( tree == NULL ){ /* 空树不能进行二次free */
return;
}
if( tree->left != NULL ){ /* 删除左子树 */
DestroyTree(tree->left);
tree->left = NULL;
}
if( tree->right != NULL ){ /* 删除右子树 */
DestroyTree(tree->right);
tree->right = NULL;
}
free(tree); /* 删除根节点 */
}
这样就完成了对有序二叉树的基本操作,关于哈夫曼树,B+,B* 树,川一君会在后面讲到。