二叉排序树(Binary Sort Tree) ,又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。
1、若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2、若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3、它的左、右子树也分别为二叉排序树。
总之每一个结点都满足下面条件: 左值 < 根值 < 右值
目的不是为了排序,而是为了提高查找和插入删除关键字的速度。
在仔细的介绍代码之前,光看这种结构,也能感受出,有点像二分那样,每次都可以省掉左子树或者右子树(但左右子树个数不一定相同),从而提高查找速度
『结点结构』
typedef int TElemType;
typedef struct BiTNode //结点结构
{
TElemType data; //结点数据
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode , *BiTree;
① 二叉排序树查找操作
/*
递归查找二叉排序树T是否存在key
@param T二叉排序树,key要找的值
@param f 指向T的双亲,初始调用值为NULL
@return success 返回 指针p指向 数据元素结点, 返回TRUE
error 返回 指针p指向 查找路径最后一个节点,返回FALSE */
Status SearchBST(BiTree T, int key , BiTree f, BiTree *p ){
if(!T){ //查找不成功
*p = f;
return FALSE;
}else if( key == T->data ){ //查找成功
*p = T;
return TRUE;
}else if( key < T->data ){
return SearchBST( T->lchild, key, T , p );//左子树继续查找
}else {
return SearchBST( T->rchild, key, T , p );//右子树继续查找
}
}
【解释】递归查找,类似二分,与key相等则返回,比key小走左边,比key大走右边。(从左到右,从小到大)
62
↙ ↘
58 88
↙ ↙ ↘
47 73 99
↙ ↘ ↙
35 51 93
↙ ↘ ↙ ↘
29 37 49 56
↙ ↙ ↘
36 48 50
②二叉排序树插入操作
/*
当二叉树排序树T中不存在关键字等于 key 的数据元素时,
插入key 并返回TRUE,否则返回FALSE */
Status InsertBST(BiTree *T, int key){
BiTree p,s;
if( ! SearchBST(*T, key, NULL, &p)){ //查找不成功
s = (BiTree) malloc (sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
if(!p){ // (1)连根结点都没有
*T = s;
}else if(key < p->data){ // (2)小于根 放左子树
p->lchild = s;
}else { // (3)大于根 放右子树
p->rchild = s;
}
return TRUE;
}else{
return FALSE; // 树中已有关键字相同的结点
}
}
【解释】插入时,先查询元素是否存在,存在就不插入了。
★找完之后,p已经是最接近,且最适合,且就是要插入点要插入的地方。
(1)这个是给第一个点设立的,如果连一个根节点都没有,那就新建一个根节点;【插入 62 的时候】
(2)如果小于根,那就插在左子树,因为要遵循,左子树小于根,根小于右子树;
(3)如果大于根,那就插在右子树,因为要遵循,左子树小于根,根小于右子树。
62
↙ ↘
58 88
↙ ↙ ↘
47 73 99
↙ ↘ ↙
35 51 93
↙ ↘ ↙ ↘ ↘
29 37 49 56 95
↙ ↙ ↘
36 48 50
【插入一个新节点
95 】
一开始先查找元素在不在树里,key = 95 ,从62开始找,62 > 95,走右边 到88 ….再走99,再走93,发现到头了,返回没找到
没找到?那就插入一个新的结点吧。那插在93的左边还是右边呢。
遵循左子树小于根,根小于右子树,就插在右子树吧。就插入成功了。
③二叉排序树删除操作
/*
若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点
并返回TRUE;否则返回FALSE */
Status DeleteBST(BiTree *T,int key){
if(!*T){ //找到最后没有关键字等于key的数据元素
return FALSE;
}else{
if(key == (*T)->data){ //找到key
return Delete(T);
}else if(key < (*T)->data){ //小于根 找左边
return DeleteBST(&(*T)->lchild , key);
}else { //大于根 找右边
return DeleteBST(&(*T)->rchild , key);
}
}
}
【解释】
显然,删除操作要在树里存在这个 和 key 值相等的结点的情况下才能删除,不然连这个点都不存在,也就没有删除的意义了。
类似查找操作。找到点,执行 Delete( )操作。
/*从二叉排序树中删除结点p,并重连它的左或右子树*/
Status Delete(BiTree *p){
BiTree q,s;
if((*p)->rchild == NULL){ //【1】右子树空,链接左子树
q = *p; *p = (*p)->lchild; free(q);
}else if((*p)->lchild == NULL){ //【2】左子树空,链接右子树
q = *p; *p = (*p)->rchild; free(q);
}else{ //【3】左右子树都有
q = *p; s = (*p)->lchild; //【4】左转
while(s->rchild){ //【4】找到右的尽头
q = s; s = s->rchild;
}
(*p)->data = s->data;
if(q != *p){
q->rchild = s->lchild; //【5】
}else{
q->lchild = s->lchild; //【6】
}
free(s);
}
return TRUE;
}
【解释】
在删除的时候,我们我们肯定会要到四种情况。
1、这个结点是叶子;2、这个结点有右子树,没有左子树;3、这个结点有左子树,没有右子树;4、这个结点既有左子树,也有右子树
情况一、这个结点是叶子。例如删掉 29 或 50:
62
↙ ↘
58 88
↙ ↙ ↘
47 73 99
↙ ↘ ↙
35 51 93
↙ ↘ ↙ ↘
p → 29 37 49 56
↙ ↙ ↘
36 48 50 ← p
【直接删掉,毫无影响】
情况二、这个结点有右子树,没有左子树,即代码中【1】部分,例如删掉35
62 62
↙ ↘ ↙ ↘
58 88 58 88
↙ ↙ ↘ ↙ ↙ ↘
47 73 99 47 73 99
↙ ↘ ↙ ↙ ↘ ↙
p → 35 51 93 37 51 93
↘ ↙ ↘ ↙ ↙ ↘
37 49 56 36 49 56
↙ ↙ ↘ ↙ ↘
36 48 50 48 50
【那么把p结点的右子树 赋值为 p 就完成了拼接】 【删除掉35,拼接37】
【问】删掉 35 ,那么肯定要有一个值来接替 35 的位置,那么哪个值适合呢?
【答】肯定是 37 嘛,35 连 左孩子都没有, 37 是他王位唯一的继承人(2333不给他给谁)
【问】那这样直接把 37 搬过去,如果 37 下面还有数值,怎么办?能保证依旧排序吗?
【答1】当然能保证。37做为根的子树,必然也是排好序的,那么 37 结点以下我们就不用在证明了。
又因为 37 是在 35 到 47 范围内的。
(为什么 47一定会比 37 大?)因为当你作为 37 作为 key 进来的时候,遇到47,自然是向左下走的,因为又有35,肯定是在35的右下。
所以 37 放在47的左下方,毫无问题。遵循 左子结点 < 根节点 < 右子结点 (自己走一遍就清楚了)
情况三、这个结点有左子树,没有右子树,即代码中【2】部分,例如删掉 99
62
↙ ↘
58 88
↙ ↙ ↘
47 73 99 ← p
↙ ↘ ↙
35 51 93
↙ ↘ ↙ ↘
29 37 49 56
↙ ↙ ↘
36 48 50
【把p结点的左子树赋值给p就完成拼接,再把留下的
99 给释放掉】
至于如果 93下面还有数值,是否保证依旧是排序的?我想这个和上面的是异曲同工的吧,不用解释了
情况四,这个结点有左子树,又有右子树,即代码中【3】部分,例如删掉 47
62
↙ ↘
58 88
↙ ↙ ↘
p → 47 73 99
↙ ↘ ↙
35 51 93
↙ ↘ ↙ ↘
29 37 49 56
↙ ↙ ↘
36 48 50
【问】删掉 47 那么我们就要找一个值 来代替 47 的位置,那找哪个值好呢,肯定是 47 的左子树里面最大的,或者 47 右子树里最小的,为什么呢?
【答】因为 47 这个位置啊,要很恰好,他这两个孩子,35为根的子树 和 51 为根的子树,必须要担当起他的这个位置的 职责。(突然中二)
首先我们肯定知道的是, 35 为根的子树里的每一个 结点 都小于 51 为根的子树的结点 (不想解释了,自己模拟一遍)
(1)如果让 35 子树 里的某个结点来 接管 47 这个位置
假设是35,他上去当的话,不行啊,37不服气啊。因为 要在 47 这个位置,必须是老大,他的左子树 都要小于 他。
虽然35靠的和47进,那也没用,就算上去了,还会被 37 造反掉。
如果35 接管 47 ,那 37 和36 都要叛逃到 51 的阵营里了。
所以必然是 最大的 37 来当,这样就满足了 37 根节点比 他的左孩子都要大的特点。
(2)如果让 51 子树 里的某个结点来 接管 47 这个位置
同理,肯定是要找最小的,虽然51 离 47 近,那也没用,小的 48 上去了,才能保证 在 51阵营里,都比48更大。
★而我的代码给出的是(1)种情况,让 结点 的左孩子 上去。 其实也可以写一个 右孩子的
【问】为什么要【4】左转,找到右子树的尽头,才能找到 左子树里最大的值 ?
【答】左转,找到35,然后向右找,找到右边的尽头。找到 37 (如果37 的右边还有,那继续往右走)
这个 37 是 35 为根的子树里面 最大数了。(大于35 的,都在35的右边,大于37的都在37的右边,37没右边,那37就最大了)
【问】找到最大值 37 了,我要让这个最大值继位 47 的位置,那 37 还有部下怎么办?
【答】那只好让37的部下分配到其他人的手下啦。
注意:这个时候,37只有左子树,没有右子树了。如果有右子树,那就不会是37 当老大了。
因为 37 晋升了,原本 连着37的 那个 35 右子树 就刚好断开了,那就让 35 和36 连起来。
37 拍拍屁股走人了,37原来的工作就让 36 来接手做了。
(真是残酷的职场,35,36:气的我的绿了,作为37的老板35没走,36的老板37 走了,谁叫刚好 37 适合呢)
★代码中:p指针 所指 即 要换的位子(47)
q指针 所指 即 找到要换的(37)的老板
s指针 所指 即 找到晋升的(37)
这个例子刚好是代码中【5】这种情况,q 来接管 s 的左孩子
62 62
↙ ↘ ↙ ↘
58 88 58 88
↙ ↙ ↘ ↙ ↙ ↘
37 73 99 37 73 99
↙ ↘ ↙ ↙ ↘ ↙
35 51 93 35 51 93
↙ ↘ ↙ ↘ ↙ ↘ ↙ ↘
29 【】 49 56 29 36 49 56
↙ ↙ ↘ ↙ ↘
36 48 50 48 50
那代码中【6】这种情况呢?
其实很简单,咱们再添一个 45 到 48 的手下。(上面的我都省略了,只看 51 这个子树)
51
↙ ↘
49 56
↙ ↘
48 50
↙
45
删掉 49,那么48上位,这个时候p = q ,那就直接48 把他的左孩子带过去吧,继续当p的左孩子
其实还有一种除了 【5】【6】两种情况以外另一种情况,比如 35 要删掉。(35都绿过了,那就让他紫一下吧)
往左找,发现就 29 自己一个人,那 29 上去 毫无牵挂,29 的孩子随你操作(反正都空的)
35
↙ ↘
29 36
【总的代码】附带层次遍历
#include <iostream>
#include <string.h>
#include <cstdio>
#include <stdlib.h>
#include <queue>
#define FALSE 0
#define TRUE 1
#define Status int
using namespace std;
typedef int TElemType;
typedef struct BiTNode //结点结构
{
TElemType data; //结点数据
//int floor;
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode , *BiTree;
/*
递归查找二叉排序树T是否存在key
@param T二叉排序树,key要找的值
@param f 指向T的双亲,初始调用值为NULL
@return success 返回 指针p指向 数据元素结点, 返回TRUE
error 返回 指针p指向 查找路径最后一个节点,返回FALSE */
Status SearchBST(BiTree T, int key , BiTree f, BiTree *p ){
if(!T){ //查找不成功
*p = f;
return FALSE;
}else if( key == T->data ){ //查找成功
*p = T;
return TRUE;
}else if( key < T->data ){
return SearchBST( T->lchild, key, T , p );//左子树继续查找
}else {
return SearchBST( T->rchild, key, T , p );//右子树继续查找
}
}
/*
当二叉树排序树T中不存在关键字等于 key 的数据元素时,
插入key 并返回TRUE,否则返回FALSE */
Status InsertBST(BiTree *T, int key){
BiTree p,s;
if( ! SearchBST(*T, key, NULL, &p)){ //查找不成功
s = (BiTree) malloc (sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
if(!p){ // 连结点都没有,那新建
// s->floor = 0;
*T = s;
}else if(key < p->data){ // 小于根 放左子树
// s->floor = p->floor+1;
p->lchild = s;
}else { // 大于根 放右子树
// s->floor = p->floor+1;
p->rchild = s;
}
return TRUE;
}else{
return FALSE; // 树中已有关键字相同的结点
}
}
/*从二叉排序树中删除结点p,并重连它的左或右子树*/
Status Delete(BiTree *p){
BiTree q,s;
if((*p)->rchild == NULL){ //右子树空,链接左子树
q = *p; *p = (*p)->lchild; free(q);
}else if((*p)->lchild == NULL){ //左子树空,链接右子树
q = *p; *p = (*p)->rchild; free(q);
}else{ //左右子树都有
q = *p; s = (*p)->lchild; //左转
while(s->rchild){ //找到右的尽头
q = s; s = s->rchild;
}
(*p)->data = s->data;
if(q != *p){
q->rchild = s->lchild; //【1】
}else{
q->lchild = s->lchild; //【2】
}
free(s);
}
return TRUE;
}
/*
若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点
并返回TRUE;否则返回FALSE */
Status DeleteBST(BiTree *T,int key){
if(!*T){ //找到最后没有关键字等于key的数据元素
return FALSE;
}else{
if(key == (*T)->data){ //找到key
return Delete(T);
}else if(key < (*T)->data){ //小于根 找左边
return DeleteBST(&(*T)->lchild , key);
}else { //大于根 找右边
return DeleteBST(&(*T)->rchild , key);
}
}
}
void prin(BiTree T ){
if(!T) return ;
BiTree tr;
int lastf=0;
queue<BiTree> queue;
queue.push(T);
while(!queue.empty()){
tr = queue.front();
queue.pop();
// if(lastf+1==tr->floor)
// printf("\n");
printf("%5d",tr->data);
// lastf=tr->floor;
if(tr->lchild)
queue.push(tr->lchild);
if(tr->rchild)
queue.push(tr->rchild);
}
}
int main()
{
int a[12] = {62,88,58,47,35,73,51,99,37,93,36,40};
BiTree T = NULL;
for(int i = 0; i<12; i++){
InsertBST(&T, a[i]);
}
prin(T);
return 0;
}
二叉排序树总结:
比较次数就是他的层数,有效的减少了查找时比较的次数。
但最坏的时候,就是成了一条线,都是按顺序插入进去的,这就尴尬了,那其实就是一个顺序表。不平衡的最坏情况 O( n )
我们更希望二叉排序树能够平衡起来,那就可以达到折半查找的效果了。平衡二叉排序树:查找的时间复杂O(logn)。
真是劳心劳力的画了好多图。。二叉排序树的各种情况基本都考虑进去了,分享给大家,对大家有所帮助