1、概述
二叉查找树是一种含有附加属性的二叉树,即其左孩子小于父结点,父结点小于或等于右孩子
(二叉查找树的定义是二叉树定义的扩展)
2、 用链表实现二叉查找树html
addElement操做:
addElement方法根据给定元素的值,在树中的恰当位置添加该元素,若是这个元素不是 comparable,则addElement方法会抛出NoComparableElemementException;
若是树为空,则这个新元素就将成为根结点;
若是树非空,这个新元素会与树根元素进行比较:
若是它小于根结点中存储的那个元素且根的左孩子为null,则这个新元素就将成为根的左孩子。
若是这个新元素小于根结点中存储的那个元素且根的左孩子不是null,则会遍历根的左孩子,并再次进行比较操做;
若是这个新元素大于或等于树根存储的那个元素且根的右孩子为null,则这个新元素会成为根的右孩子,
若是这个新元素大于或等于树根处存储的那个元素且根的右孩子不是null,则会遍历根的右孩子,并再次进行比较操做
如图:
java
private BinaryTreeNode<T> replacement(BinaryTreeNode<T> node) { BinaryTreeNode<T> result = null; if ((node.left == null) && (node.right == null)) { result = null; } else if ((node.left != null) && (node.right == null)) { result = node.left; } else if ((node.left == null) && (node.right != null)) { result = node.right; } else { BinaryTreeNode<T> current = node.right;// 初始化右侧第一个结点 BinaryTreeNode<T> parent = node; // 获取右边子树的最左边的结点 while (current.left != null) { parent = current; current = current.left; } current.left = node.left; // 若是当前待查询的结点 if (node.right != current) { parent.left = current.right;// 总体的树结构移动就能够了 current.right = node.right; } result = current; } return result; }
removeAllOccurrences操做
removeAllOccurrences方法负责从二叉查找树中删除指定元素的全部存在;
或者,当在树中找不到指定元素时,则抛出 ElementNotFoundException异常;
若是指定的元素不是Comparable,则removeAllOccurrences方法也会抛出 ClassCastException异常,该方法会调用一次 removeElement方法,以此确保当树中根本不存在指定元素时会抛出异常,
只要树中还含有目标元素,就会再次调用 removeElement方法,注意, removeAllOccurrences方法使用了 LinkedBinaryTree类的 contans方法,还要注意,在 LinkedBinaryTree类中的find方法已经被重载了,以便利用二又查找树的有序属性node
3、用有序列表实现二叉查找树git
4、平衡二叉查找树算法
蜕化树:看起来更像一个链表,但实际上它的效率比链表的还低,由于每一个结点还附带额外的开销
如图(b)所示:
编程
右旋
要平衡化该树,咱们须要:
使树根的左孩子元素成为新的根元素
使原根元素成为这个新树根的右孩子元素。
使树根的左孩子的右孩子,成为原树根的新的左孩子
数据结构
左旋
要平衡化该树,咱们须要:
使树根的右孩子元素成为新的根元素
使原根元素成为这个新树根的左孩子元素。
使原树根右孩子结点的左孩子,成为原树根的新的右孩子。
性能
右左旋
并不是全部的不平衡问题均可以只进行某一种旋转就能解决
对于由树根右孩子的左子树中较长路径而致使的不平衡,
咱们必须先让树根右孩子的左孩子,绕着树根的右孩子进行一次右旋,而后再让所得的树根右孩子绕着树根进行一次左旋
学习
左右旋
对于由树根左孩子的右子树中较长路径而致使的不平衡,
咱们必须先让树根左孩子的右孩子绕着树根的左孩子进行一次左旋,而后再让所得的树根左孩子绕着树根进行一次右旋
字体
5、实现二叉查找树:AVL树
6、实现二叉查找树:红黑树
(1). 红黑树:一种平衡二叉查找树,其中的每一个结点存储一种颜色(红或黑,用布尔值实现,false等价于红色)
控制结点颜色的规则:
(2). 在某种程度上,红黑树的平衡限制没有AVL树那么严格,可是,他们的序仍然是logn
(3). 红黑树中的元素插入
红黑树的插入操做相似于前面的addElement方法,可是这里老是把插入的新元素颜色设置为红色,
插入新元素以后,必要时将从新平衡化该树,根据须要改变元素的颜色以便维持红黑树的属性
形式1:current == root (current是当前正在处理的结点) 咱们老是设置根结点颜色为黑色,而全部路径都包括树根,所以不能违背各条路径都拥有一样数目黑色元素这一规则 形式2:current.parent.color == black(即当前结点的父结点颜色为黑色) current所指向的结点老是一个红色结点,这意味着,若是当前结点的父结点是黑色,则可知足全部规则,由于红色结点并不影响路径中的黑色结点数目; 另外因为是从插入点处上溯处理,所以早已平衡了当前结点下面的子树
可能一:
若是是左孩子,利用 current.parent.parent.left.color 获得颜色信息(null元素的颜色为黑色),且存在两种状况:
父结点的兄弟为红色或黑色:这两种状况下,咱们阐述的处理步骤都将发生在一个循环内部(该循环的终止条件如前所述)
若是父结点的兄弟为红色,这时的处理步骤以下:
设置current的父亲的颜色为 black 设置父结点的兄弟的颜色为black 设置 current的祖父的颜色为red 设置 current指向current的祖父
若是父结点的兄弟为黑色,首先要查看current是左孩子仍是右孩子:
若是current是右孩子,则必须设置current等于其父亲,在继续以前还要再向左旋转current.right;
后面的步骤,与开始时current为左孩子同样
若是current是左孩子: 设置current的父亲的颜色为black 设置current的祖父的颜色为red 若是current的祖父不等于null,则让current的父亲绕着current的祖父向右旋转
可能二:
若是是右孩子,存在两种状况:
父结点的兄弟为红色或黑色:这两种状况下,咱们阐述的处理步骤都将发生在一个循环内部(该循环的终止条件如前所述)
若是父结点的兄弟为红色,这时的处理步骤以下(同当前结点的父结点的兄弟颜色为红时):
设置current的父亲的颜色为 black 设置父结点的兄弟的颜色为black 设置 current的祖父的颜色为red 设置 current指向current的祖父
若是父结点的兄弟为黑色,首先要查看current是左孩子仍是右孩子(与当前结点的父结点的兄弟颜色为黑时,操做对称):
若是current是左孩子,则必须设置current等于其父亲,在继续以前还要再向右旋转current.left;
后面的步骤,与开始时current为右孩子同样
若是current是右孩子: 设置current的父亲的颜色为black 设置current的祖父的颜色为red 若是current的祖父不等于null,则让current的父亲绕着current的祖父向左旋转
(4). 红黑树中的元素删除
与元素插入的那些状况同样,删除的两种状况也是对称的——取决于 current是左孩子仍是右孩子。
当 current为右孩子时:
(在插入时,咱们最关注的是当前结点的父亲的兄弟的颜色)
而对删除而言,焦点要放在当前结点的兄弟的颜色上(用 current.parent. left.color来指代这种颜色):
还要观察该兄弟的孩子的颜色,要注意的重要一点是:颜色的默认值是black;
这样,任什么时候刻若是试图取得mull对象的颜色,结果都将是black
其余的状况很容易推导出来,只要把上述状况中的“左”换成“右”、“右”换成“左”便可
若是兄弟的颜色是red,则在作其余事以前必须完成以下处理步骤:
* 设置兄弟的颜色为black * 设置current的父亲的颜色为red * 让兄弟绕着 current的父亲向右旋转 * 设置兄弟等于 current的父亲的左孩子
下面再继续处理过程:无论这个初始兄弟是red仍是 black,这里的处理会根据兄弟的孩子的颜色分红两种状况:
若是兄弟的两个孩子都是black(或null),则须要
* 设置兄弟的颜色为red * 设置 current等于 current的父亲
若是兄弟的两个孩子不全为black,则将查看兄弟的左孩子是不是black,若是是,则在继续以前必须完成以下步骤:
* 设置兄弟的右孩子的颜色为 black * 设置兄弟的颜色为red * 让兄弟的右孩子绕着兄弟自己向右旋转 * 设置兄弟等于cumt的父亲的左孩子
最后是兄弟的两个孩子都不为 black这一状况,这时必须:
* 设置兄弟的颜色为 current的父亲的颜色。 * 设置 current的父亲的颜色为black * 设置兄弟的左孩子的颜色为black * 让兄弟绕着 current的父亲向右旋转 * 设置 current等于树根
该循环终止以后,咱们要酬除该结点,并设置其父亲的孩子引用为mull
如今所明白的是红黑树是为了让二叉树路径不会过长而致使查找等操做的效率太低,经过红黑结点的限制,使得红黑树的最大长度约为2logn
可是对于红黑树的设计思想来源不是很明白,为何要这样设计,以及这样设计真的能达到效果,真的是对的嘛?
像AVL树那样,每次插入删除都要进行旋转使得树平衡,这不就够了吗?为何还要再弄一个红黑树?
百度了相关的资料,而后总结汇总在这里:
3-节点则是扩充版,包含2个元素和三条连接:两个元素A、B,左边的连接指向小于A的节点,中间的连接指向介于A、B值之间的节点,右边的连接指向大于B的节点。
在这两种节点的配合下,2-3树能够保证在插入值过程当中,任意叶子节点到根节点的距离都是相同的。彻底实现了矮胖矮胖的目标。怎么配合的呢,下面来看2-3树的构造过程。
所谓构造,就是从零开始一个节点一个节点的插入。
在二叉查找树中,插入过程从根节点开始比较,小于节点值往右继续与左子节点比,大于则继续与右子节点比,直到某节点左或右子节点为空,把值插入进去。这样没法避免偏向问题。在2-3树中,插入的过程是这样的。
若是将值插入一个2-节点,则将2-节点扩充为一个3-节点。
若是将值插入一个3-节点,分为如下几种状况。
(1).3-节点没有父节点,即整棵树就只有它一个三节点。此时,将3-节点扩充为一个4-节点,即包含三个元素的节点,而后将其分解,变成一棵二叉树。
此时二叉树依然保持平衡。
(2).3-节点有一个2-节点的父节点,此时的操做是,3-节点扩充为4-节点,而后分解4-节点,而后将分解后的新树的父节点融入到2-节点的父节点中去。
(3).3-节点有一个3-节点的父节点,此时操做是:3-节点扩充为4-节点,而后分解4-节点,新树父节点向上融合,上面的3-节点继续扩充,融合,分解,新树继续向上融合,直到父节点为2-节点为止,若是向上到根节点都是3-节点,将根节点扩充为4-节点,而后分解为新树,至此,整个树增长一层,仍然保持平衡。
第三种状况稍微复杂点,为了便于直观理解,如今咱们从零开始构建2-3树,囊括上面全部的状况,看完因此步骤后,你也能够本身画一画。
咱们将{7,8,9,10,11,12}中的数值依次插入2-3树,画出它的过程:
因此,2-3树的设计彻底能够保证二叉树保持矮矮胖胖的状态,保持其性能良好。可是,将这种直白的表述写成代码实现起来并不方便,由于要处理的状况太多。这样须要维护两种不一样类型的节点,将连接和其余信息从一个节点复制到另外一个节点,将节点从一种类型转换为另外一种类型等等。
所以,红黑树出现了,红黑树的背后逻辑就是2-3树的逻辑,可是因为用红黑做为标记这个小技巧,最后实现的代码量并不大。(可是,要直接理解这些代码是如何工做的以及背后的道理,就比较困难了。因此你必定要理解它的演化过程,才能真正的理解红黑树)
看看红黑树和2-3树的关联,首先,红和黑的含义:红黑树中,全部的节点都是标准的2-节点,为了体现出3-节点,这里将3-节点的两个元素用左斜红色的连接链接起来,即链接了两个2-节点来表示一个3-节点。这里红色节点标记就表明指向其的连接是红连接,黑色标记的节点就是普通的节点。因此才会有那样一条定义,叫“从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点”,由于红色节点是能够与其父节点合并为一个3-节点的,红黑树实现的实际上是一个完美的黑色平衡,若是你将红黑树中全部的红色连接放平,那么它全部的叶子节点到根节点的距离都是相同的。因此它并非一个严格的平衡二叉树,可是它的综合性能已经很优秀了。
【参考资料】
清晰理解红黑树的演变---红黑的含义
二叉查找树是有特定属性的,即一个结点的左孩子小于当前结点,当前结点又小于等于其右孩子的
因此一棵树,最大值必定在树的最右边,最小值在树的最左边,在此基础上能够分析代码:
public T removeMax() { T result = null; if (isEmpty()) throw new EmptyCollectionException("LinkedBinarySearchTree");//判断树是否为空,空则抛出异常 else {//不空的话,则判断树根的右孩子是否为空 if (root.right == null) {//根的右孩子为空,则最大值即为根元素,而后执行删除根元素操做 result = root.element; root = root.left;//让根的左孩子等于根,即完成删除操做 } else {//根的右孩子不为空,则执行递归操做,一直找到整棵树最右边的元素(即最大元素) BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.right; while (current.right != null) { parent = current; current = current.right; } result = current.element;//找到最大元素current,赋给result parent.right = current.left;//把要删除的元素的左孩子赋给要删除元素的双亲的右孩子,即完成删除当前元素的操做 } modCount--; } return result; }
这个删除最大值的代码与找最大值最小值的操做findMin,findMax差很少,都是同样的思路,只是找到以后,不会进行删除操做而已
以下是找最大值的代码实现,最小值的实现与之对称:
public T findMax() { T result = null; if (isEmpty()) throw new EmptyCollectionException("LinkedBinarySearchTree"); else { if (root.right == null) { result = root.element; } else { BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.right; while (current.right != null) { parent = current; current = current.right; } result = current.element; } } return result; }
最后的结果运行如图:
首先要理解什么是AVL树,什么样的树算AVL树
树都是用来存储数据的,普通的二叉树能够按照要求在任一能够放置孩子的结点处放置结点
而AVL树对放置结点的位置有要求,即放置完后会对结点进行调整,左旋、右旋,使树达到平衡
而且致使二叉树失衡的可能只有两种操做:插入、删除
因此设计的关键在于:如何经过旋转,使得失衡点处从新平衡
根据书上的介绍,旋转一共有四种状况:左旋,右旋,左右旋,右左旋,以知足对失衡点处的失衡状况的可能性分析
即四种旋转状况对应四种可能的失衡状况:
假设结点X为失衡点:(原先的二叉树代码中没有删除操做,这里也只讨论插入带来的失衡问题)
① 在结点X的左孩子结点的左子树中插入元素
② 在结点X的左孩子结点的右子树中插入元素
③ 在结点X的右孩子结点的左子树中插入元素
④ 在结点X的右孩子结点的右子树中插入元素
参照上面课本知识梳理中四种旋转的平衡化技术,对应这四种状况:第①状况和第④状况是对称的,能够经过单旋转来解决,而第②种状况和第③状况是对称的,须要双旋转来解决
左单旋代码实现:
private AVLNode<T> singleRotateLeft(AVLNode<T> x){ //把w结点旋转为根结点 AVLNode<T> w= x.left; //同时w的右子树变为x的左子树 x.left=w.right; //x变为w的右子树 w.right=x; //从新计算x/w的高度 x.height=Math.max(height(x.left),height(x.right))+1; w.height=Math.max(height(w.left),x.height)+1; return w;//返回新的根结点 }
右单旋代码实现:
private AVLNode<T> singleRotateRight(AVLNode<T> w){ AVLNode<T> x=w.right; w.right=x.left; x.left=w; //从新计算x/w的高度 w.height=Math.max(height(w.left),height(w.right))+1; x.height=Math.max(height(x.left),w.height)+1; //返回新的根结点 return x; }
利用上面写好的代码能够直接用于双旋的状况:
左右旋代码实现:
private AVLNode<T> doubleRotateWithLeft(AVLNode<T> x){ //w先进行RR旋转 x.left=singleRotateRight(x.left); //再进行x的LL旋转 return singleRotateLeft(x); }
右左旋代码实现:
private AVLNode<T> doubleRotateWithRight(AVLNode<T> x){ //先进行LL旋转 x.right=singleRotateLeft(x.right); //再进行RR旋转 return singleRotateRight(x); }
平衡化技术的代码实现以后,再实现插入操做便可,可是要分两步(由于是AVL树嘛):
首先由于是二叉查找树嘛,确定要对要插入的元素找到合适的位置嘛。。。(这里跟二叉树同样能够用递归算法来找)
而后呢就是平衡判断,判断插入元素以后,树是否平衡(只要评估子树便可),不平衡则经过上述的四种旋转来使树从新平衡
插入操做的代码以下:
public void insert(T data) { if (data==null){ throw new RuntimeException("data can\'t not be null "); } this.root=insert(data,root); } private AVLNode<T> insert(T data , AVLNode<T> p){ //说明已没有孩子结点,能够建立新结点插入了. if(p==null){ p=new AVLNode<T>(data); }else if(data.compareTo(p.data)<0){//向左子树寻找插入位置 p.left=insert(data,p.left); //插入后计算子树的高度,等于2则须要从新恢复平衡,因为是左边插入,左子树的高度确定大于等于右子树的高度 if(height(p.left)-height(p.right)==2){ //判断data是插入点的左孩子仍是右孩子 if(data.compareTo(p.left.data)<0){ //进行LL旋转 p=singleRotateLeft(p); }else { //进行左右旋转 p=doubleRotateWithLeft(p); } } }else if (data.compareTo(p.data)>0){//向右子树寻找插入位置 p.right=insert(data,p.right); if(height(p.right)-height(p.left)==2){ if (data.compareTo(p.right.data)<0){ //进行右左旋转 p=doubleRotateWithRight(p); }else { p=singleRotateRight(p); } } } else ;//if exist do nothing //从新计算各个结点的高度 p.height = Math.max( height( p.left ), height( p.right ) ) + 1; return p;//返回根结点 }
【参考资料】
二叉树与AVL树
java数据结构与算法之平衡二叉树(AVL树)的设计与实现
错题1:
错题1解析:选择排序经过重复地将下一个最小的元素放到最后排序的位置来对列表进行排序
错题2:
错题2解析:与错题1重复
错题3:
错题3解析:在从二叉搜索树中删除元素时,必须提高另外一个节点来替换要删除的节点。
错题4:
错题4解析:与错题3重复
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 0/0 | 1/1 | 4/4 | |
第二周 | 560/560 | 1/2 | 6/10 | |
第三周 | 415/975 | 1/3 | 6/16 | |
第四周 | 1055/2030 | 1/4 | 14/30 | |
第五周 | 1051/3083 | 1/5 | 8/38 | |
第六周 | 785/3868 | 1/6 | 16/54 | |
第七周 | 733/4601 | 1/7 | 20/74 |