二叉平衡搜索树AVL 学习解析 及代码实现研究
Posted 踩踩踩从踩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉平衡搜索树AVL 学习解析 及代码实现研究相关的知识,希望对你有一定的参考价值。
前言
我们为了提高数据检索效率,从而引进树型结构;而常见的就是二叉树,一颗基本的二叉树他们都需要满足一个基本性质--即树中的任何节点的值大于它的左子节点,且小于它的右子节点。按照这个基本性质使得树的检索效率大大提高。如果一旦失去平衡则会出现检索效率大大降低的效果,为了维持二叉树的平衡,大牛们提出了各种实现的算法,如:AVL,SBT,伸展树,TREAP ,红黑树等等;这篇文章将以java实现二叉平衡搜索树(avl)
原理及实现
二叉平衡搜索树(Self-Balancing Binary Search Tree)又被称为AVL树,是一种二叉排序树,其中每一个节点的左子树和右子树高度差最多等于1
左边的是AVL树,它任意节点的子节点的高度差最多相差1 ,右边的非平衡二叉树也叫最小不平衡树,距离插入节点最近,且平衡因子的绝对值大于1的结点为根的子树,我们称之为最小不平衡树。
这其中有个很重要的概念,也是实现平衡二叉树的重要元素、
平衡因子
二叉树上节点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor)
旋转
要实现平衡二叉树 就需要 向左旋转 向右旋转 添加数据过后进行左右平衡
左旋
右旋
先添加一组数据进行分析
主要实现的流程图
当数据添加到1时,平衡因子最大已经大于1了,开始 右旋
现在进行一次左旋就可以将树平衡起来,继续添加数据
继续添加数据
这就是整个进行平衡二叉树的的转换流程
- 我们为保持平衡二叉树的过程,添加前六个数时,因为不复杂,平衡因子为2 ,即左边比右边深,则找到添加元素的父节点 ,进行右旋即可;平衡因子为-2 则,右边比左边深,则找到添加元素的父节点 ,进行左旋即可
- 当我们添加七个数时,为平衡因子出现 -2 ,但是我们不能左旋,因为一旦左旋,则 会出现右边比父节点大,也就是说插入节点 为 左子节点,但平衡因子为-2,我们只能采用先右旋 父节点;然后在父节点 进行左旋即可
- 插入86最后一个数 ,也就是插入 节点的 右边 , 我们也只能 先右旋 父节点, 然后在父节点的父节点 进行左旋就可以了。具体实现可以看一下代码
java实现
创建树节点
public class Node<E extends Comparable<E>> {
E elemet;
int balance = 0;//平衡因子
Node<E> left;
Node<E> right;
Node<E> parent;
public Node(E elem, Node<E> pare) {
this.elemet = elem;
this.parent = pare;
}
public E getElemet() {
return elemet;
}
public void setElemet(E elemet) {
this.elemet = elemet;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public Node<E> getLeft() {
return left;
}
public void setLeft(Node<E> left) {
this.left = left;
}
public Node<E> getRight() {
return right;
}
public void setRight(Node<E> right) {
this.right = right;
}
public Node<E> getParent() {
return parent;
}
public void setParent(Node<E> parent) {
this.parent = parent;
}
}
- 创建Node 树节点类
- 在树节点中 添加 泛型 数据 平衡因子 balance 默认为 0 在进行计算平衡时,需要改变
- 并添加 树节点所需的 左子树节点 右子树节点 以及父节点
左旋操作
public void left_rotate(Node<E> x){
if(x!=null){
Node<E> y=x.right;//先取到Y
//1.把h作为X的右孩子 x.right=h
x.right=y.left;
//2.h不为空 ,则将h的父亲设置为 x
if(y.left!=null){
y.left.parent=x;
}
//3。把Y移到原来X的位置上 y的父亲修改为 x的父亲
y.parent=x.parent;
// 4.x父亲为空
if(x.parent==null){
//设置根节点为y节点
root=y;
}else{
//x父亲不为空,则需要判断 x是父亲的左孩子还是右孩子 将父的左孩子或者右孩子设置为y
if(x.parent.left==x){
x.parent.left=y;
}else if(x.parent.right==x){
x.parent.right=y;
}
}
//3.X作为Y的左孩子
y.left=x;
x.parent=y;
}
}
整个左旋操作还是很简单的,基本就是通过链表前后断链并重新链接实现的 需要结合代码看一下,在代码上备注了有的
- 先取到Y 把h作为X的右孩子 x.right=h
- h不为空 ,则将h的父亲设置为 x
- 把Y移到原来X的位置上 y的父亲修改为 x的父亲
- x父亲为空 设置根节点为y节点
- x父亲不为空,则需要判断 x是父亲的左孩子还是右孩子 将父的左孩子或者右孩子设置为y
- X作为Y的左孩子
右旋操作
public void right_rotate(Node<E> y) {
if (y != null) {
Node<E> yl = y.left;
//step1
y.left = yl.right;
if (yl.right != null) {
yl.right.parent = y;
}
// step2
yl.parent = y.parent;
if (y.parent == null) {
root = yl;
} else {
if (y.parent.left == y) {
y.parent.left = yl;
} else if (y.parent.right == y) {
y.parent.right = yl;
}
}
// step3
yl.right = y;
y.parent = yl;
}
}
右旋操作 和左旋操作很像
- 首先拿到 y的左节点 x 在拿到 x的右节点 i 用于右旋时,进行移动
- i不为空,则将 i的父节点 设置为 y
- 拿到 y的 父节点 判断是否为根节点 ,不为根节点 则 判断是否y为父节点的左节点 还是有节点 并将父节点的左节点 或右节点 设置为 x 将 x的父节点设置为y的父节点
- 最后将 x的右节点设置为y, y的父节点设置为x 就可以了
新增一个节点
public boolean insertElement(E element) {
Node<E> t = root;
if (t == null) {
root = new Node<E>(element, null);
size = 1;
root.balance = 0;
return true;
} else {
//开始找到要插入的位置
int cmp = 0;
Node<E> parent;
Comparable<? super E> e = (Comparable<? super E>) element;
do {
parent = t;
cmp = e.compareTo(t.elemet);
if (cmp < 0) {
t = t.left;
} else if (cmp > 0) {
t = t.right;
} else {
return false;
}
} while (t != null);
//开始插入数据
Node<E> child = new Node<E>(element, parent);
if (cmp < 0) {
parent.left = child;
} else {
parent.right = child;
}
//节点已经放到了树上
//检查平衡,回溯查找
while (parent != null) {
cmp = e.compareTo(parent.elemet);
if (cmp < 0) {
parent.balance++;
} else {
parent.balance--;
}
if (parent.balance == 0) {//如果插入后还是平衡树,不用调整
break;
}
if (Math.abs(parent.balance) == 2) {
//出现了平衡的问题,需要修正
fixAfterInsertion(parent);
break;
} else {
parent = parent.parent;
}
}
}
size++;
return true;
}
- 插入数据 ,判断 根节点是否为空,是空则设置为根节点, 平衡因子为0, size 设置为1
- 然后开始找插入的位置,用do while进行查找 到需要插入的位置 记录着根节点 进行判断是插入左边还是 右边
do {
parent = t;
cmp = e.compareTo(t.elemet);
if (cmp < 0) {
t = t.left;
} else if (cmp > 0) {
t = t.right;
} else {
return false;
}
} while (t != null);
- 然后插入数据
if (cmp < 0) {
parent.left = child;
} else {
parent.right = child;
}
- 接下来就是检查平衡并修正数据 ,这里涉及到左平衡和右平衡
private void fixAfterInsertion(Node<E> parent) {
if (parent.balance == 2) {
leftBalance(parent);
}
if (parent.balance == -2){
rightBalance(parent);
}
}
设置平衡因子
设定平衡因子(左子树高度-右子树高度):
LH=1(表示:左子树高,插入节点为左子节点)
RH=-1(表示:右子树高,插入节点为右子节点)
EH=0(表示:左右子树高度相同)
右平衡操作
为什么需要右平衡操作,插入元素破坏平衡,平衡因子在设置为负数,既右子树过深;既然右子树过深,肯定要左旋
从上面的a[10]={3,2,1,4,5,6,75,100,91,86}一组数据 来推理的规律 分成两种情况(当插入新节点,平衡因子要改变的只有父节点往上的节点,叶子节点不变的)
- 当节点插入t的 右子节点(tr)的右子树时,直接将t元素,也就是插入元素往上遍历,失去平衡的点,进行左旋即可;tr到顶端 ;然后将 t节点及右子节点(tr)平衡因子设置eh即可。
当节点插入t的右子节点(tr)的左子树时,需要分为三种情况
- 右子节点(tr)的左子树时根节点(trl)平衡因子为lh,则 左子树深, 右子节点(tr)右旋,然后t节点进行左旋, trl到顶端 ;t的平衡因子和trl平衡因子设置为 eh,tr平衡因子设置为rh
- 右子节点(tr)的左子树时根节点(trl)平衡因子为rh, 则右子树深,右子节点(tr)右旋,然后t节点进行左旋,trl到顶端 ; tr的平衡因子和trl平衡因子设置为 eh,t平衡因子设置为lh
- 右子节点(tr)的左子树时根节点(trl)平衡因子为eh,因为在不断往上走的情况下,可能会出现这种情况;右子节点(tr)右旋,然后t节点进行左旋,trl到顶端 ;所有的平衡因子设置为eh
public void rightBalance(Node<E> t) {
Node<E> tr = t.right;
switch (tr.balance) {
case RH://新的结点插入到t的右孩子的右子树中
left_rotate(t);
t.balance = EH;
tr.balance = EH;
break;
case LH://新的结点插入到t的右孩子的左子树中
Node<E> trl = tr.left;
switch (trl.balance) {
case RH:
t.balance = LH;
tr.balance = EH;
trl.balance = EH;
break;
case LH:
t.balance = EH;
tr.balance = RH;
trl.balance = EH;
break;
case EH:
tr.balance = EH;
trl.balance = EH;
t.balance = EH;
break;
}
right_rotate(t.right);
left_rotate(t);
break;
}
}
- 如果新的结点插入到t的右孩子的右子树中,则直接进行左旋操作即可
如果新的结点插入到t的右孩子的左子树中,则需要进行分情况讨论
- 情况a:当t的右孩子的左子树根节点的balance = LEFT_HIGH
- 情况b:当t的右孩子的左子树根节点的balance = RIGHT_HIGH
- 情况C:当t的右孩子的左子树根节点的balance = EQUAL_HIGH
左平衡操作
为什么需要左平衡操作,插入元素破坏平衡,平衡因子在设置为正数数,t结点的不平衡是因为左子树过深;既然左子树过深,肯定要右旋保证平衡
上面有右平衡已经分析了左右平衡旋转,左平衡是很像的,因此我简单简述一下过程
- 当节点插入t的 左子节点(tl)的左子树时,直接将t元素,也就是插入元素往上遍历,失去平衡的点,进行右旋即可;tl到顶端 ;然后将 t节点及右子节点(tl)平衡因子设置eh即可。
当节点插入t的左子节点(tl)的右子树时,需要分为三种情况
- 左子节点(tl)的右子树时根节点(tlr)平衡因子为lh,则 左子树深, 右子节点(tl)左旋,然后t节点进行右旋, tlr到顶端 ;tl的平衡因子和tlr 平衡因子设置为 eh,t平衡因子设置为rh
- 左子节点(tr)的右子树时根节点(tlr)平衡因子为rh, 则右子树深,右子节点(tl)左旋,然后t节点进行右旋, tlr到顶端 ; t的平衡因子和tlr平衡因子设置为 eh,tl平衡因子设置为lh
- 左子节点(tr)的右子树时根节点(tlr)平衡因子为eh,因为在不断往上走的情况下,可能会出现这种情况;右子节点(tl)左旋,然后t节点进行右旋, tlr到顶端 ;所有的平衡因子设置为eh
public void leftBalance(Node<E> t) {
Node<E> tl = t.left;
switch (tl.balance) {
case LH:
right_rotate(t);
tl.balance = EH;
t.balance = EH;
break;
case RH:
Node<E> tlr = tl.right;
switch (tlr.balance) {
case LH:
t.balance = RH;
tl.balance = EH;
tlr.balance = EH;
break;
case RH:
t.balance = EH;
tl.balance = LH;
tlr.balance = EH;
break;
case EH:
t.balance = EH;
tl.balance = EH;
tlr.balance = EH;
break;
default:
break;
}
left_rotate(t.left);
right_rotate(t);
break;
}
}
左平衡操作,即结点t的不平衡是因为左子树过深
- 如果新的结点插入到t的左孩子的左子树中,则直接进行右旋操作即可
如果新的结点插入到t的左孩子的右子树中,则需要进行分情况讨论
- 情况a:当t的左孩子的右子树根节点的balance = RIGHT_HIGH
- 情况b:当t的左孩子的右子树根节点的balance = LEFT_HIGH
- 情况c:当t的左孩子的右子树根节点的balance = EQUAL_HIGH
删除节点操作
我想大家都能经过上面的理解,结合二叉排序树,然后在来理解删除节点还是比较简单把;
- 在删除某个节点时,需要保证数据排序性,因此需要将树进行先按照二叉排序树的删除节点 ;还需将平衡因子重新设置
- 在走一遍上面的左平衡 或者是右平衡
总结
整个二叉平衡搜索树,最关键 的点在于如何保证每一个节点的左子树和右子树高度差最多等于1;而要保证这一特性,则需要我们做左右平衡,如何判断左右平衡则需要平衡因子的判断,所以 如何左平衡 右平衡 ,显得格外重要,在左右平衡中分为几种属性。
以上是关于二叉平衡搜索树AVL 学习解析 及代码实现研究的主要内容,如果未能解决你的问题,请参考以下文章