阅读目录html
数据结构中有不少树的结构,其中包括二叉树、二叉搜索树、2-3树、红黑树等等。本文中对数据结构中常见的几种树的概念和用途进行了汇总,不求严格精准,但求简单易懂。node
回到顶部算法
二叉树是数据结构中一种重要的数据结构,也是树表家族最为基础的结构。数据库
二叉树的定义:二叉树的每一个结点至多只有二棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第i层至多有2i-1个结点;深度为k的二叉树至多有2k-1个结点;对任何一棵二叉树T,若是其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。数组
二叉树的示例:数据结构
满二叉树和彻底二叉树:app
满二叉树:除最后一层无任何子节点外,每一层上的全部结点都有两个子结点。也能够这样理解,除叶子结点外的全部结点均有两个子结点。节点数达到最大值,全部叶子结点必须在同一层上。函数
满二叉树的性质:性能
1) 一颗树深度为h,最大层数为k,深度与最大层数相同,k=h;优化
2) 叶子数为2h;
3) 第k层的结点数是:2k-1;
4) 总结点数是:2k-1,且总节点数必定是奇数。
彻底二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层全部的结点都连续集中在最左边,这就是彻底二叉树。
注:彻底二叉树是效率很高的数据结构,堆是一种彻底二叉树或者近似彻底二叉树,因此效率极高,像十分经常使用的排序算法、Dijkstra算法、Prim算法等都要用堆才能优化,二叉排序树的效率也要借助平衡性来提升,而平衡性基于彻底二叉树。
二叉树的性质:
1) 在非空二叉树中,第i层的结点总数不超过2i-1, i>=1;
2) 深度为h的二叉树最多有2h-1个结点(h>=1),最少有h个结点;
3) 对于任意一棵二叉树,若是其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
4) 具备n个结点的彻底二叉树的深度为log2(n+1);
5)有N个结点的彻底二叉树各结点若是用顺序方式存储,则结点之间有以下关系:
若I为结点编号则 若是I>1,则其父结点的编号为I/2;
若是2I<=N,则其左儿子(即左子树的根结点)的编号为2I;若2I>N,则无左儿子;
若是2I+1<=N,则其右儿子的结点编号为2I+1;若2I+1>N,则无右儿子。
6)给定N个节点,能构成h(N)种不一样的二叉树,其中h(N)为卡特兰数的第N项,h(n)=C(2*n, n)/(n+1)。
7)设有i个枝点,I为全部枝点的道路长度总和,J为叶的道路长度总和J=I+2i。
二叉查找树定义:又称为是二叉排序树(Binary Sort Tree)或二叉搜索树。二叉排序树或者是一棵空树,或者是具备下列性质的二叉树:
1) 若左子树不空,则左子树上全部结点的值均小于它的根结点的值;
2) 若右子树不空,则右子树上全部结点的值均大于或等于它的根结点的值;
3) 左、右子树也分别为二叉排序树;
4) 没有键值相等的节点。
二叉查找树的性质:对二叉查找树进行中序遍历,便可获得有序的数列。
二叉查找树的时间复杂度:它和二分查找同样,插入和查找的时间复杂度均为O(logn),可是在最坏的状况下仍然会有O(n)的时间复杂度。缘由在于插入和删除元素的时候,树没有保持平衡(好比,咱们查找上图(b)中的“93”,咱们须要进行n次查找操做)。咱们追求的是在最坏的状况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
二叉查找树的高度决定了二叉查找树的查找效率。
二叉查找树的插入过程以下:
1) 若当前的二叉查找树为空,则插入的元素为根节点;
2) 若插入的元素值小于根节点值,则将元素插入到左子树中;
3) 若插入的元素值不小于根节点值,则将元素插入到右子树中。
二叉查找树的删除,分三种状况进行处理:
1) p为叶子节点,直接删除该节点,再修改其父节点的指针(注意分是根节点和不是根节点),如图a;
2) p为单支节点(即只有左子树或右子树)。让p的子树与p的父亲节点相连,删除p便可(注意分是根节点和不是根节点),如图b;
3) p的左子树和右子树均不空。找到p的后继y,由于y必定没有左子树,因此能够删除y,并让y的父亲节点成为y的右子树的父亲节点,并用y的值代替p的值;或者方法二是找到p的前驱x,x必定没有右子树,因此能够删除x,并让x的父亲节点成为y的左子树的父亲节点。如图c。
二叉树相关实现源码:
插入操做:
struct node { int val; pnode lchild; pnode rchild; }; pnode BT = NULL; //递归方法插入节点 pnode insert(pnode root, int x) { pnode p = (pnode)malloc(LEN); p->val = x; p->lchild = NULL; p->rchild = NULL; if(root == NULL){ root = p; } else if(x < root->val){ root->lchild = insert(root->lchild, x); } else{ root->rchild = insert(root->rchild, x); } return root; } //非递归方法插入节点 void insert_BST(pnode q, int x) { pnode p = (pnode)malloc(LEN); p->val = x; p->lchild = NULL; p->rchild = NULL; if(q == NULL){ BT = p; return ; } while(q->lchild != p && q->rchild != p){ if(x < q->val){ if(q->lchild){ q = q->lchild; } else{ q->lchild = p; } } else{ if(q->rchild){ q = q->rchild; } else{ q->rchild = p; } } } return; }
删除操做:
<P> <PRE>bool delete_BST(pnode p, int x) //返回一个标志,表示是否找到被删元素 { bool find = false; pnode q; p = BT; while(p && !find){ //寻找被删元素 if(x == p->val){ //找到被删元素 find = true; } else if(x < p->val){ //沿左子树找 q = p; p = p->lchild; } else{ //沿右子树找 q = p; p = p->rchild; } } if(p == NULL){ //没找到 cout << "没有找到" << x << endl; } if(p->lchild == NULL && p->rchild == NULL){ //p为叶子节点 if(p == BT){ //p为根节点 BT = NULL; } else if(q->lchild == p){ q->lchild = NULL; } else{ q->rchild = NULL; } free(p); //释放节点p } else if(p->lchild == NULL || p->rchild == NULL){ //p为单支子树 if(p == BT){ //p为根节点 if(p->lchild == NULL){ BT = p->rchild; } else{ BT = p->lchild; } } else{ if(q->lchild == p && p->lchild){ //p是q的左子树且p有左子树 q->lchild = p->lchild; //将p的左子树连接到q的左指针上 } else if(q->lchild == p && p->rchild){ q->lchild = p->rchild; } else if(q->rchild == p && p->lchild){ q->rchild = p->lchild; } else{ q->rchild = p->rchild; } } free(p); } else{ //p的左右子树均不为空 pnode t = p; pnode s = p->lchild; //从p的左子节点开始 while(s->rchild){ //找到p的前驱,即p左子树中值最大的节点 t = s; s = s->rchild; } p->val = s->val; //把节点s的值赋给p if(t == p){ p->lchild = s->lchild; } else{ t->rchild = s->lchild; } free(s); } return find; }
查找操做:
pnode search_BST(pnode p, int x) { bool solve = false; while(p && !solve){ if(x == p->val){ solve = true; } else if(x < p->val){ p = p->lchild; } else{ p = p->rchild; } } if(p == NULL){ cout << "没有找到" << x << endl; } return p; }
咱们知道,对于通常的二叉搜索树(Binary Search Tree),其指望高度(即为一棵平衡树时)为log2n,其各操做的时间复杂度O(log2n)同时也由此而决定。可是,在某些极端的状况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操做的时间复杂度将退化成线性的,即O(n)。咱们能够经过随机化创建二叉搜索树来尽可能的避免这种状况,可是在进行了屡次的操做以后,因为在删除时,咱们老是选择将待删除节点的后继代替它自己,这样就会形成老是右边的节点数目减小,以致于树向左偏沉。这同时也会形成树的平衡性受到破坏,提升它的操做的时间复杂度。因而就有了咱们下边介绍的平衡二叉树。
平衡二叉树定义:平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具备如下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,而且左右两个子树都是一棵平衡二叉树。平衡二叉树的经常使用算法有红黑树、AVL树等。在平衡二叉搜索树中,咱们能够看到,其高度通常都良好地维持在O(log2n),大大下降了操做的时间复杂度。
最小二叉平衡树的节点的公式以下:
F(n)=F(n-1)+F(n-2)+1
这个相似于一个递归的数列,能够参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。
有关AVL树的具体实现,能够参考C小加的博客《一步一步写平衡二叉树(AVL)》。
AVL树定义:AVL树是最早发明的自平衡二叉查找树。AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。在AVL中任何节点的两个儿子子树的高度最大差异为1,因此它也被称为高度平衡树,n个结点的AVL树最大深度约1.44log2n。查找、插入和删除在平均和最坏状况下都是O(logn)。增长和删除可能须要经过一次或屡次树旋转来从新平衡这个树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好状况和最坏状况都维持在O(logN)。可是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来讲,时间上稳定了不少。
AVL树的自平衡操做——旋转:
AVL树最关键的也是最难的一步操做就是旋转。旋转主要是为了实现AVL树在实施了插入和删除操做之后,树从新回到平衡的方法。下面咱们重点研究一下AVL树的旋转。
对于一个平衡的节点,因为任意节点最多有两个儿子,所以高度不平衡时,此节点的两颗子树的高度差2.容易看出,这种不平衡出如今下面四种状况:
1) 6节点的左子树3节点高度比右子树7节点大2,左子树3节点的左子树1节点高度大于右子树4节点,这种状况成为左左。
2) 6节点的左子树2节点高度比右子树7节点大2,左子树2节点的左子树1节点高度小于右子树4节点,这种状况成为左右。
3) 2节点的左子树1节点高度比右子树5节点小2,右子树5节点的左子树3节点高度大于右子树6节点,这种状况成为右左。
4) 2节点的左子树1节点高度比右子树4节点小2,右子树4节点的左子树3节点高度小于右子树6节点,这种状况成为右右。
从图2中能够能够看出,1和4两种状况是对称的,这两种状况的旋转算法是一致的,只须要通过一次旋转就能够达到目标,咱们称之为单旋转。2和3两种状况也是对称的,这两种状况的旋转算法也是一致的,须要进行两次旋转,咱们称之为双旋转。
单旋转
单旋转是针对于左左和右右这两种状况的解决方案,这两种状况是对称的,只要解决了左左这种状况,右右就很好办了。图3是左左状况的解决方案,节点k2不知足平衡特性,由于它的左子树k1比右子树Z深2层,并且k1子树中,更深的一层的是k1的左子树X子树,因此属于左左状况。
为使树恢复平衡,咱们把k2变成这棵树的根节点,由于k2大于k1,把k2置于k1的右子树上,而本来在k1右子树的Y大于k1,小于k2,就把Y置于k2的左子树上,这样既知足了二叉查找树的性质,又知足了平衡二叉树的性质。
这样的操做只须要一部分指针改变,结果咱们获得另一颗二叉查找树,它是一棵AVL树,由于X向上一移动了一层,Y还停留在原来的层面上,Z向下移动了一层。整棵树的新高度和以前没有在左子树上插入的高度相同,插入操做使得X高度长高了。所以,因为这颗子树高度没有变化,因此通往根节点的路径就不须要继续旋转了。
双旋转
对于左右和右左这两种状况,单旋转不能使它达到一个平衡状态,要通过两次旋转。双旋转是针对于这两种状况的解决方案,一样的,这样两种状况也是对称的,只要解决了左右这种状况,右左就很好办了。图4是左右状况的解决方案,节点k3不知足平衡特性,由于它的左子树k1比右子树Z深2层,并且k1子树中,更深的一层的是k1的右子树k2子树,因此属于左右状况。
为使树恢复平衡,咱们须要进行两步,第一步,把k1做为根,进行一次右右旋转,旋转以后就变成了左左状况,因此第二步再进行一次左左旋转,最后获得了一棵以k2为根的平衡二叉树。
AVL树实现源码:
//AVL树节点信息 template<class T> class TreeNode { public: TreeNode():lson(NULL),rson(NULL),freq(1),hgt(0){} T data;//值 int hgt;//高度 unsigned int freq;//频率 TreeNode* lson;//指向左儿子的地址 TreeNode* rson;//指向右儿子的地址 }; //AVL树类的属性和方法声明 template<class T> class AVLTree { private: TreeNode<T>* root;//根节点 void insertpri(TreeNode<T>* &node,T x);//插入 TreeNode<T>* findpri(TreeNode<T>* node,T x);//查找 void insubtree(TreeNode<T>* node);//中序遍历 void Deletepri(TreeNode<T>* &node,T x);//删除 int height(TreeNode<T>* node);//求树的高度 void SingRotateLeft(TreeNode<T>* &k2);//左左状况下的旋转 void SingRotateRight(TreeNode<T>* &k2);//右右状况下的旋转 void DoubleRotateLR(TreeNode<T>* &k3);//左右状况下的旋转 void DoubleRotateRL(TreeNode<T>* &k3);//右左状况下的旋转 int Max(int cmpa,int cmpb);//求最大值 public: AVLTree():root(NULL){} void insert(T x);//插入接口 TreeNode<T>* find(T x);//查找接口 void Delete(T x);//删除接口 void traversal();//遍历接口 }; //计算节点的高度 template<class T> int AVLTree<T>::height(TreeNode<T>* node) { if(node!=NULL) return node->hgt; return -1; } //求最大值 template<class T> int AVLTree<T>::Max(int cmpa,int cmpb) { return cmpa>cmpb?cmpa:cmpb; } //左左状况下的旋转 template<class T> void AVLTree<T>::SingRotateLeft(TreeNode<T>* &k2) { TreeNode<T>* k1; k1=k2->lson; k2->lson=k1->rson; k1->rson=k2; k2->hgt=Max(height(k2->lson),height(k2->rson))+1; k1->hgt=Max(height(k1->lson),k2->hgt)+1; } //右右状况下的旋转 template<class T> void AVLTree<T>::SingRotateRight(TreeNode<T>* &k2) { TreeNode<T>* k1; k1=k2->rson; k2->rson=k1->lson; k1->lson=k2; k2->hgt=Max(height(k2->lson),height(k2->rson))+1; k1->hgt=Max(height(k1->rson),k2->hgt)+1; } //左右状况的旋转 template<class T> void AVLTree<T>::DoubleRotateLR(TreeNode<T>* &k3) { SingRotateRight(k3->lson); SingRotateLeft(k3); } //右左状况的旋转 template<class T> void AVLTree<T>::DoubleRotateRL(TreeNode<T>* &k3) { SingRotateLeft(k3->rson); SingRotateRight(k3); } //插入 template<class T> void AVLTree<T>::insertpri(TreeNode<T>* &node,T x) { if(node==NULL)//若是节点为空,就在此节点处加入x信息 { node=new TreeNode<T>(); node->data=x; return; } if(node->data>x)//若是x小于节点的值,就继续在节点的左子树中插入x { insertpri(node->lson,x); if(2==height(node->lson)-height(node->rson)) if(x<node->lson->data) SingRotateLeft(node); else DoubleRotateLR(node); } else if(node->data<x)//若是x大于节点的值,就继续在节点的右子树中插入x { insertpri(node->rson,x); if(2==height(node->rson)-height(node->lson))//若是高度之差为2的话就失去了平衡,须要旋转 if(x>node->rson->data) SingRotateRight(node); else DoubleRotateRL(node); } else ++(node->freq);//若是相等,就把频率加1 node->hgt=Max(height(node->lson),height(node->rson)); } //插入接口 template<class T> void AVLTree<T>::insert(T x) { insertpri(root,x); } //查找 template<class T> TreeNode<T>* AVLTree<T>::findpri(TreeNode<T>* node,T x) { if(node==NULL)//若是节点为空说明没找到,返回NULL { return NULL; } if(node->data>x)//若是x小于节点的值,就继续在节点的左子树中查找x { return findpri(node->lson,x); } else if(node->data<x)//若是x大于节点的值,就继续在节点的左子树中查找x { return findpri(node->rson,x); } else return node;//若是相等,就找到了此节点 } //查找接口 template<class T> TreeNode<T>* AVLTree<T>::find(T x) { return findpri(root,x); } //删除 template<class T> void AVLTree<T>::Deletepri(TreeNode<T>* &node,T x) { if(node==NULL) return ;//没有找到值是x的节点 if(x < node->data) { Deletepri(node->lson,x);//若是x小于节点的值,就继续在节点的左子树中删除x if(2==height(node->rson)-height(node->lson)) if(node->rson->lson!=NULL&&(height(node->rson->lson)>height(node->rson->rson)) ) DoubleRotateRL(node); else SingRotateRight(node); } else if(x > node->data) { Deletepri(node->rson,x);//若是x大于节点的值,就继续在节点的右子树中删除x if(2==height(node->lson)-height(node->rson)) if(node->lson->rson!=NULL&& (height(node->lson->rson)>height(node->lson->lson) )) DoubleRotateLR(node); else SingRotateLeft(node); } else//若是相等,此节点就是要删除的节点 { if(node->lson&&node->rson)//此节点有两个儿子 { TreeNode<T>* temp=node->rson;//temp指向节点的右儿子 while(temp->lson!=NULL) temp=temp->lson;//找到右子树中值最小的节点 //把右子树中最小节点的值赋值给本节点 node->data=temp->data; node->freq=temp->freq; Deletepri(node->rson,temp->data);//删除右子树中最小值的节点 if(2==height(node->lson)-height(node->rson)) { if(node->lson->rson!=NULL&& (height(node->lson->rson)>height(node->lson->lson) )) DoubleRotateLR(node); else SingRotateLeft(node); } } else//此节点有1个或0个儿子 { TreeNode<T>* temp=node; if(node->lson==NULL)//有右儿子或者没有儿子 node=node->rson; else if(node->rson==NULL)//有左儿子 node=node->lson; delete(temp); temp=NULL; } } if(node==NULL) return; node->hgt=Max(height(node->lson),height(node->rson))+1; return; } //删除接口 template<class T> void AVLTree<T>::Delete(T x) { Deletepri(root,x); } //中序遍历函数 template<class T> void AVLTree<T>::insubtree(TreeNode<T>* node) { if(node==NULL) return; insubtree(node->lson);//先遍历左子树 cout<<node->data<<" ";//输出根节点 insubtree(node->rson);//再遍历右子树 } //中序遍历接口 template<class T> void AVLTree<T>::traversal() { insubtree(root); }
红黑树的定义:红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中得到的。它是复杂的,但它的操做有着良好的最坏状况运行时间,而且在实践中是高效的: 它能够在O(logn)时间内作查找,插入和删除,这里的n是树中元素的数目。
红黑树和AVL树同样都对插入时间、删除时间和查找时间提供了最好可能的最坏状况担保。这不仅是使它们在时间敏感的应用如实时应用(real time application)中有价值,并且使它们有在提供最坏状况担保的其余数据结构中做为建造板块的价值;例如,在计算几何中使用的不少数据结构均可以基于红黑树。此外,红黑树仍是2-3-4树的一种等同,它们的思想是同样的,只不过红黑树是2-3-4树用二叉树的形式表示的。
红黑树的性质:
红黑树是每一个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制的通常要求之外,对于任何有效的红黑树咱们增长了以下的额外要求:
性质1. 节点是红色或黑色。
性质2. 根是黑色。
性质3. 全部叶子都是黑色(叶子是NIL节点)。
性质4. 每一个红色节点必须有两个黑色的子节点。(从每一个叶子到根的全部路径上不能有两个连续的红色节点。)
性质5. 从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点。
下面是一个具体的红黑树的图例:
这些约束确保了红黑树的关键特性: 从根到叶子的最长的可能路径很少于最短的可能路径的两倍长。结果是这个树大体上是平衡的。由于操做好比插入、删除和查找某个值的最坏状况时间都要求与树的高度成比例,这个在高度上的理论上限容许红黑树在最坏状况下都是高效的,而不一样于普通的二叉查找树。
要知道为何这些性质确保了这个结果,注意到性质4致使了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。由于根据性质5全部最长的路径都有相同数目的黑色节点,这就代表了没有路径能多于任何其余路径的两倍长。
如下内容整理自wiki百科之红黑树。
红黑树的自平衡操做:
由于每个红黑树也是一个特化的二叉查找树,所以红黑树上的只读操做与普通二叉查找树上的只读操做相同。然而,在红黑树上进行插入操做和删除操做会致使再也不符合红黑树的性质。恢复红黑树的性质须要少许(O(logn))的颜色变动(实际是很是快速的)和不超过三次树旋转(对于插入操做是两次)。虽然插入和删除很复杂,但操做时间仍能够保持为O(logn) 次。
咱们首先以二叉查找树的方法增长节点并标记它为红色。若是设为黑色,就会致使根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的(违背性质5)。可是设为红色节点后,可能会致使出现两个连续红色节点的冲突,那么能够经过颜色调换(color flips)和树旋转来调整。下面要进行什么操做取决于其余临近节点的颜色。同人类的家族树中同样,咱们将使用术语叔父节点来指一个节点的父节点的兄弟节点。注意:
插入操做:
假设,将要插入的节点标为N,N的父节点标为P,N的祖父节点标为G,N的叔父节点标为U。在图中展现的任何颜色要么是由它所处情形这些所做的假定,要么是假定所暗含的。
情形1: 该树为空树,直接插入根结点的位置,违反性质1,把节点颜色有红改成黑便可。
情形2: 插入节点N的父节点P为黑色,不违反任何性质,无需作任何修改。在这种情形下,树还是有效的。性质5也未受到威胁,尽管新节点N有两个黑色叶子子节点;但因为新节点N是红色,经过它的每一个子节点的路径就都有同经过它所取代的黑色的叶子的路径一样数目的黑色节点,因此依然知足这个性质。
注: 情形1很简单,情形2中P为黑色,一切安然无事,但P为红就不同了,下边是P为红的各类状况,也是真正难懂的地方。
情形3: 若是父节点P和叔父节点U两者都是红色,(此时新插入节点N作为P的左子节点或右子节点都属于情形3,这里右图仅显示N作为P左子的情形)则咱们能够将它们两个重绘为黑色并重绘祖父节点G为红色(用来保持性质4)。如今咱们的新节点N有了一个黑色的父节点P。由于经过父节点P或叔父节点U的任何路径都一定经过祖父节点G,在这些路径上的黑节点数目没有改变。可是,红色的祖父节点G的父节点也有多是红色的,这就违反了性质4。为了解决这个问题,咱们在祖父节点G上递归地进行上述情形的整个过程(把G当成是新加入的节点进行各类情形的检查)。好比,G为根节点,那咱们就直接将G变为黑色(情形1);若是G不是根节点,而它的父节点为黑色,那符合全部的性质,直接插入便可(情形2);若是G不是根节点,而它的父节点为红色,则递归上述过程(情形3)。
情形4: 父节点P是红色而叔父节点U是黑色或缺乏,新节点N是其父节点的左子节点,而父节点P又是其父节点G的左子节点。在这种情形下,咱们进行针对祖父节点G的一次右旋转; 在旋转产生的树中,之前的父节点P如今是新节点N和之前的祖父节点G的父节点。咱们知道之前的祖父节点G是黑色,不然父节点P就不多是红色(若是P和G都是红色就违反了性质4,因此G必须是黑色)。咱们切换之前的父节点P和祖父节点G的颜色,结果的树知足性质4。性质5也仍然保持知足,由于经过这三个节点中任何一个的全部路径之前都经过祖父节点G,如今它们都经过之前的父节点P。在各自的情形下,这都是三个节点中惟一的黑色节点。
情形5: 父节点P是红色而叔父节点U是黑色或缺乏,而且新节点N是其父节点P的右子节点而父节点P又是其父节点的左子节点。在这种情形下,咱们进行一次左旋转调换新节点和其父节点的角色; 接着,咱们按情形4处理之前的父节点P以解决仍然失效的性质4。注意这个改变会致使某些路径经过它们之前不经过的新节点N(好比图中1号叶子节点)或不经过节点P(好比图中3号叶子节点),但因为这两个节点都是红色的,因此性质5仍有效。
注: 插入其实是原地算法,由于上述全部调用都使用了尾部递归。
删除操做:
若是须要删除的节点有两个儿子,那么问题能够被转化成删除另外一个只有一个儿子的节点的问题。对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,咱们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素,并把它的值转移到要删除的节点中。咱们接着删除咱们从中复制出值的那个节点,它一定有少于两个非叶子的儿子。由于只是复制了一个值,不违反任何性质,这就把问题简化为如何删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点仍是咱们从中复制出值的那个节点。
咱们只须要讨论删除只有一个儿子的节点(若是它两个儿子都为空,即均为叶子,咱们任意将其中一个看做它的儿子)。若是咱们删除一个红色节点(此时该节点的儿子将都为叶子节点),它的父亲和儿子必定是黑色的。因此咱们能够简单的用它的黑色儿子替换它,并不会破坏性质3和性质4。经过被删除节点的全部路径只是少了一个红色节点,这样能够继续保证性质5。另外一种简单状况是在被删除节点是黑色而它的儿子是红色的时候。若是只是去除这个黑色节点,用它的红色儿子顶替上来的话,会破坏性质5,可是若是咱们重绘它的儿子为黑色,则曾经经过它的全部路径将经过它的黑色儿子,这样能够继续保持性质5。
须要进一步讨论的是在要删除的节点和它的儿子两者都是黑色的时候,这是一种复杂的状况。咱们首先把要删除的节点替换为它的儿子。出于方便,称呼这个儿子为N(在新的位置上),称呼它的兄弟(它父亲的另外一个儿子)为S。在下面的示意图中,咱们仍是使用P称呼N的父亲,SL称呼S的左儿子,SR称呼S的右儿子。
若是N和它初始的父亲是黑色,则删除它的父亲致使经过N的路径都比不经过它的路径少了一个黑色节点。由于这违反了性质5,树须要被从新平衡。有几种情形须要考虑:
情形1: N是新的根。在这种情形下,咱们就作完了。咱们从全部路径去除了一个黑色节点,而新根是黑色的,因此性质都保持着。
注意: 在情形二、5和6下,咱们假定N是它父亲的左儿子。若是它是右儿子,则在这些情形下的左和右应当对调。
情形2: S是红色。在这种情形下咱们在N的父亲上作左旋转,把红色兄弟转换成N的祖父,咱们接着对调N的父亲和祖父的颜色。完成这两个操做后,尽管全部路径上黑色节点的数目没有改变,但如今N有了一个黑色的兄弟和一个红色的父亲(它的新兄弟是黑色由于它是红色S的一个儿子),因此咱们能够接下去按情形4、情形5或情形6来处理。
情形3: N的父亲、S和S的儿子都是黑色的。在这种情形下,咱们简单的重绘S为红色。结果是经过S的全部路径,它们就是之前不经过N的那些路径,都少了一个黑色节点。由于删除N的初始的父亲使经过N的全部路径少了一个黑色节点,这使事情都平衡了起来。可是,经过P的全部路径如今比不经过P的路径少了一个黑色节点,因此仍然违反性质5。要修正这个问题,咱们要从情形1开始,在P上作从新平衡处理。
情形4: S和S的儿子都是黑色,可是N的父亲是红色。在这种情形下,咱们简单的交换N的兄弟和父亲的颜色。这不影响不经过N的路径的黑色节点的数目,可是它在经过N的路径上对黑色节点数目增长了一,添补了在这些路径上删除的黑色节点。
情形5: S是黑色,S的左儿子是红色,S的右儿子是黑色,而N是它父亲的左儿子。在这种情形下咱们在S上作右旋转,这样S的左儿子成为S的父亲和N的新兄弟。咱们接着交换S和它的新父亲的颜色。全部路径仍有一样数目的黑色节点,可是如今N有了一个黑色兄弟,他的右儿子是红色的,因此咱们进入了情形6。N和它的父亲都不受这个变换的影响。
情形6: S是黑色,S的右儿子是红色,而N是它父亲的左儿子。在这种情形下咱们在N的父亲上作左旋转,这样S成为N的父亲(P)和S的右儿子的父亲。咱们接着交换N的父亲和S的颜色,并使S的右儿子为黑色。子树在它的根上的还是一样的颜色,因此性质3没有被违反。可是,N如今增长了一个黑色祖先: 要么N的父亲变成黑色,要么它是黑色而S被增长为一个黑色祖父。因此,经过N的路径都增长了一个黑色节点。
此时,若是一个路径不经过N,则有两种可能性:
在任何状况下,在这些路径上的黑色节点数目都没有改变。因此咱们恢复了性质4。在示意图中的白色节点能够是红色或黑色,可是在变换先后都必须指定相同的颜色。
红黑树实现源码:
<PRE>#define BLACK 1 #define RED 0 using namespace std; class bst { private: struct Node { int value; bool color; Node *leftTree, *rightTree, *parent; Node() { color = RED; leftTree = NULL; rightTree = NULL; parent = NULL; value = 0; } Node* grandparent() { if (parent == NULL) { return NULL; } return parent->parent; } Node* uncle() { if (grandparent() == NULL) { return NULL; } if (parent == grandparent()->rightTree) return grandparent()->leftTree; else return grandparent()->rightTree; } Node* sibling() { if (parent->leftTree == this) return parent->rightTree; else return parent->leftTree; } }; void rotate_right(Node *p) { Node *gp = p->grandparent(); Node *fa = p->parent; Node *y = p->rightTree; fa->leftTree = y; if (y != NIL) y->parent = fa; p->rightTree = fa; fa->parent = p; if (root == fa) root = p; p->parent = gp; if (gp != NULL) { if (gp->leftTree == fa) gp->leftTree = p; else gp->rightTree = p; } } void rotate_left(Node *p) { if (p->parent == NULL) { root = p; return; } Node *gp = p->grandparent(); Node *fa = p->parent; Node *y = p->leftTree; fa->rightTree = y; if (y != NIL) y->parent = fa; p->leftTree = fa; fa->parent = p; if (root == fa) root = p; p->parent = gp; if (gp != NULL) { if (gp->leftTree == fa) gp->leftTree = p; else gp->rightTree = p; } } void inorder(Node *p) { if (p == NIL) return; if (p->leftTree) inorder(p->leftTree); cout << p->value << " "; if (p->rightTree) inorder(p->rightTree); } string outputColor(bool color) { return color ? "BLACK" : "RED"; } Node* getSmallestChild(Node *p) { if (p->leftTree == NIL) return p; return getSmallestChild(p->leftTree); } bool delete_child(Node *p, int data) { if (p->value > data) { if (p->leftTree == NIL) { return false; } return delete_child(p->leftTree, data); } else if (p->value < data) { if (p->rightTree == NIL) { return false; } return delete_child(p->rightTree, data); } else if (p->value == data) { if (p->rightTree == NIL) { delete_one_child(p); return true; } Node *smallest = getSmallestChild(p->rightTree); swap(p->value, smallest->value); delete_one_child(smallest); return true; } } void delete_one_child(Node *p) { Node *child = p->leftTree == NIL ? p->rightTree : p->leftTree; if (p->parent == NULL && p->leftTree == NIL && p->rightTree == NIL) { p = NULL; root = p; return; } if (p->parent == NULL) { delete p; child->parent = NULL; root = child; root->color = BLACK; return; } if (p->parent->leftTree == p) { p->parent->leftTree = child; } else { p->parent->rightTree = child; } child->parent = p->parent; if (p->color == BLACK) { if (child->color == RED) { child->color = BLACK; } else delete_case(child); } delete p; } void delete_case(Node *p) { if (p->parent == NULL) { p->color = BLACK; return; } if (p->sibling()->color == RED) { p->parent->color = RED; p->sibling()->color = BLACK; if (p == p->parent->leftTree) rotate_left(p->sibling()); else rotate_right(p->sibling()); } if (p->parent->color == BLACK && p->sibling()->color == BLACK && p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK) { p->sibling()->color = RED; delete_case(p->parent); } else if (p->parent->color == RED && p->sibling()->color == BLACK && p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK) { p->sibling()->color = RED; p->parent->color = BLACK; } else { if (p->sibling()->color == BLACK) { if (p == p->parent->leftTree && p->sibling()->leftTree->color == RED && p->sibling()->rightTree->color == BLACK) { p->sibling()->color = RED; p->sibling()->leftTree->color = BLACK; rotate_right(p->sibling()->leftTree); } else if (p == p->parent->rightTree && p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == RED) { p->sibling()->color = RED; p->sibling()->rightTree->color = BLACK; rotate_left(p->sibling()->rightTree); } } p->sibling()->color = p->parent->color; p->parent->color = BLACK; if (p == p->parent->leftTree) { p->sibling()->rightTree->color = BLACK; rotate_left(p->sibling()); } else { p->sibling()->leftTree->color = BLACK; rotate_right(p->sibling()); } } } void insert(Node *p, int data) { if (p->value >= data) { if (p->leftTree != NIL) insert(p->leftTree, data); else { Node *tmp = new Node(); tmp->value = data; tmp->leftTree = tmp->rightTree = NIL; tmp->parent = p; p->leftTree = tmp; insert_case(tmp); } } else { if (p->rightTree != NIL) insert(p->rightTree, data); else { Node *tmp = new Node(); tmp->value = data; tmp->leftTree = tmp->rightTree = NIL; tmp->parent = p; p->rightTree = tmp; insert_case(tmp); } } } void insert_case(Node *p) { if (p->parent == NULL) { root = p; p->color = BLACK; return; } if (p->parent->color == RED) { if (p->uncle()->color == RED) { p->parent->color = p->uncle()->color = BLACK; p->grandparent()->color = RED; insert_case(p->grandparent()); } else { if (p->parent->rightTree == p && p->grandparent()->leftTree == p->parent) { rotate_left(p); rotate_right(p); p->color = BLACK; p->leftTree->color = p->rightTree->color = RED; } else if (p->parent->leftTree == p && p->grandparent()->rightTree == p->parent) { rotate_right(p); rotate_left(p); p->color = BLACK; p->leftTree->color = p->rightTree->color = RED; } else if (p->parent->leftTree == p && p->grandparent()->leftTree == p->parent) { p->parent->color = BLACK; p->grandparent()->color = RED; rotate_right(p->parent); } else if (p->parent->rightTree == p && p->grandparent()->rightTree == p->parent) { p->parent->color = BLACK; p->grandparent()->color = RED; rotate_left(p->parent); } } } } void DeleteTree(Node *p) { if (!p || p == NIL) { return; } DeleteTree(p->leftTree); DeleteTree(p->rightTree); delete p; } public: bst() { NIL = new Node(); NIL->color = BLACK; root = NULL; } ~bst() { if (root) DeleteTree(root); delete NIL; } void inorder() { if (root == NULL) return; inorder(root); cout << endl; } void insert(int x) { if (root == NULL) { root = new Node(); root->color = BLACK; root->leftTree = root->rightTree = NIL; root->value = x; } else { insert(root, x); } } bool delete_value(int data) { return delete_child(root, data); } private: Node *root, *NIL; };
B树也是一种用于查找的平衡树,可是它不是二叉树。
B树的定义:B树(B-tree)是一种树状数据结构,可以用来存储排序后的数据。这种数据结构可以让查找数据、循序存取、插入数据及删除的动做,都在对数时间内完成。B树,归纳来讲是一个通常化的二叉查找树,能够拥有多于2个子节点。与自平衡二叉查找树不一样,B-树为系统最优化大块数据的读和写操做。B-tree算法减小定位记录时所经历的中间过程,从而加快存取速度。这种数据结构常被应用在数据库和文件系统的实做上。
在B树中查找给定关键字的方法是,首先把根结点取来,在根结点所包含的关键字K1,…,Kn查找给定的关键字(可用顺序查找或二分查找法),若找到等于给定值的关键字,则查找成功;不然,必定能够肯定要查找的关键字在Ki与Ki+1之间,Pi为指向子树根节点的指针,此时取指针Pi所指的结点继续查找,直至找到,或指针Pi为空时查找失败。
B树做为一种多路搜索树(并非二叉的):
1) 定义任意非叶子结点最多只有M个儿子;且M>2;
2) 根结点的儿子数为[2, M];
3) 除根结点之外的非叶子结点的儿子数为[M/2, M];
4) 每一个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
5) 非叶子结点的关键字个数=指向儿子的指针个数-1;
6) 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7) 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8) 全部叶子结点位于同一层;
以下图为一个M=3的B树示例:
B树建立的示意图:
B+树是B树的变体,也是一种多路搜索树:
1) 其定义基本与B-树相同,除了:
2) 非叶子结点的子树指针与关键字个数相同;
3) 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
4) 为全部叶子结点增长一个链指针;
5) 全部关键字都在叶子结点出现;
下图为M=3的B+树的示意图:
B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树能够在非叶子结点命中),其性能也等价于在关键字全集作一次二分查找;
B+的性质:
1.全部关键字都出如今叶子结点的链表中(稠密索引),且链表中的关键字刚好是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点至关因而叶子结点的索引(稀疏索引),叶子结点至关因而存储(关键字)数据的数据层;
4.更适合文件索引系统。
下面为一个B+树建立的示意图:
B*树是B+树的变体,在B+树的非根和非叶子结点再增长指向兄弟的指针,将结点的最低利用率从1/2提升到2/3。
B*树以下图所示:
B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增长新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,因此它不须要指向兄弟的指针;
B*树的分裂:当一个结点满时,若是它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(由于兄弟结点的关键字范围改变了);若是兄弟也满了,则在原结点与兄弟结点之间增长新结点,并各复制1/3的数据到新结点,最后在父结点增长新结点的指针;
因此,B*树分配新结点的几率比B+树要低,空间使用率更高。
Tire树称为字典树,又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不只限于字符串),因此常常被搜索引擎系统用于文本词频统计。它的优势是:利用字符串的公共前缀来减小查询时间,最大限度地减小无谓的字符串比较,查询效率比哈希树高。
Tire树的三个基本性质:
1) 根节点不包含字符,除根节点外每个节点都只包含一个字符;
2) 从根节点到某一节点,路径上通过的字符链接起来,为该节点对应的字符串;
3) 每一个节点的全部子节点包含的字符都不相同。
Tire树的应用:
1) 串的快速检索
给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最先出现的顺序写出全部不在熟词表中的生词。
在这道题中,咱们能够用数组枚举,用哈希,用字典树,先把熟词建一棵树,而后读入文章进行比较,这种方法效率是比较高的。
2) “串”排序
给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出。用字典树进行排序,采用数组的方式建立字典树,这棵树的每一个结点的全部儿子很显然地按照其字母大小排序。对这棵树进行先序遍历便可。
3) 最长公共前缀
对全部串创建字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,因而,问题就转化为求公共祖先的问题。