1、树查找html
在之间介绍数据结构的文章中咱们介绍过二叉树查找,若是忘记的你们能够查看下这篇文章数据结构-树的那些事(四),这里对二叉树就不作介绍,咱们来讲一下二叉排序树;java
二叉排序树(Binary Search Tree):又被称为二叉查找树或者二叉搜索树,固然首先是二叉树,另外特色以下:node
1.若它的左子树不为空,则左子树的结点小于它根节点的值;git
2.若它的右子树不为空,则右子树的结点大于它根节点的值;github
3.它的左、右子树也分别为二叉排序树;算法
明白特色接下来咱们来讲一下查找的性能,二叉排序树查找性能取决于二叉树的形状,可是二叉树排序的形状是不肯定,最坏的状况查找性能为O(n),最好状况查找性能为O(logn),以下图数据库
接下来咱们谈一下二叉排序树的增长和删除操做,查询就不必说明,就写下代码实现下就好;网络
增长操做:数据结构
1.若当前的二叉树排序树为空,则插入元素的根节点;ide
2.若插入的值小于根节点,则插入到左子树当中;
3.若插入的值大于根节点,则插入到右子树当中;
删除操做:
1.若删除的是子节点,则直接删除该结点;
2.若删除的节点仅有左或者右子树的结点,则让该结点的子树与父亲节点相连,删除该结点便可;
3.若删除的节点左子树和右子树都有子结点,以下面两幅图,这里面强调一下第二幅图,删除47之后用37或者48均可以知足二叉排序树,这里咱们为何选择47而要放弃37,是由于二叉排序树按照中序遍历之后造成的是一个有序序列(由小到大排序),选择37之后不知足这个特征;
上面咱们谈到二叉排序树性能问题,接下来咱们来讲一下平衡二叉树看如何处理二叉树性能问题,全部代码在介绍完成之后提供实现;
平衡二叉树(AVL树):首先平衡二叉树是一颗二叉树排序树,他的特色是每个节点左子树和右子树的高度差至多等于1,这样就能避免照成单枝树,影响查询效率,来个看图识AVL树,下图中图二知足二叉排顺序树的特征,58节点的左子树大于59,图三58的左子树的高度为2,右子树的高度为0,高度差大于1,因此不知足平衡二叉树,接下来咱们谈谈平衡二叉树插入和删除照成失衡之后的操做(平衡二叉树的旋转)。
失衡之后操做:
失衡之后可能形成4种状况:LL(左左)、RR(右右)、LR(左右)、RL(右左),接下来咱们用图说话,看一下这4种状况;
对这4种状况进行统一整理就是:插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,致使"根的左子树的高度"比"根的右子树的高度"大2,致使AVL树失去了平衡。就用第一种说一下当插入D的时候致使,致使5左子树的高度为3,5的右子树为1,差值大于1,这个时候咱们抓住3,使5进行右旋,3的右孩子变成5的左孩子这个时候平衡达成。
这里简单讲一下对应结点的抽象,不一样于二叉树的状况就是这里要增长一个高度的属性,好用来判断左子树和右子树的差值;
B树:首先要明白B树要处理什么问题,咱们上面提到过的AVL树、二叉排序树以及咱们没有提到过的红黑树,这些都是在内存中内部查找树,而B树是前面平衡树算法的扩展,它支持保存在磁盘或者网络上的符号表进行外部查找,这些文件可能比咱们之前考虑的输入要大的多(难以存入内存)。既然内容保存在磁盘中,那么天然会由于树的深度过大而形成磁盘I/O读写过于频繁(磁盘读写速率是有限制的),进而致使查询效率低下。那么下降树的深度天然很重要了。所以,咱们引入了B树,多路查找树。
明白了B树是干什么用的,咱们来个他下个定义,B树是一种平衡的多路查找树,结点最大的孩子数目为B树的阶,一个m阶的B树特征以下:
1.每一个根节点至少有2个孩子节点;
2.每一个非根的分支节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m;
3.每个叶子节点都包含k-1个元素,其中 m/2 <= k <= m;
4.全部的叶子节点都位于同一层次;
5.每一个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划(这个在下面解释下);
接下来咱们以3阶树为例子来讲说上面这些特征是如何体现的,而后在讨论下B树的增长和删除节点状况,
上图是一颗3阶树,首先最大的孩子数目为3,因此是3阶树,咱们来看下12,这是2结点,包含1个根节点和2个孩子结点,知足第2,3条,左子树11小于12,右子树1三、15大于12,接下来咱们看2,6结点,3结点包含二、6两个元素,和3个孩子结点,左子树1小于二、6,右子树8大于二、6,三、5位于左、右子树中间,知足以上条件;
增长操做有3种状况:
1.对于空树,插入一个2结点便可;
2.插入到一个2结点的叶子上。这种状况考虑将其升级为3结点便可,这里进行解释一下,当插入3的时候,经过遍历会发现3比4小,只能插入到4的左子树,4有个子结点,这个时候只能将其升级为3结点,3比1大,插入到1的后继,这个时候造成右图样子。
3.插入3结点的状况相对比较麻烦,3结点是3阶树最大的容量,当向3结点插入的时候就要考虑拆分的状况,咱们来看一下这几种状况。
第一种状况:向2结点满元素的子节点插入时候,左图知足这种状况,当向4的右孩子结点6,7插入5的时候,六、7已是3结点的,不能在增长,这个时候发现4结点是2结点,就须要将其升级为3结点,因而讲六、7拆分,6与4组成3结点,5成为中间孩子,7成为右孩子,如右图。
第二种状况:当向3结点的满元素的子节点插入时候,左图知足这种状况,当向十二、14的左孩子九、10插入11的时候,发现九、10已经知足3结点,不能在增长,可是父亲节点也是3结点,这个时候在向上查找,发现十二、14的父亲结点8,还能够进行插入,这个时候讲8升级为3结点,将8与12合并,最终生成右图样子。
再来看一种比较特殊的状况:当向四、6左孩子结点一、3插入元素的时候,发现都是3结点没法在进行拆分,这个是将一、3结点、四、6结点、八、12结点都进行拆分,造成右图样子;
删除操做:
1.删除位于3结点的叶子结点,删除改元素便可,不会影响到别的结构,以下图:
2.删除元素位于一个2结点上,这里的状况比较复杂,咱们分状况介绍:
第一种状况:该结点的双亲都是2结点,且拥有一个3结点的孩子,以下图,删除结点1,这种状况只须要左旋,6成为父亲节点,4成为6的左孩子,7成为6的右孩子。
第二种状况:该节点的双亲是2结点,右孩子也是2结点,以下图,删除4,左旋致使没有右孩子,不知足B树定义的第一条,因此这个时候须要对树的结构进行调整,首先将7进行左旋,将8进行左旋,9也进行左旋,造成图三所示的样子。
第三种状况:该结点双亲是一个3结点,以下图所示,删除10结点,十二、14不能成为3结点,因而将此结点拆分,并将十二、13合并成左孩子。
第四种状况:满二叉树的状况,删除任何一个叶子都会使整棵树不能知足第一条的定义,以下图所示,当删除8的时候,须要考虑将整棵树的层数减小,这个时候将六、7合并为9的左孩子,这个时候不知足第4条定义,须要将九、14合并为,最终造成右图所示。
3.删除的元素元素若是不是叶子结点,则考虑按照中序遍历后获得的该元素的前驱或者后继来进行替换该结点。这里咱们就不上图来讲明了。
接下来咱们作一些思考,由二叉树查找、二叉树排序树、平衡二叉树、B树咱们能得出一个结论,树的高度越小查找的速度越快,当元素的数目必定的时候,怎么样才能使树的高度下降,固然是扩展树的阶数才能下降树的高度,以前很火的什么4Y个URL中查询某个URL等之类的题,我想看到这里的时候你们会有一些想法了吧,等等之后我会对类问题进行一次总结,此次先不谈了。
再来作一个思考,对于n个关键字的m阶树,最坏须要须要几回查找?
1.由于根至少有两个孩子,所以第2层至少有两个结点。
2.除去跟结点和叶子外,每一个分支的结点至少都有m/2个孩子;
3.第三层结点的个数2*m/2个节点;
4.第四层结点的个数2*(m/2)^2个结点;
5.第K+1层结点的个数2*(m/2)^k-1个结点,这里K+1层就是叶子结点;
6.B树有n个关键字,当你找到叶子节点,就等于查找不成功的结点为n+1,所以n+1>=2*(m/2)^k-1,即k<=log(m/2)((n+1)/2)+1;
7.结论根结点到关键字结点涉及的结点个数不操过k<=log(m/2)((n+1)/2)+1;
总结以下,一颗n个关键字结点的m阶树最大高度为k<=log(m/2)((n+1)/2)+1,B树的每一个非根的分支节点m个孩子,其中 m/2 <= m<= m,随着m的增长,B树的高度降低,查找性能提高;
B+树:
接下来咱们继续探讨一个问题,看下图,当咱们对B树进行中序遍历的时候,假设每个结点都在硬盘不一样的页面上,这个时候必然会通过页2---页1--页3--页4--页1---页4---页1---页5,这样会致使每次那一个元素都要对结点的元素进行遍历,咱们有没有什么办法让元素只被访问一次,带着这个问题咱们来看下B+树。
接下来咱们仍是老套路看图说话,下图是一颗B+树,其有以下特色:出如今分支结点中的元素会被看成分支结点位置的中序后继结点再次列出,另外每一个叶子结点都会保存一个执行后一个叶子结点的指针。
一颗m阶树的B+树和m阶的B树差别以下:
1.n颗子树的结点包含n个关键字;
2.全部的叶子结点包含所有关键字的信息,以及指向这些关键字记录的指针,叶子结点自己按照从小到大的顺序进行排列;
3.全部分支结点均可以当作索引,结点中含有子树的最大(或最小)关键字。
若是上面的B+树看的还不是很直观,那么咱们再来看一颗更直观一点的;这里咱们就不谈什么页分裂问题,这个问题待索引的时候再来探讨,咱们来讲下B+树和B树在查找方面的比较,首先2者与二叉树比较,提高了磁盘I/O效率,可是B树没有解决遍历元素时效率低的问题,如同上面提出的那个问题,这里咱们能够经过B+树来解决,B+树只须要经过叶子节点遍历就能够实现对整棵树的遍历。B+更大的优点在于范围查找,这一点是B树没法比较的,进行范围查找是只须要在叶子结点上遍历便可。B+树的插入和删除都与B树相似,可是插入和删除都是在叶子结点上进行。
上面介绍树的几种不一样的数据结构,主要是为了引出B+树,明白这种结构的好处才能为咱们后面数据库索引打下基础,另外这里没有介绍红黑树,这个等HashMap源码解读的时候再来看,Java代码实现主要提供平衡二叉树的实现,B树的实现仍是给你们提供一个参考https://github.com/int32bit/algorithms/blob/master/B-Tree/src/BTree.java;
2、Java代码实现
public class AVLNode<T extends Comparable<T>> { public T key;//关键字 public int height;//高度 public AVLNode<T> lchild;//左孩子 public AVLNode<T> rchilid;//右孩子 public AVLNode(T key){ this(key,null,null); } public AVLNode(T key,AVLNode<T> lchild,AVLNode<T> rchilid){ this.key=key; this.lchild=lchild; this.rchilid=rchilid; this.height=0; } } public class AVLTree<T extends Comparable<T>> { private AVLNode<T> root; public AVLNode<T> getRoot() { return root; } public AVLTree(){ this.root=null; } public AVLTree(T key){ this.root=new AVLNode<T>(key); } //获取树的高度 private int height(AVLNode<T> node){ if (node!=null){ return node.height; } return 0; } public int height(){ return height(root); } //比较两个值的大小 private int max(int a,int b){ return a>b?a:b; } //递归前序遍历 private void preOrder(AVLNode<T> node){ if (node!=null){ System.out.print(node.key+" "); preOrder(node.lchild); preOrder(node.rchilid); } } public void preOrder(){ preOrder(root); } //中序遍历 private void midOrder(AVLNode<T> node){ if (node!=null){ midOrder(node.lchild); System.out.print(node.key+" "); midOrder(node.rchilid); } } public void midOrder(){ midOrder(root); } //后序遍历 private void postOrder(AVLNode<T> node){ if (node!=null){ postOrder(node.lchild); postOrder(node.rchilid); System.out.print(node.key+" "); } } public void postOrder(){ postOrder(root); } //递归查找key元素 private AVLNode<T> search(AVLNode<T> node,T key){ if (node==null) return node; int compare=key.compareTo(node.key); if (compare<0) return search(node.lchild,key); else if (compare>0) return search(node.rchilid,key); else return node; } public AVLNode<T> search(T key){ return search(root,key); } //LL旋转 private AVLNode<T> llRotation(AVLNode<T> node){ AVLNode<T> newNode; newNode=node.lchild; node.lchild=newNode.rchilid; newNode.rchilid=node; node.height=max(height(node.lchild),height(node.rchilid))+1; newNode.height=max(height(node.lchild),node.height)+1; return newNode; } //RR旋转 private AVLNode<T> rrRotation(AVLNode<T> node){ AVLNode<T> newNode; //根结点右旋 newNode=node.rchilid; node.rchilid=newNode.lchild; newNode.lchild=node; node.height=max(height(node.lchild),height(node.rchilid))+1; newNode.height=max(height(newNode.rchilid),node.height)+1; return newNode; } //LR旋转 private AVLNode<T> lrRotation(AVLNode<T> node){ node.lchild=rrRotation(node.lchild); return llRotation(node); } //RL旋转 private AVLNode<T> rlRotation(AVLNode<T> node){ node.rchilid=llRotation(node.rchilid); return rrRotation(node); } //插入结点 private AVLNode<T> insert(AVLNode<T> node,T key){ if (node==null){ node=new AVLNode<T>(key); }else { int cmp=key.compareTo(node.key);//比较节点的值 if (cmp<0){//小于插入左子树 node.lchild=insert(node.lchild,key); //判断是否失衡 //向左侧插入结点只能照成ll或者lr状况 if (height(node.lchild)-height(node.rchilid)==2){ //插入的结点和当前结点的左子树比较 //大于说明是LR状况否者LL if (key.compareTo(node.lchild.key)>0) node=lrRotation(node); else node=llRotation(node); } }else if (cmp>0){ //同上下面不作介绍 node.rchilid=insert(node.rchilid,key); if (height(node.rchilid)-height(node.lchild)==2){ if (key.compareTo(node.rchilid.key)>0) node=rrRotation(node); else node=rlRotation(node); } }else { System.out.println("添加失败:不容许添加相同的节点!"); } } node.height=max(height(node.lchild),height(node.rchilid))+1; return node; } public void insert(T key){ root=insert(root,key); } //查找最大值 private AVLNode<T> findMax(AVLNode<T> node){ if (node==null) return null; while (node.rchilid!=null){ node=node.rchilid; } return node; } public T finMax(){ AVLNode<T> p=findMax(root); if (p!=null){ return p.key; } return null; } //查找最小值 private AVLNode<T> finMin(AVLNode<T> node){ if (node==null) return null; while (node.lchild!=null){ node=node.lchild; } return node; } public T finMin(){ AVLNode<T> p=finMin(root); if (p!=null){ return p.key; } return null; } //删除结点 private AVLNode<T> remove(AVLNode<T> node,AVLNode<T> del){ if (node==null||del==null) return null; //删除的结点和当前结点比较 int cmp=del.key.compareTo(node.key); if (cmp<0){ //递归向左查找结点 node.lchild=remove(node.lchild,del); //在左子树中删除后该节点失衡,若失衡,则能够确定的是该节点的右子树比左子树高 if (height(node.rchilid)-height(node.lchild)==2){ AVLNode<T> rTree=node.rchilid;//右子树失衡2种状况 右右和右左 if (height(rTree.lchild)>height(rTree.rchilid)) node=rlRotation(node); else node=rrRotation(node); } }else if (cmp>0){ node.rchilid=remove(node.rchilid,del);//同上相反左边失衡 if (height(node.lchild)-height(node.rchilid)==2){ AVLNode<T> lTree=node.lchild; if (height(lTree.rchilid)>height(lTree.lchild)) node=lrRotation(node); else node=llRotation(node); } }else { //找到了要删除的节点,该节点左右子树都不为空 if ((node.lchild!=null)&&(node.rchilid!=null)){ //判断左右孩子的高度 if (height(node.lchild)>height(node.rchilid)){ //若是左子树高度大于右子树 //则找到左子树最大的结点替换当前结点 //这样操做会避免失衡 AVLNode<T> maxNode=findMax(node.lchild); node.key=maxNode.key; node.lchild=remove(node.lchild,maxNode); }else { //同上 AVLNode<T> minNode=finMin(node.rchilid); node.key=minNode.key; node.rchilid=remove(node.rchilid,minNode); } }else {//单一结点则删除 node=(node.lchild!=null)?node.lchild:node.rchilid; } } return node; } public void remove(T key){ AVLNode<T> removeNode=search(key); if (removeNode!=null) root=remove(root,removeNode); } } public class AVLTest { private static int arr[]= {1,2,3,4,5,6,7,8,9,10,12,11,13,14,15 }; public static void main(String[] args) { AVLTree<Integer> tree = new AVLTree<Integer>(); for (int i=0;i<arr.length;i++){ System.out.printf("%d ", arr[i]); tree.insert(arr[i]); } System.out.printf("\n前序遍历: "); tree.preOrder(); System.out.printf("\n中序遍历: "); tree.midOrder(); System.out.printf("\n后序遍历: "); tree.postOrder(); System.out.printf("\n"); System.out.printf("高度: %d\n", tree.height()); System.out.printf("最小值: %d\n", tree.finMin()); System.out.printf("最大值: %d\n", tree.finMax()); tree.remove(7); System.out.printf("\n高度: %d", tree.height()); System.out.printf("\n中序遍历: "); tree.midOrder(); } }
3、数据库索引
已经探讨B+树的好处,接下来咱们来看一下B+插入和删除操做;
插入操做:
B+树插入必须保证插入后的叶子结点记录依然排序,另外还须要考虑插入到B+树的三种状况,接下来咱们来谈一下这3种状况:
1.当Leaf Page和Index Page都未满的时候,直接将记录插入到叶子节点便可(Leaf Page和Index Page分别指的是叶子结点和父亲结点);
当插入28这个值的时候,Leaf Page和Index Page都未满,直接插入到叶子节点
2.当Leaf Page满,Index Page未满时候,首先拆分Leaf Page,将中间结点放入到Inde Page中,而后小于中间节点的放左边,大于或者等于中间结点的放右边;
再次插入70这个值的时候,Leaf Page已经满,可是Index Page还没满,在根节点插入叶子节点中间结点,而后在进行页分裂;
3.当Leaf Page和Index Page都满的时候,首先拆分Leaf Page,小于中间结点的记录放左边,大于或者等于中间结点的记录放右边,接下来拆分Index Page,至关于提高整颗树的高度,小于中间结点的放入到左边,大于或者等于中间结点的放入到右边,最后讲中间节点放入到上一层Index Page;
接下来插入95,这个时候Leaf Page和Index Page都满值,没办法插入,这个时候就须要进行2次拆分,首先拆分叶子结点,最后在拆分根结点。
以上图2和3都没有添加双向链表,主要是为让你们看明白分裂的状况。
删除操做:
B+树使用填充因子来控制树的删除删除变化,50%是填充因子可设置的最小的值,小于这个值这个时候就须要作合并操做,删除操做同时也必须保证删除后的结点依然排序。
1.Leaf Page和Index Page都大于填充因子,若是删除的是叶子节点直接删除就好,若是删除的是Index Page的元素,使用该结点的右结点代替;
如上图删除70这个节点,直接删除就能够了
2.Leaf Page小于填充因子,Index Page大于填充因子,这个时候合并叶子节点和他的兄弟节点,更新Index Page;
接下来删除25,在这个时候知足第一种状况,可是这个值又在Index Page中,删除25之后,还须要将右兄弟替换到Page Index中。
3.Leaf Page和Index Page都小于填充因子,这个时候须要叶子节点作合并,当删除掉Index Page结点之后Index Page也须要作合并。
接下来咱们删除60,这个时候删除60之后Leaf Page不知足填充因子,进行合并,同时删除Index Page的值也须要作合并。
B+树索引的本质就是B+树在数据库中的实现,B+树在数据库中的高度通常为2-4层,这个怎么实现的咱们先不要去管,可是咱们要知道这样作就会致使咱们查询速度很快,高度为2-4层意味着咱们查找一个数据所须要作的IO操做为2-4次,磁盘每秒能够作100次的IO操做,这就意味着咱们能在0.02-0.04秒查找出数据。是否是很快。数据库中分为汇集索引和非汇集索引,这二者的差异在于叶子节点是否存在一整行数据。
汇集索引:
这里思考一个问题,咱们在建表的时候会创建一个主键,这是为何?其实这个主键就是咱们的汇集索引,要是不创建主键,那么咱们存储的数据就会在无序的存在磁盘中,固然查询会慢,可是创建主键之后,数据库中的数据会按照B+树的特色进行构建,整个表变成一个索引,叶子节点存放的是表中的整行数据,这就是咱们所说的汇集索引,非叶子节点的索引存放的是键值以及数据页的偏移量。汇集索引的好处在于,对于主键查找的和排序很是快,由于叶子节点存放的就是咱们的数据,另外每一个叶子节点之间的链接都是双向链表,不须要再一次进行查找。
非汇集索引:
非汇集索引的叶子节点并不包含全部行的数据,叶子节点除了包含键值之外,每一个叶子节点的索引包含一个书签,用来保存相对应的行数据的汇集索引的键。因此当经过非汇集索引查询数据的时候,首先会遍历非汇集索引并经过叶子节点的指针得到指向汇集索引的键,而后经过汇集索引查找与之匹配的叶子节点。每次给表中增长一个非汇集索引,表中的字段就会被复制出来一份,生成索引,因此给表添加索引就会增长表空间,减慢插入时候的速度。
联合索引:
联合索引就是在表上的多个列创建索引,联合索引本质上仍是一棵B+树,不一样的是联合索引的键值数量不是1,而是大于等于2。为何须要联合索引的存在?
1.假设(a,b)列创建索引,对于查询条件而言,当查询的条件为a和b,则可使用,条件单独为a的时候也可使用,可是为b的状况不可使用;
2.对于(a,b)列创建的索引,第二个列是排序的,这样当咱们按照b条件排序的时候就不须要进行排序操做;
明白了索引的原理我想你们之后在优化查询的时候也有清晰的思路了吧,以上是B-Tree索引介绍,另外Hash索引咱们之后再聊。
4、结束语
主要参考《大话数据结构》和《MySQL技术内幕:InnoDB存储引擎》
提早祝福你们新年快乐!今天是个人最后工做日了~能够回家看望父母了!另外有什么不懂能够进群咨询438836709~