目录
1,平衡二叉树的介绍
1.1,二叉排序树存在的问题
在介绍平衡二叉树之前,我们先来看看上一节中二叉排序树存在的问题,我们创建二叉排序树的时候,是根据给定的序列进行递归创建,但是这里有一个问题,如果给定的序列分布够均匀,理想情况下我们可以创建一颗类似于二分查找的二叉判定树,这种情况是最好的情况(也可以说二分查找判定书是最优的二叉排序树),但是大部分情况下,我们创建出来的二叉排序树不是类似于二分查找那样的二叉排序树,比如给定已知序列:(1,2,3,4,5),对应的二叉排序树为:
- 上面创建的树有有哪些问题?
- 左子树全部为空,从形式上看,更像一个单链表。
- 插入速度没有影响 查询速度明显降低(因为需要依次比较), 不能发挥BST 的优势,因为每次还需要比较左子树,其查询速度比 单链表还慢
- 也就是如果我们给定的序列是升序有序或者降序有序,那我们创建的二叉排序树将是一棵单支树,此时树的高度已经达到了最大,在这样的单支树上面进行查找操作时间复杂度也达到最大O(n),基本上为线性查找的时间复杂度,这种情况是我们不想看到的,上面的二分查找判定树对应的二叉树的高度是最低的,所以查找的效率比较高。
好了,为了解决上面创建树的过程中可能出现单支树的问题,我们就引出了今天要树的平衡二叉树,也叫作AVL树,它是一棵高度平衡的二叉树,在二叉排序树的基础上面,(也就是说在二叉排序树的形态之上),又作出了一些限制,来保证创建的树的高度不会出现单支树的情况,以保证我们每次在树上查找元素的效率最高。
1.2,平衡二叉树
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
- 平衡二叉树有以下特点:
- 它是一 棵空树。
- 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1
- 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
- 平衡之意,如天平,即两边的分量大约相同。如定义,假如一棵树的左右子树的高度之差超过1,如左子树的树高为2,右子树的树高为0,子树树高差的绝对值为2就打破了这个平衡。如依次插入1,2,3三个结点(如下图)后,根结点的右子树树高减去左子树树高为2,树就失去了平衡。那么我们在创建平衡二叉树的过程中,通过什么参数来判断平衡二叉树树是否达到平衡呢?
- 平衡因子:左子树的高度减去右子树的高度。由平衡二叉树的定义可知,平衡因子的取值只可能为0,1,-1.分别对应着左右子树等高,左子树比较高,右子树比较高。
我们来看上面的三棵树,其中第一棵和第三棵不是平衡二叉树,在第一棵树中,根节点的左孩子节点3的左子树的高度是2,但是节点3的右孩子高度是0,两个高度差是2,很明显不符合我们平衡二叉树的定义,同样的第三棵树,根节点的左子树高度是3,而右子树的高度是1,高度差是2,也不符合平衡二叉树的定义,对于第二棵树,每一个节点的子树高度差的绝对值不会超过1,所以是一棵平衡二叉树。
1.3,平衡二叉树的创建
好了,下面我们来看看平衡二叉树的创建过程。在创建平衡二叉树的过程中,保证平衡二叉树平衡的思想如下,每当在平衡二叉树中插入或者删除一个节点的时候,首先检查其插入路径上的节点是否因为此次插入或者删除操作而导致了不平衡,如果导致了不平衡,那么就先找到插入路径上面距离插入节点最近的平衡因子的绝对值大于1的节点A,再对以节点A为根的子树,在保证二叉树特性的前提下,调整各个节点的位置,使之重新达到平衡状态。
- 针对平衡二叉树的调整,一共有四种方式进行调整。
- LL旋转(右单旋转):这是因为在节点A的左孩子B上的左子树上面插入了新节点,导致A节点的平衡因子增大,所以要进行LL旋转。
- 旋转方式:以节点B为旋转轴,将节点B旋转到其父节点的位置,B的右子树作为节点A的左子树。
- RR旋转(左单旋转):由于在节点A的右孩子B的右子树R上插入节点,A节点的平衡因子由-1变为-2,所以需要进行调整。
- 调整方法:以B节点为旋转轴进行旋转,B节点的左子树作为A节点的右子树。
- LR平衡旋转(先左后右双旋转):由于在A节点的左孩子的右子树R上面插入新节点,导致A节点的平衡因子由1变为2,需要进行两次旋转。
- 旋转方法:先向左旋转,后向右旋转,先将A节点的左孩子节点B的右孩子C提升到B节点的位置,然后再把C节点向右旋转提升到C的位置即可。
- RL平衡旋转(先右后左双旋转):由于在A节点的右孩子的左子树上面添加新节点,导致A节点的平衡因子由-1变为-2,所以需要进行双旋转。
- 调整方法:先右旋转后左旋转,先将A节点的右孩子B的左孩子节点C向右旋转提升到B的位置,然后在将C进行向左旋转提升到A的位置即可。
- 注意一点:LR和RL旋转,新节点究竟插入C节点的左子树还是右子树不会影响旋转的过程。
好了,现在创建平衡二叉树过程中旋转操作已经讲完了,现在我们来创建一颗平衡二叉树:(15,3,7,10,9,8)
- 第一步:插入节点15
- 第二步:插入节点3
- 第三步:插入节点7,在这一步,插入节点7后,发现根节点的平衡因子是2,所以需要进行LR旋转操作。
- 第四步:插入节点10,
- 第五步:插入节点9,在插入节点9之后,我们发现节点15的平衡因子变为了2,发生不平衡,所以需要进行旋转,但是这里注意,虽然节点7页发生不平衡,但是我们说,旋转先找到插入路径上面距离插入节点最近的平衡因子的绝对值大于1的节点首先进行旋转,所以在这里15这个节点首先发生不平衡,所以先对节点15做平衡处理之后再对其余的不平衡节点做平衡处理。
- 第六步:插入节点8,在插入节点8之后,节点7又发生不平衡,需要进行一次RL旋转。
平衡二叉树在构建的过程中,始终都在检查插入一个节点是否导致了树发生了不平衡,所以最终建立的二叉树,高度最好,查找效率也很好。
1.4,平衡二叉树的查找
平衡二叉树的查找过程和二叉排序树的查找过程一样,在查找过程中,和关键字比较的次数不会超过树的最大深度,假设以标示高度为
的平衡二叉树的最少节点数,那么有
,并且有
,可以证明,含有
个节点的平衡二叉树的最大深度是
,所以说平衡二叉树的平均查找长度是
。
2,代码实现
2.1,平衡二叉树的节点类型
class AvlNode{
private int data;
AvlNode left;
AvlNode right;
public AvlNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
@Override
public String toString() {
return "AvlNode{" +
"data=" + data +
'}';
}
}
2.2,LL旋转(单右旋转)
- 旋转步骤:
- 以当前节点的值创建一个新的节点newNode。
- 把新节点的右子树设置为当前节点的右子树。
- 把新节点的左子树设置为当前节点左孩子的右子树。
- 把当前节点的值设置为当前节点左子树的值。
- 把当前节点的左子树设置为当前节点左子树的左子树。
- 把当前节点的右子树设置为新的节点。
/**
* 对AVL树进行右旋转,因为左边的子树高度大于右边子树的高度
*/
public void rightRotate(){
// 以当前节点的值创建一个新的节点
AvlNode newNode=new AvlNode(this.data);
newNode.right=this.right;
newNode.left=this.left.right;
this.data=this.left.data;
this.left=this.left.left;
this.right=newNode;
}
2.3,RR旋转(单左旋转)
- 旋转步骤:(当前节点指的是发生不平衡的那个节点)
- 以当前节点的值创建一个新的节点newNode。
- 把新节点的左子树设置为当前节点的左子树
- 把新节点的右子树设置为当前节点右节点的左子树。
- 把当前节点的值换为右子节点的值。
- 把当前节点的右子树设置为右子树的右子树。
- 把当前节点的左子树设置为新节点。
/**
* 对avl树进行左旋转,因为右边的子树高度太高,需要降低
*/
public void leftRotate(){
// 创建一个新的节点,存储当前节点的值
AvlNode newNode=new AvlNode(this.data);
// 把新的节点的左子树设置为当前节点的左子树
newNode.left=this.left;
// 把新节点的右子树设置为当前节点右子树的左子树
newNode.right=this.right.left;
// 把当前节点的值替换成右子节点的值
this.data=right.data;
// 当前节点的右子树设置成当前节点的右子树的右子树
this.right=this.right.right;
// 当前节点的左子节点设置成新的节点
left=newNode;
}
2.4,向avl树中添加一个节点
注意:我们在上面创建平衡二叉树的过程中有四种旋转方式,在这里实现过程中,另外两种双旋转其实也可以分解为单旋转,只是多做一次判断而已,详情请看代码。
/**
* 向二叉排序树中添加一个节点
* @param node 需要添加的节点
*/
public void add(AvlNode node){
// 判断添加的节点是否是空
if(node == null){
return ;
}
// 判断当前节点的值和根节点的值大小
if(node.getData() < this.getData()){
if(this.left == null){
// 如果左子节点为空,直接挂上去即可
this.left=node;
}else {
// 如果不是空,就递归进行向左子树添加
this.left.add(node);
}
}else {
// 判断左子树是否是空,空的话直接添加节点
if(this.right == null){
this.right=node;
}else {
// 不空的话递归在左子树进行添加
this.right.add(node);
}
}
// 当添加完一个节点后,如果右子树的高度-左子树的高度绝对值大于1,就进行调整
// 发生左旋转,,也就是右子树的高度比较高
if(this.rightHeight()-this.leftHeight()>1){
// 如果右子树的左子树的高度大于他的右子树的右子树的高度
// 需要先对右子节点进行右旋转,然后对当前节点进行左旋转
if(right!= null && right.rightHeight()<right.leftHeight()){
// 先右旋转
this.right.rightRotate();
// 左旋转
this.leftRotate();
}else {
this.leftRotate();
}
// 每次添加一个节点判断一次,所以此if语句如果执行,那么结束后必须retrurn
// 不能再向下判断
return;
}
// 当添加完一个节点,发现左子树的高度-右子树的高度的差值>1,说明左子树比右子树高
// 也就是需要进行右旋转操作
if(leftHeight()-rightHeight()>1){
if(this.left!= null && left.rightHeight()>left.leftHeight()){
//先要对当前节点的左节点进行向左的旋转,然后在向右旋转
// 这里针对当前节点的左子节点进行向左旋转
left.leftRotate();
// 针对当前节点进行向右旋转
this.rightRotate();
}else {
// 否则直接进行右旋转
this.rightRotate();
}
}
}
2.5,求左右子树的高度
在向平衡二叉树中添加节点的额过程中,我们总是在求子数的高度差,所以在这里建立两个求子数高度的函数。
/**
* 返回右子树的高度
* @return 右子树的高度
*/
public int rightHeight(){
if(right == null){
return 0;
}else {
return right.height();
}
}
/**
* 返回左子树的高度
* @return 左子树的高度
*/
public int leftHeight(){
if(left == null){
return 0;
}else {
return left.height();
}
}
2.6,求平衡二叉树的高度
/**
* 返回以当前节点为根的子树的高度
* @return 返回子树的高度
*/
public int height(){
return Math.max(left == null?0:this.left.height(),right == null?0:this.right.height())+1;
}
2.7,中序遍历二叉树
// 中序遍历二叉树
public void midOrder(){
if(this == null){
return;
}
if(this.left != null){
this.left.midOrder();
}
System.out.println(this.getData());
if(this.right != null){
this.right.midOrder();
}
}
2.8,创建一颗平衡二叉树
class AVLTree{
private AvlNode root;
/**
* 向二叉排序树中添加一个节点
* @param node 待添加而定节点
*/
public void add(AvlNode node){
if(this.root == null){
this.root=node;
}else {
this.root.add(node);
}
}
/**
* 中序遍历二叉树
*/
public void midOrder(){
if(this.root == null){
System.out.println("当前二叉排序树是空树!");
return;
}else {
this.root.midOrder();
}
}
public AvlNode getRoot() {
return root;
}
}
3,测试代码
public class AvlSortedTreeDemo {
public static void main(String[] args) {
int arr[]={4,3,6,5,7,8};
int arr1[]={10,12,8,9,7,6};
AVLTree avlTree=new AVLTree();
for(int i=0;i<arr1.length;i++){
avlTree.add(new AvlNode(arr1[i]));
}
// avlTree.midOrder();
System.out.println(avlTree.getRoot().height());
System.out.println(avlTree.getRoot().leftHeight());
System.out.println(avlTree.getRoot().rightHeight());
}
}
参考资料:
[1] https://www.jianshu.com/p/51f26db28a67
[2] 数据结构与算法(Java版)