二叉搜索树性质与实现

来源:互联网 发布:韩国成人台 网络电视 编辑:程序博客网 时间:2024/06/06 01:02

一、概念

       搜索树支持许多动态集合操作,包括SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT和DELETE等,因此,一棵搜索树既可以作为一个字典又可以作为一个优先队列。

      二叉搜索树上的基本操作所费时间与这棵树的高度成正比,对于有N个结点的一棵完全二叉树,这些操作的最坏运行时间为O(logN),但对于N个结点构成的线性链而言,同样的操作要花费O(N)的最坏运行时间,幸运的是一棵随机构造的二叉搜索树的期望高度是O(logN),所以这样一棵树上的动态集合的基本操作平均运行时间为O(logN)。

     实际上,我们并不能总是保证随机的构建二叉搜索树,却可以设计它的变体,来保证基本操作具有好的最坏情况性能。红黑树、B树就是这样的例子。其中B树特别适用于二级(磁盘)存储器上的数据库维护。

什么是二叉搜索树?

       二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:

     (1)、若它的左子树不为空,则左子树上所有结点的值均小于等于它的根结点的值;
     (2)、若它的右子树不为空,则右子树上所有结点的值均大于等于它的根结点的值;
     (3)、它的左、右子树也分别为二叉查找树。

      组织形式为:每个结点就是一个对象,除了key和卫星数据外,每个结点还包括left、right和parent。有木有感觉很熟悉?中序遍历其实就是对数据排序结果!所以二叉搜索树又可以用来排序。

为什么需要二叉搜索树?

      目的并非为了排序,而是为了提高查找和删除关键字的速度,不管怎么说,在一个有序的数据集上查找,速度总是要快于无序的数据集。

什么是卫星数据?

      在算法导论里,指的是一条纪录(一个对象中)中除了关键字key以外的其他数据。例如在排序算法中,参与排序的数据称做关键字key,而该对象附带的其他数据则称做卫星数据。在排序的过程中,我们只考虑关键字key的大小。形象一点说,其他数据可以看作是关键字key的卫星,反映了其他数据与key的依属关系。

操作与性能

     假设数据结构为BSTree(二叉树)中内含BSNode(树节点)数据结构,且有成员treeRoot作为这棵树的根节点。

     1.遍历:

      对于具有n个结点的子树的根x,从x遍历一次需要O(n)时间,恰好调用自己两次,一次是左孩子,一次是右孩子。

/*中序遍历  伪代码*/void InOrder(BSNode *pRoot){if(pRoot){InOrder(pRoot->LChild);cout<<pRoot->data<<endl;InOrder(pRoot->RChild);}}

     2.查找:

     从树根开始向下寻找,遇到节点x,比较关键字k与x.data,如果k>x.data说明k如果存在的话一定在x的左子树,否则在右子树。这样递归下去直到叶子节点,如果到达叶子节点都没找到这样的关键字为k的节点,说明找不到。运行时间为O(h),h为这棵树的高度。

/*查找  伪代码输入一个指向树根的指针pRoot与关键字k,如果存在返回指向该节点的指针,不存在返回NUll*/void TreeSearch(<span style="font-family: Arial, Helvetica, sans-serif;">BSNode </span>*pRoot , int k)//递归版本{if(pRoot==NULL && pRoot->data==k)return pRoot;if(pRoot->data < k)return TreeSearch(pRoot->LChild , k);elsereturn TreeSearch(pRoot->RChild , k);}void TreeSearch(BSNode *pRoot , int k)//遍历版本{while(pRoot && pRoot->data!=k){if(pRoot->data < k)pRoot = pRoot->LChild;elsepRoot = pRoot->RChild;}return pRoot;}

      3.最大关键字元素和最小关键字元素:时间复杂度O(h)

       最大关键字元素:通过树根找最左边的那个元素,如果无左孩子,则为根节点最大;

       最小关键字元素:通过树根找最右边的那个元素,如果无右孩子,则为根节点最小;

