二叉查找树BST总结分析
1.ADT(抽象数据类型)
ADT BST
{
数据元素:数据节点(键值,位置信息)
数据关系:父子关系
基本操作:
插入节点
查找节点
删除节点
遍历节点(前中后层序)
}
2.二叉查找树性质:(不存在相同键值的节点)
1.空树
2.非空树并且左子树所有节点的键值均小于本节点的键值
右子树所有的节点的键值均大于本节点的键值
该节点的左右子树均为二叉查找树
3.二叉查找树的核心操作复杂度分析:
T( n ) = T( n/2 ) + O( 1 )
T( n ) = T( n/4 ) + 2 O( 1 )
T( n ) = T( n/8 ) + 3 O( 1 )
…
…[共 logN 次]
…
T( n ) = T( 1 ) + logN·O( 1 )
T( n ) = O(logN)
4.二叉查找树的基本操作分析:
1.插入节点:
插入节点的核心在于查找到当前的节点的父亲节点,对于空树来说,插入的节点直接充当根节点
对于非空树来说,操作步骤如下:(c节点代表当前访问到的的节点的位置,p节点代表带插入的节点,n代表待插入节点的父节点)
1.c节点为空,那么n即为c节点的父亲节点,此时我们要进行大小比较少,如果p节点的键值比n节点的键值小,那么p作为n的左孩子否则作为右孩子
2.否则,比较p节点的键值与c节点的键值
3.如果键值相同,说明用户正在试图插入一个已经存在的节点,此时根据情况,我们可以抛出异常或者我们提醒用户
4.如果p节点的键值比c节点的键值要小,说明p节点应该存在于c节点的左子树中,此时,n节点为c节点,我们将c转移到左孩子上,跳转到操作1
5.否则,跳转到右孩子上,然后执行操作1
2.查找节点:
查找节点的操作和插入节点的操作大致是相同的,复杂度都是logN:(k代表待查找的键值,c代表当前的节点(初始为根节点))
1.如果k等于c的键值,说明该节点我们已经查找到了,返回查找到的地址就好
2.如果c节点为空,说明该节点查找失败,该节点不存在,返回NULL即可
3.否则,我们进行判断,如果k的键值相对较小,说明待查找的节点要么不存在要么就在左子树中,我们将c跳转到左子树中,执行操作1
4.否则,说明待查找的节点要么不存在,要么就存在在右子树中,我们将c跳转到右孩子中,执行操作1
3.遍历结点:(前中后层序遍历二叉树是一样的)
4.删除节点:
从 BST 中删除节点比插入节点难度更大。因为删除一个非叶子节点,就必须选择其他节点来填补因删除节点所造成的树的断裂。如果不选择节点来填补这个断裂,那么就违背了 BST 的性质要求。
删除节点算法的第一步是定位要被删除的节点,这可以使用前面介绍的查找算法,因此运行时间为 O(log2n)。接着应该选择合适的节点来代替删除节点的位置,它共有三种情况需要考虑。
- 情况 1:如果删除的节点没有右孩子,那么就选择它的左孩子来代替原来的节点。二叉查找树的性质保证了被删除节点的左子树必然符合二叉查找树的性质。因此左子树的值要么都大于,要么都小于被删除节点的父节点的值,这取决于被删除节点是左孩子还是右孩子。因此用被删除节点的左子树来替代被删除节点,是完全符合二叉搜索树的性质的。
- 情况 2:如果被删除节点的右孩子没有左孩子,那么这个右孩子被用来替换被删除节点。因为被删除节点的右孩子都大于被删除节点左子树的所有节点,同时也大于或小于被删除节点的父节点,这同样取决于被删除节点是左孩子还是右孩子。因此,用右孩子来替换被删除节点,符合二叉查找树的性质。
- 情况 3:如果被删除节点的右孩子有左孩子,就需要用被删除节点右孩子的左子树中的最下面的节点来替换它,就是说,我们用被删除节点的右子树中最小值的节点来替换。
我们知道,在 BST 中,最小值的节点总是在最左边,最大值的节点总是在最右边。因此替换被删除节点右子树中最小的一个节点,就保证了该节点一定大于被删除节点左子树的所有节点。同时,也保证它替代了被删除节点的位置后,它的右子树的所有节点值都大于它。因此这种选择策略符合二叉查找树的性质。
和查找、插入算法类似,删除算法的运行时间也与 BST 的拓扑结构有关,最佳情况是 O(log2n),而最坏情况是 O(n)。
5.C++类封装(代码示例):
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
using namespace std;
typedef struct node
{
int key;
struct node* left;
struct node* right;
}point;
class errorsame
{
};
class bst
{
public:
bst()
{
root=NULL;
num=0;
}
~bst()
{
clear(root);
}
void add(int);
void del(int);
point* find(int);
void clear(point*);
void preorder(point*);
void midorder(point*);
void aftorder(point*);
void rankorder();
point* returnroot()
{
return root;
}
private:
point* root;
int num;
};
void bst::clear(point* p)
{
if(p==NULL) return ;
else
{
clear(p->left);
clear(p->right);
free(p);
}
}
void bst::add(int p)
{
if(num==0)
{
root=new point;
root->right=root->left=NULL;
root->key=p;
num++;
return ;
}
else
{
try
{
point *k=root;
point* w=root;
while(w!=NULL)
{
if(w->key==p) throw errorsame();
else
{
if(w->key>p) w=w->left;
else w=w->right;
}
if(w!=NULL) k=w;
}
if(k->key>p)
{
point *a=new point();
a->left=a->right=NULL;a->key=p;
k->left=a;
}
else
{
point *a=new point();
a->left=a->right=NULL;a->key=p;
k->right=a;
}
num++;
}
catch(errorsame e)
{
cout<<"try to add the same point in the tree!"<<endl;
}
}
}
void bst::del(int p)
{
point* now=root;
point* father=NULL;
while(now->key!=p)
{
if(now->key>p)
{
father=now;
now=now->left;
}
else
{
father=now;
now=now->right;
}
if(now==NULL)
{
cout<<"can not find the point!"<<endl;
return ;
}
}
if(father==NULL)
{
point* help=root;
if(root->right==NULL) root=root->left;
else
{
if(root->right->left==NULL)
{
root->right->left=root->left;
root=root->right;
}
else
{
point* z=NULL;
point* k=root->right;
point* w=root;
while(k->left!=NULL)
{
if(k->left->left==NULL) z=k;
k=k->left;
}
z->left=k->right;
k->left=root->left;
k->right=root->right;
root=k;
}
}
free(help);
}
else if(now->right==NULL)
{
point* help=now;
if(father->left==now) father->left=now->left;
else father->right=now->left;
free(help);
}
else
{
if(now->right->left==NULL)
{
point* help=now;
now->right->left=now->left;
if(father->left==now) father->left=now->right;
else father->right=now->right;
free(help);
}
else
{
point* z;
point* k=now->right;
while(k->left!=NULL)
{
if(k->left->left==NULL) z=k;
k=k->left;
}
if(father->left==now)
{
z->left=k->right;
father->left=k;
k->left=now->left;
k->right=now->right;
}
else
{
z->left=k->right;
father->right=k;
k->left=now->left;
k->right=now->right;
}
}
}
}
point* bst::find(int p)
{
point* a=root;
while(a!=NULL)
{
if(a->key==p) return a;
if(a->key>p) a=a->left;
else a=a->right;
}
}
void bst::preorder(point* p)
{
if(p==NULL) return ;
else
{
printf("%d ",p->key);
preorder(p->left);
preorder(p->right);
}
}
void bst::midorder(point* p)
{
if(p==NULL) return ;
else
{
midorder(p->left);
printf("%d ",p->key);
midorder(p->right);
}
}
void bst::aftorder(point* p)
{
if(p==NULL) return ;
else
{
aftorder(p->left);
aftorder(p->right);
printf("%d ",p->key);
}
}
void bst::rankorder()
{
point* queue[100];
int head=1;
int tail=2;
queue[1]=root;
while(head!=tail)
{
if(queue[head]->left!=NULL) queue[tail++]=queue[head]->left;
if(queue[head]->right!=NULL) queue[tail++]=queue[head]->right;
head++;
}
for(int i=1;i<=tail-1;i++) printf("%d ",queue[i]->key);
}
int main()
{
bst my;
my.add(1);
my.add(15);
my.add(7);
my.add(5);
my.add(11);
my.add(2);
my.add(9);
my.preorder(my.returnroot());
cout<<endl;
my.midorder(my.returnroot());
cout<<endl;
my.aftorder(my.returnroot());
cout<<endl;
my.rankorder();
cout<<endl;
cout<<my.find(7)->key<<endl;
my.del(1);
my.preorder(my.returnroot());
return 0;
}
6.效率低下的原因:
因为作为二叉查找树,根据输入的顺序的不同,最终构造的二叉查找树的拓扑状态是不一样的,所以我们有可能最后构造出来的二叉查找树是类似于一个链表,所以我们的查找的效率会大大的降低,不会打到我们期望的logN的水平,会不断的趋近于n发展,所以说我们需要对BST进行一种优化,即将引出SBT自平衡二叉树。
7.遗留问题:
1.二叉查找树在退化线性结构的时候,复杂度的计算目前尚且不熟练
2.二分法和二叉链表的核心区别
3.为什么说遍历儿茶查找树的效率相对来说会低于线性结构的数据结构,是因为访问点回溯的原因吗