数据结构与算法系列
数据结构与算法之哈希表
数据结构与算法之跳跃表
数据结构与算法之平衡二叉树
数据结构与算法之十大经典排序
数据结构与算法之二分查找三模板
数据结构与算法之平衡二叉树
简介
平衡二叉树,全名平衡二叉搜索树,因为其提出者为Adelse_Velskil和Landis,所以又名为AVL树。
说到二叉搜索树,就不得不提一提它的性质,左子树的节点大小都要比根节点小,右子树的节点大小都要比根节点大,同样左子树与右子树也都是二叉搜索树。这就使得一般情况下,二叉搜索树这一数据结构的各种操作可以达到O(logn)
的时间复杂度。
但是!!!由于这个“但是”所以大佬们提出了平衡二叉树的概念。
原来如果是下面这种二叉搜索树,那么或许就不太好解决了。
二叉树退化成了链表形式,这样的话,所有的操作就与链表无差了,也就是所谓的二叉树“失衡”了。
在此基础上就平衡二叉树,意在使得失衡的二叉树变得“平衡”起来。
性质
平衡二叉树在结构上沿袭二叉搜索树的性质:
- 平衡二叉树可以为空
- 平衡二叉树的任一节点的左右子树均为平衡二叉树,且左右子树的高度差值不可以超过1
举例说明
下图不是平衡二叉树,因为40不是一个平衡二叉树,其左右高度相差大于1
下面的也不是一个平衡二叉树,因为9和40的高度差值大于2
下面则是较为标准的平衡二叉树:
实现
节点
平衡二叉树节点定义如下
Java版本
class AVLNode{//树节点定义
private AVLNode left;//左节点
private AVLNode right;//右节点
private int data;
private int height;//树高度
public AVLNode(AVLNode left, AVLNode right, int data) {
this(left, right, data, 0);
}
public AVLNode(int data){
this(null, null, data);
}
public AVLNode(AVLNode left, AVLNode right, int data, int height) {
this.left = left;
this.right = right;
this.data = data;
this.height = height;
}
public void setHeight(int h) {
this.height = h;
};
public int getHeight() {
return this.height;
};
};
Go版本
type AVLNode struct {
left *AVLNode
right *AVLNode
data int
height int
}
func createNewNode(data int) *AVLNode{
return &AVLNode{
left: nil,
right: nil,
data: data,
height: 0,
}
}
平衡因子
对于平衡的概念,这一数据结构以左子树与右子树的高度差值是否大于1为标准从而认定二叉搜索树是否失衡,大佬们将这一标准称作平衡因子(Balance Factor)。
Java版本
public int getBalanceFactor(AVLNode tree){
return tree.left.getHeight() - tree.right.getHeight();
}
Go版本
type AVLTree struct {
root *AVLNode
}
func createNewTree(data int) *AVLTree{
return &AVLTree{
root:createNewNode(data),
}
}//AVL树结构
type method interface {//要实现的方法
getBalanceFactor(treeNode *AVLNode) int
leftLeftRotation(treeNode *AVLNode) *AVLNode
rightRightRotation(treeNode *AVLNode) *AVLNode
leftRightRotation(treeNode *AVLNode) *AVLNode
rightLeftRotation(treeNode *AVLNode) *AVLNode
insertNode(treeNode *AVLNode, key int) *AVLNode
insertValue(key int)
removeNode(treeNode *AVLNode, z *AVLNode)
removeValue(key int)
search(key int)
}
func(avlTree *AVLTree)getBalanceFactor(treeNode *AVLNode) int{
return treeNode.left.height - treeNode.right.height
}
显然,返回值大于0代表左子树比较高,返回值小于0代表着右子树比较高,等于0两边一样高。
节点插入失衡调整
上图,当插入60以后我们的节点就失衡了,那么该如何调整呢?别着急,下面我就带你一步一步解决失衡情况。根据插入节点的位置可分为四类:LL、RR、LR、RL。
LL:在某一节点左子树根节点的左子树插入节点导致失衡
按照从简入繁的节奏,先介绍LL情况应当如何去处理。
上图即为一种左子树根节点的左子树插入节点导致失衡,那么要解决失衡,就要想办法降低第一个出现失衡的节点的高度。
下面的流程展示了如何进行平衡调整:
- 找到
第一个失衡的节点
20(也就是标题中某一节点) - 将20的左子树节点的右子树节点转移给20
- 为了降低第一个失衡点的高度,同时我们发现10节点没有了右子树,属于失衡状态,那么就是让20成为它左子树节点10的右子树节点,这样既降低了第一个失衡节点的高度,同时又使得10节点没有发生新的失衡。
从这里我们引入一个概念,叫做最小失衡子树。
在新插入的节点向上查找,以第一个平衡因子的绝对值超过1的节点为根的子树称为最小不平衡子树,也称最小失衡子树。
其实就是刚才所说的第一个开始失衡的节点。
LL是最简单的一种情况,并且我们把这一调整失衡过程成为一次右旋操作。右旋即顺时针方向,你可以理解为,将20连同它的右子树以它的左子树节点为轴点进行了顺时针旋转,旋转到了右侧(右旋)。
那么我们明白了,对于LL的情况,我们只需一次右旋操作即可。
Java版本
/*
* LL:左左对应的情况(右旋)
* params:
* treeNode 根节点
* return:
* 返回旋转后的根节点
* */
private AVLNode leftLeftRotation(AVLNode treeNode){
AVLNode leftNode = treeNode.left;
treeNode.left = leftNode.right;
leftNode.right = treeNode;
int h = Math.max(treeNode.left.getHeight(), treeNode.right.getHeight()) + 1;//原根节点的高度取决于它的新左子树节点高度和它的未改动的右子树节点高度
treeNode.setHeight(h);
h = Math.max(leftNode.left.getHeight(), treeNode.getHeight()) + 1;//新根节点的高度取决它的左子树节点高度和成为它右子树节点的原根节点高度
leftNode.setHeight(h);
return leftNode;
}
Go版本
//LL : 右旋
func(avlTree *AVLTree)leftLeftRotation(treeNode *AVLNode) *AVLNode{
leftNode := treeNode.left
treeNode.left = leftNode.right
leftNode.right = treeNode
if treeNode.left.height > treeNode.right.height{
treeNode.height = treeNode.left.height + 1
}else{
treeNode.height = treeNode.right.height + 1
}
if leftNode.left.height > treeNode.height{
leftNode.height = leftNode.left.height + 1
}else{
leftNode.height = treeNode.height + 1
}
return leftNode
}
RR:在某一节点右子树根节点的右子树插入节点导致失衡
同样,对于RR情况,只不过翻过来而已。
- 找到
第一个失衡的节点
20(也就是标题中某一节点) - 将20的右子树节点的左子树节点转移给20
- 为了降低第一个失衡点的高度,同时我们发现40节点没有左子树,属于失衡状态,那么就是让20成为它右子树节点40的左子树节点,这样既降低了第一个失衡节点的高度,同时又使得40节点没有发生新的失衡。
我们把这一调整失衡过程成为一次右旋操作。左旋即逆时针方向,你可以理解为,将20连同它的左子树以它的右子树节点为轴点进行了逆时针旋转到了左侧(左旋)。
那么我们明白了,对于RR的情况,我们只需一次左旋操作即可。
Java版本
/*
* RR:右右对应的情况(左旋)
* params:
* treeNode 根节点
* return:
* 返回旋转后的根节点
* */
private AVLNode rightRightRotation(AVLNode treeNode){
AVLNode rightNode = treeNode.right;
treeNode.right = rightNode.left;
rightNode.left = treeNode;
int h = Math.max(treeNode.left.getHeight(),treeNode.right.getHeight())+1;
treeNode.setHeight(h);
h = Math.max(rightNode.left.getHeight(),treeNode.getHeight())+1;
rightNode.setHeight(h);
return rightNode;
}
Go版本
//RR : 左旋
func(avlTree *AVLTree)rightRightRotation(treeNode *AVLNode) *AVLNode{
rightNode := treeNode.right
treeNode.right = rightNode.left
rightNode.left = treeNode
if treeNode.left.height > treeNode.right.height{
treeNode.height = treeNode.left.height + 1
}else{
treeNode.height = treeNode.right.height + 1
}
if rightNode.right.height > treeNode.height{
rightNode.height = rightNode.right.height + 1
}else{
rightNode.height = treeNode.height + 1
}
return rightNode
}
LR:在某一节点左子树根节点的右子树插入节点导致失衡
下图这种情况,就是在20的左子树根节点 9的 右子树节点 15上插入节点13导致了失衡。这种情况其实就是使用了一次左旋再使用一次右旋即可。
- 先对20的左子树根节点进行RR操作
- 再对根节点20进行LL操作
RL:右子树中某一节点的左子树插入节点导致失衡
此处只展示过程,不再赘述
Java版本
private AVLNode leftRightRotation(AVLNode treeNode){
treeNode.left = rightRightRotation(treeNode.left);
return leftLeftRotation(treeNode);
}
private AVLNode rightLeftRotation(AVLNode treeNode){
treeNode.right = leftLeftRotation(treeNode.right);
return rightRightRotation(treeNode);
}
Go版本
func(avlTree *AVLTree)leftRightRotation(treeNode *AVLNode) *AVLNode{
treeNode.left = avlTree.rightRightRotation(treeNode.left)
return avlTree.leftLeftRotation(treeNode)
}
func(avlTree *AVLTree)rightLeftRotation(treeNode *AVLNode) *AVLNode{
treeNode.right = avlTree.leftLeftRotation(treeNode.right)
return avlTree.rightRightRotation(treeNode)
}
完整插入操作
对于,插入以上四种类型相对来说套路比较固定,也比较容易理解。
完整插入操作如下
Java版本
/*
* 插入节点——insert(tree,key)
* params:
* tree AVL树的根节点
* key 插入的节点键值
* return:
* 根节点
* */
private AVLNode insert(AVLNode tree, int key){
if(tree==null) {
//新建节点
tree = new AVLNode(null, null, key);
if(tree==null){
System.out.println("ERROR : create avlTree node failed!");
return null;
}
}else{
int cmp = key - tree.data;
if(cmp<0) {
tree.left = insert(tree.left, key);
if(getBalanceFactor(tree) == 2) {//左子树是2
if(key - tree.left.data > 0) {//大于左子树,说明插入在右子树节点
tree = leftRightRotation(tree);//左子树的右子树节点
}else {
tree = leftLeftRotation(tree);//左子树的左子树节点
}
}
}else if(cmp>0){
tree.right = insert(tree.right, key);
if(getBalanceFactor(tree) == -2) {//右子树应该是-2
if(key - tree.right.data > 0) {//大于右子树,说明插入在右子树节点
tree = rightRightRotation(tree);//右子树的左子树节点
}else {
tree = rightLeftRotation(tree);//右子树的右子树节点
}
}
}else {
System.out.println("添加失败:不允许添加相同的节点!");
}
}
tree.setHeight(Math.max(tree.left.getHeight(), tree.right.getHeight()) + 1);
return tree;
}
public void insert(int key) {
Root = insert(Root,key);
}
Go版本
func(avlTree *AVLTree)insertNode(treeNode *AVLNode, key int) *AVLNode {
if treeNode == nil{
treeNode = createNewNode(key)
}else{
cmp := key - treeNode.data
if cmp > 0 {//右子树
treeNode.right = avlTree.insertNode(treeNode.right, key)
if avlTree.getBalanceFactor(treeNode) == -2 {
if key - treeNode.right.data > 0{
treeNode = avlTree.rightRightRotation(treeNode)
}else{
treeNode = avlTree.rightLeftRotation(treeNode)
}
}
}else if cmp < 0{
treeNode.left = avlTree.insertNode(treeNode.left, key)
if avlTree.getBalanceFactor(treeNode) == 2 {
if key - treeNode.left.data > 0{
treeNode = avlTree.leftRightRotation(treeNode)
}else{
treeNode = avlTree.leftLeftRotation(treeNode)
}
}
}else{
fmt.Println("添加失败,不允许加入相同节点")
}
}
if treeNode.left.height > treeNode.right.height {
treeNode.height = treeNode.left.height + 1
}else {
treeNode.height = treeNode.right.height + 1
}
return treeNode
}
func(avlTree *AVLTree)insert(key int) {
avlTree.root = avlTree.insertNode(avlTree.root, key)
}
除了插入操作,其实删除操作也会带来失衡,删除一个节点意味着某些节点的高度会下降。
节点删除失衡调整
删除节点首先要明确节点的类型,以及删除后是否失衡。
根据节点类型分为三大类,四小类:
- 叶子节点
- 仅有左子树/仅有右子树
- 既有左子树又有右子树
删除节点为叶子节点
对于叶子节点,相对比较容易,因为是叶子节点,所以删除后直接从父节点开始考虑是否失衡,如果没有失衡继续向上判断父节点的父节点,然后开始套娃。。。。。。最常规的解决套娃问题的方法就显而易见了,你敢套娃,我就敢递归!(当然不能递归太深,狗头保命,递归可以使用栈实现)。
删除节点仅有左子树或者右子树
对于仅有左子树或者右子树的也比较简单,就是把该节点删除后,将它的子树交付给自己的父节点然后同样进行套娃判断即可。
删除节点既有左子树也有右子树
如果一个节点的左子树与右子树都存在,该如何是好呢?其实不慌,删除节点的意思就是将这个点的节点值抹去,除了删除节点还有一种办法就是使用其他的值对其进行替代!因为叶子节点的删除是最容易的,所以可以考虑将该节点的左子树中最大叶子节点(肯定是比当前节点小但是又比左子树其他节点大)或者右子树中最小叶子节点(肯定是比当前节点大但是又比右子树其他节点小),至于使用哪一个,当然还是要根据高度来了,当替代完以后,只需要按照删除叶子节点的模式继续删除就好,删除完毕再进行套娃…
Java版本
/*
* 删除节点——remove(tree,key)
* params:
* tree AVL树的根节点
* z 删除的节点
* return:
* 根节点
* */
private AVLNode remove(AVLNode tree,AVLNode z){
if(tree==null||z==null) return null;
int cmp = z.data - tree.data;
if(cmp<0) {//删除的节点在左子树
tree.left = remove(tree.left,z);
if(getBalanceFactor(tree)== -2) {//删掉左子树的节点,应该是右子树变高,要调整右子树节点
AVLNode r = tree.right;
if(r.right.getHeight() > r.left.getHeight())
tree = rightLeftRotation(tree);
else
tree = rightRightRotation(tree);
}
}else if(cmp>0){
tree.right = remove(tree.right, z);
if(getBalanceFactor(tree) == 2) {
AVLNode l = tree.left;
if(l.right.getHeight() > l.left.getHeight()) {
tree =leftRightRotation(tree);
}else {
tree =leftLeftRotation(tree);
}
}
}else {//上述其实也是一个寻找要删除节点的过程,一直往下递归寻找也就意味着保留了每次的父节点
if((tree.left != null) && (tree.right != null)) {
if(tree.left.getHeight() > tree.right.getHeight()) {
AVLNode max = maximum(tree.left);
tree.data = max.data;//把最大值赋给要删除的节点
tree.left = remove(tree.left, max);//再把最大值原来那个叶子节点删掉
}else{
AVLNode min = minimum(tree.right);//同上
tree.data = min.data;
tree.right = remove(tree.right, min);
}
}else {
AVLNode tmp = tree;
tree = (tree.left != null)? tree.left : tree.right;//此操作已经将当前节点的高度降低了!!!
tmp = null;//删掉原来那个节点
}
}
return tree;
}
public void remove(int key) {
AVLNode z;
if((z = search(Root,key)) != null)
Root = remove(Root, z);
}
/*
* 查找最小结点:返回tree为根结点的AVL树的最小结点。
*/
private AVLNode minimum(AVLNode tree) {
if (tree == null)
return null;
while(tree.left != null)
tree = tree.left;
return tree;
}
/*
* 查找最大结点:返回tree为根结点的AVL树的最大结点。
*/
private AVLNode maximum(AVLNode tree) {
if (tree == null)
return null;
while(tree.right != null)
tree = tree.right;
return tree;
}
/*
* 查找节点——search(tree,key)
* params:
* tree AVL树的根节点
* key 查找的节点的值
* return:
* 找到的节点
* */
public AVLNode search(AVLNode tree, int key) {
AVLNode tmp = tree;
while(tmp != null) {
if(tmp.data == key) return tmp;
else if(tmp.data > key) tmp = tmp.left;
else tmp = tmp.right;
}
return tmp;
}
Go版本
func(avlTree *AVLTree)removeNode(treeNode *AVLNode, del *AVLNode) *AVLNode {
if treeNode == nil || del == nil {
return nil
}
cmp := del.data - treeNode.data
if cmp > 0 {//删除的点在右子树
treeNode.left = avlTree.removeNode(treeNode.left, del)
if avlTree.getBalanceFactor(treeNode) == 2 {
l := treeNode.left
if l.right.height > l.left.height{
treeNode = avlTree.leftRightRotation(treeNode)
}else{
treeNode = avlTree.leftLeftRotation(treeNode)
}
}
}else if cmp < 0{
treeNode.right = avlTree.removeNode(treeNode.right, del)
if avlTree.getBalanceFactor(treeNode) == -2 {
r := treeNode.right
if r.right.height > r.left.height{
treeNode = avlTree.rightRightRotation(treeNode)
}else{
treeNode = avlTree.rightLeftRotation(treeNode)
}
}
}else{
if treeNode.left != nil && treeNode.right != nil{
if treeNode.left.height > treeNode.right.height{
max := avlTree.findMaxNode(treeNode.left)
treeNode.data = max.data
treeNode.left = avlTree.removeNode(treeNode.left, max)
}else{
min := avlTree.findMinNode(treeNode.left)
treeNode.data = min.data
treeNode.left = avlTree.removeNode(treeNode.left, min)
}
}else{
if treeNode.left != nil{
treeNode = treeNode.left
}else {
treeNode = treeNode.right
}
}
}
return treeNode
}
func(avlTree *AVLTree) remove(key int) {
tmp := avlTree.search(avlTree.root, key)
if tmp != nil{
avlTree.root = avlTree.removeNode(avlTree.root, tmp)
}
}
func(avlTree *AVLTree) findMaxNode(treeNode *AVLNode) *AVLNode{
if(treeNode == nil){
return nil
}
for treeNode.right != nil{
treeNode = treeNode.right
}
return treeNode
}
func(avlTree *AVLTree) findMinNode(treeNode *AVLNode) *AVLNode{
if(treeNode == nil){
return nil
}
for treeNode.left != nil{
treeNode = treeNode.left
}
return treeNode
}
func(avlTree *AVLTree) search(treeNode *AVLNode, key int) *AVLNode{
tmp := treeNode
for tmp != nil{
if tmp.data == key {
return tmp
}else if tmp.data > key{
tmp = tmp.right
}else{
tmp = tmp.left
}
}
return tmp
}
可以看出删除操作其实相对插入操作要复杂的多,这也是平衡二叉树的弊端,因此在AVL树的基础上,又引入了传说中的红黑树
,这个以后博主学习之后再与大家分享吧。
总结
- 平衡二叉树是一棵高度平衡的二叉树,因此避免了二叉树极端情况,查询的时间复杂度为
O(logn)
。 - 插入操作,会引入的失衡情况有四种:LL、RR、LR、RL,最复杂的情况也只是进行两次旋转,所以时间复杂度是
O(1)
(仅针对插入这一操作)。
- 删除操作,是平衡二叉树的弊端,每次需要往上考虑父节点的变化,导致过程比较繁琐。
- 不管是插入操作,还是删除操作,我们都是从第一个开始失衡的节点开始处理。