/*最大最小关键字元素  伪代码*/BSTree* TreeMin(BSNode *pRoot){while(pRoot->LChild)pRoot = pRoot->LChild;return pRoot;}BSTree* TreeMax(BSNode *pRoot){while(pRoot->RChild)pRoot = pRoot->RChild;return pRoot;}

       4.后继和前驱,时间复杂度O(h)。

/*查找后继  伪代码*/BSTree* TreeSUCCESSOR(BSNode *x){if(x->RChild != NULL)//有右子树,则直接定位到右子树最小关键字节点return TreeMin(x->RChild);//---没有右子树,沿树向上搜索到一个有左孩子的节点y,y左子树中包含x(这样x后继是y)BSTree* xParent = x->parent;while(xParent!=NULL && x==xParent->RChild){x = xParent;xParent = xParent->parent;}return xParent;}
      5.插入,沿着根节点向下寻找合适的位置,如果欲插入关键值key>pNode.data,沿着右子树向下,key<=pNode.data,沿着左子树向下;直到叶子节点的某个NULL孩子处插入。时间复杂度为O(h)。

/*插入节点  伪代码在树T中插入节点x,注意到此时不能只传入pRoot,而需要传入T.root*/void TreeInsert(BSTree T , BSNode *x){BSNode *pRoot = T.root;BSTree *pPre = NULL;//存放pRoot的父节点while(pRoot!=NULL){pPre = pRoot;if(pRoot->data > x->data)pRoot = pRoot->LChild;elsepRoot = pRoot->RChild;}x->parent = pPre;if(pPre == NULL)//输入是空树T.root = x;else if(x->data < pPre->data)pPre->LChild = x;elsepPre->RChild = x;}
       6.删除,从一棵二叉树删除节点分为如下三种情况,时间复杂度O(h)。

         设要删除的节点是x,注意到删除x并不会导致x的子树被删掉,且要保护好二叉搜索树的性质。于是:

             (1)x是一个叶子节点,没有孩子,直接delete x,并置x.parent相应孩子域为NULL。

             (2)x没有左孩子或没有右孩子,直接让唯一存在的那个孩子子树替代x;

             (3)x既有左孩子又有右孩子,找到x的后继y,y一定是最小的大于x的那个数,可以知道y一定没有左子树(如果有的话一定能找到一个小于y的节点),这时可以直接让y替代x,并删掉y节点,由于y节点是没有左孩子的,属于(1)情况,可以使用(1)的方法删除。

/*删除节点  伪代码在树T中删除节点x,假设x存在于树T中。不存在的情况可以加一个简单的搜索判定,此处略去*///将u节点用v子树取代,并维护u,parent与v的父子关系void transplant(BSTree T , BSNode *u , BSNode *v){if(u->parent == NULL)//u是根节点,删掉根节点T.root = v;else if(u == u->parent->LChild)//u是父节点的左孩子u->parent->LChild = v;else//u是父节点的右孩子u->parent->RChild = v;if(v != NULL)v->parent = u->parent;}void TreeDelete(BSTree T , BSNode *x){if(x->LChild == NULL)//左子树为空transplant(T , x , x->RChild);else if(x->RChild == NULL)//右子树为空transplant(T , x , x->LChild);else{//左右子树均不为空BSNode *xSucc = TreeMin(x);//找到x的后继if(xSucc->parent != x){//x后继不是x的孩子transplant(T , xSucc , xSucc->RChild);//此时xSucc一定没有左孩子,将后继的右孩子替代后继节点//---置后继为原来x->RChild的双亲xSucc->RChild = x->RChild; x->RChild->parent = xSucc;}//---将x用后继替换,并置后继为x双亲的孩子和x左孩子的双亲。transplant(T,x,xSucc);//其内已维护x.parent与x后继父子关系,只需关注x孩子与xSucc孩子关系xSucc->LChild = x->LChild;x->LChild->parent = xSucc;}delete x;}

删除节点x一定是取x的后继吗?

       不一定,取x的前驱也可以的。考虑下图:可以将47的后继48替换47,自然也能用37来替换它,只要不破坏二叉搜索树的性质就好。

随机的构建二叉树

       据上所述,二叉搜索树每个基本操作都能在O(h)时间内完成,h为这棵树的高度。但是随着元素的插入和删除,二叉搜索树的高度是变化的,如果n个关键字按照严格递增次序插入,则这棵树一定是高度为n-1的一条链。幸运的是平均性能接近于最好情形而非最坏情形性能。

      一棵有N个不同关键字的随机构建二叉搜索树的期望高度为O(logN)。


     虽然如此,我们还是希望能让这棵二叉排序树能平衡就好了,这样引出了新的问题:平衡二叉树(AVL树),不过这是下一篇文章的事儿了。

C++实现二叉查找树

      1.注意在类的成员函数内如果删除this指针就不能再访问this的数据成员,分离BSTree与BSNode的实现可以将树的根节点所占内存释放。即BSTree中有一个BSNode类成员root作为本树的根节点。这样不需要删掉BSTree对象也能释放树申请的结点内存。

      2.分离了实现后可以做到BSNode析构函数专门删除某个节点(BSTree类的deleteNode函数用到了)用delete Node调用;BSTree中定义一个destory函数专门用来删除以pRoot为根的所有子树并释放堆内存,可以在BSTree析构函数中调用destory函数释放所有BSNode申请的堆内存。

/*二叉搜索树的实现:建立、插入、删除、查询和遍历二叉搜索树是这样的:左子树节点<=根节点<=右子树节点二叉树结构BSTree二叉树节点结构BSNode*/struct BSNode{BSNode():LChild(NULL),RChild(NULL),pParent(NULL){}~BSNode(){//默认析构函数只删除一个节点}BSNode *LChild;BSNode *RChild;BSNode *pParent;int data;};class BSTree{private:int printAtLevel(BSNode *pRoot , int k);//打印第k层的层序遍历结果void printNodeInOrder(BSNode *pRoot);void transplant(BSNode*u , BSNode *v);//用v子树替换u,维护u的父节点与新节点v的关系void destory(BSNode **pRoot);//递归销毁以pRoot为根的子树public:BSTree():root(NULL){}~BSTree();void insertNode(int x);bool deleteNode(BSNode* pNode);//删除x所在节点,成功返回trueBSNode* searchNode(int x);//查询x所在节点,如果查不到返回NULLBSNode* MinKeyNode();//查询最小关键字节点BSNode* MaxKeyNode();//查询最大关键字节点BSNode* TreeSuccessor(BSNode *x);//得到某个节点的后继void printInOrder();//中序遍历,从小到大输出排序结果void printLevelOrder();//整棵树层序遍历,查看建立的二叉树情况private:BSNode *root;//保存本树的根节点};BSTree::~BSTree(){destory(&root);}void BSTree::destory(BSNode **pRoot){if(*pRoot){destory(&((*pRoot)->LChild));destory(&((*pRoot)->RChild));delete *pRoot;*pRoot = NULL;}}void BSTree::insertNode(int x){BSNode *tempNode = new BSNode;tempNode->data = x;if(root == NULL){//空子树root = tempNode;return;}//---寻找插入的位置,x值比根节点值大走右子树,小走左子树,直到走到叶子节点的NULL处插入//注意需要保存最终NULL的父节点,以便插入成为它的孩子节点BSNode *pRoot = root;//根节点BSNode *parent = root->pParent;while (pRoot != NULL){parent = pRoot;if(pRoot->data > tempNode->data)//走左子树pRoot = pRoot->LChild;elsepRoot = pRoot->RChild;}if(parent->data > tempNode->data)parent->LChild = tempNode;elseparent->RChild = tempNode;tempNode->pParent = parent;}/*删除一个节点,分两种情况:(1)设要删除的结点是x,x有两个孩子的情况step1:找到结点x的后继y,可知:y是x右子树中最左结点,且y没有左孩子(否则y左孩子为x的后继)step2:把y的关键字给x,即让结点x成为结点y(移花接木)step3:删除y,因为y没有左孩子,所以按照(2)的方法删除y(2)设要删除的结点是y,y只有左孩子或右孩子的情况     直接置y的左孩子(或右孩子)的节点值(无论是否为NULL)为y的父节点相应孩子。*/void BSTree::transplant(BSNode*u , BSNode *v){ if(u->pParent == NULL){//u是根节点,直接删掉根节点 root = v;delete u; } if(u->pParent->LChild == u)//u是父节点的左孩子 u->pParent->LChild = v; else if(u->pParent->RChild == u)//u是父节点的右孩子 u->pParent->RChild = v; if(v != NULL) v->pParent = u->pParent;}bool BSTree::deleteNode(BSNode* pNode){if(pNode == NULL)//删除空节点return false;if(pNode == root){//删除根节点destory(&root);return true;}//---处理两种情况//没有左孩子或右孩子的情况if(pNode->LChild == NULL)//只有右孩子transplant(pNode , pNode->RChild);else if(pNode->RChild == NULL)//如果只有左孩子transplant(pNode , pNode->LChild);else{//如果既有左孩子又有右孩子BSNode *pNodeSucc = TreeSuccessor(pNode->RChild);//pNode的后继节点if(pNodeSucc->pParent != pNode){//则pNodeSucc一定没有左孩子,让pNodeSucc的右孩子替换pNodeSucctransplant(pNodeSucc , pNodeSucc->RChild);pNodeSucc->RChild = pNode->RChild;//维护pNode后继与pNode右孩子的父子关系pNode->RChild->pParent = pNodeSucc;}//---维护pNode后继和pNode左孩子transplant(pNode,pNodeSucc);pNodeSucc->LChild = pNode->LChild;pNode->LChild->pParent = pNodeSucc;}delete pNode;pNode = NULL;return true;}BSNode* BSTree::searchNode(int x){BSNode *pRoot = root;while(pRoot && x!=pRoot->data){if(x > pRoot->data)pRoot = pRoot->RChild;elsepRoot = pRoot->LChild;}if(pRoot == NULL)//空树return NULL;elsereturn pRoot;}BSNode* BSTree::MinKeyNode(){BSNode *pRoot = root;while(pRoot->LChild != NULL)pRoot = pRoot->LChild;//如果没有左孩子,则返回根节点return pRoot;}BSNode* BSTree::MaxKeyNode(){BSNode* pRoot = root;while(pRoot->RChild != NULL)pRoot = pRoot->RChild;//如果没有右孩子,则返回根节点return pRoot;}BSNode* BSTree::TreeSuccessor(BSNode *x){BSNode* pRoot = root;if(pRoot == NULL)//空树return NULL;//---如果x节点有右孩子,后继就是右孩子if(x->RChild)return x->RChild;//---如果x节点没有右孩子,则由x向上查找,直到找到第一个左子树含有x的节点BSNode *parent = x->pParent;while(parent!=NULL && parent->RChild==x){x = parent;parent = parent->pParent;}return parent;//返回NULL或找到的后继(最后一个元素--根节点没有后继)}void BSTree::printInOrder(){BSNode *pRoot = root;printNodeInOrder(pRoot);}void BSTree::printNodeInOrder(BSNode *pRoot){if(pRoot){printNodeInOrder(pRoot->LChild);cout<<pRoot->data<<" ";printNodeInOrder(pRoot->RChild);}}//思想:打印从根节点开始的第k层相当于打印根节点左右孩子开始的k-1层int BSTree::printAtLevel(BSNode *pRoot , int k){if(pRoot == NULL || k<0)return 0;//未打印节点if(k == 0){cout<<pRoot->data<<" ";return 1;//打印一个节点}//先打印左子树,返回左子树打印的节点数int LeftNodeNum = printAtLevel(pRoot->LChild , k-1);//再打印右子树,返回右子树打印的节点数int RightNodeNum = printAtLevel(pRoot->RChild  , k-1);return LeftNodeNum+RightNodeNum;}void BSTree::printLevelOrder(){  BSNode *pRoot = root;if(pRoot == NULL)cout<<"空树!"<<endl;for(int k=0 ; ;k++){if(0 == printAtLevel(pRoot , k))//整层都未打印节点,返回break;cout<<endl;}}




0 0