二叉排序树基本性质详解

线索二叉树

在学习了基本的二叉树操作之后,数据结构就来到了图结构的学习,以及相关的基本算法。 完成之后,就到了二叉树的进阶学习,有序二叉树,平衡二叉树,哈夫曼树,到后面的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;
}

刚开始自然晦涩难懂,但是只要理解了有序树的层次关系,那么一切就会变得简单易懂。其实,寻找响应的值和插入一个值是相同的过程,只是略微的目的不同。

在树中删除一个结点

在树中删除一个结点的过程较为复杂,因为删除的结点分为三种情况:

  1. 结点时叶子,left 和 right 为NULL, 此时直接删除此结点就可以。

  2. 结点有一个子树时,将子树与结点的上一个结点相连。 就相当于用唯一的子树来弥补删除此结点时出现的“空位”。

  3. 结点有两个子树,这种情况最麻烦,因为两个树都不为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);
	}
}

在删除同时具有左孩子和有孩子的结点时,务必使之后的结点的不会出现断点,不会使有序二叉树的结构被破坏。

销毁一个二叉树

因为二叉树是具有层次结构的数据结构,因此使用递归来销毁一个二叉树就自然省时省力。
我们来使用上面的例子:
在这里插入图片描述

现在要删除左边的树,使用递归的思想来看,我们从根节点开始进入二叉树,此时进入有两条路,我们一般选择左边的优先:

  1. 判断根节点8是否为NULL,不是,则判断3是否为NULL,不是则进入3中,接着判断1是否为NULL,不是,则进入1中; 判断1的左子树是否为NULL,是。 则接着判断1的右子树是否为NULL,是,则删除1结点。 接着将3的left赋为NULL
  2. 接着判断3的右孩子是否为NULL,否,进入6; 判断6的左孩子是否为NULL,否,进入4; 判断4的左孩子为NULL,有孩子也为NULL,删除4,返回到6; 判断6的右孩子,不为NULL,进入7,7为叶子结点,删除。 此时6也是叶子节点,删除6结点。
  3. 回到3结点,左孩子右孩子都为NULL,删除3结点,返回到根节点8中。
  4. 此时根节点的左孩子为NULL,判断右孩子是否为NULL,否,进入到10中; 10的左孩子为NULL,进入14中; 14为叶子节点,删除。 此时10也为叶子节点,删除。 此时根节点8也为叶子节点,删除。
  5. 这样就完成对二叉树的销毁。
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* 树,川一君会在后面讲到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值