c++ AVL树的学习及简单实现

1. AVL树的概念

1.1. 引入

前面我们简单学习了并实现了二叉搜索树,二叉搜索树可以提高查找的效率,但是有些特殊情况下,二叉搜索树会出现问题。
我们知道,单纯的二叉搜索树,时间复杂度可能不是 log(n), 而是 n, 因为如果在插入数据有序的情况下,二叉搜索树会退化成单支树(单链表),查找元素就会使效率低下。
因此,两位俄罗斯的两位数学家,,G.M.Adelson-Velskii 和 E.M.LANDIS 在1962年发明了一种解决这种问题的方法:当向搜索二叉树中插入新节点后,如果能保证每个节点的左右高度差绝对值不超过 1(超过了则需要对树中节点进行调整),即可降低树的高度,从而减少平均搜索长度。

1.2. 概念

  • 它的左右子树都是 AVL树
  • 左右子树的高度差(简称平衡因子)的绝对值不超过 1(-1/0/1)

平衡因子:右子树高度 - 左子树高度
在这里插入图片描述
比如这里的搜索二叉树,其每个节点的(右子树高度-左子树高度) 的绝对值都是小于等于 1 的。这就是一棵 AVL树
如果一棵 AVL 树有 n 个节点,其高度可以保持在 log(n)。
注意这里为什么平衡因子可以是 -1 和 1,而不是所有平衡因子都是 0?
有些情况下做不到左子树和右子树高度相等:
在这里插入图片描述
当前两层的三个节点插满后,想要插入第 4 个节点,就必须有一个节点的平衡因子发生变化,如果平衡因子只能是 0 ,那么这里就无法在不改变平衡因子的条件下插入新节点。
同时还要注意,平衡因子并不是必须的。
平衡因子的作用是辅助我们判断节点的左右子树高度差,但是并不是个必需品,如果有其他更方便的方式判断节点左右子树高度差,那么也可以不需要平衡因子的辅助。
AVL 树是高度平衡的二叉搜索树(这里的高度就是平衡因子,子树的高度差)
其实 -1, 1 的情况也就是向下多了一层,对于查找来说影响并不大。

2. 简单的模拟实现

2.1. 节点

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent;

	pair<K, V> _KV;
	int _bf;
}

这里有些地方和前面二叉搜索树不同。
_parent,当插入或者删除节点时,我们需要更新平衡因子,不只是当前插入/删除节点的平衡因子,还有对应父节点的平衡因子,这里的 _parent 方便我们向上查找父节点。
_bf ,balance factor 平衡因子,辅助我们判断左右子树高度差。

2.2. insert

AVL树本质上还是一棵二叉搜索树,所以当插入节点时,还是需要先完成查找的工作

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
	bool insert(const K& key, const V& value)
	{
		if(_root == nullptr)
		{
			_root = new Node(key, value);
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while(cur)
		{
			if(cur->_kv.first < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if(cur->_kv.first > key)
			{
				parent = cur;
				cur = cuur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(key, value);
		if(parent->_kv.first < key)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
	}
private:
	Node* _root;
}

这一步我们完成了插入,但是光完成插入还远远不能满足 AVL树的结构,接下来我们需要观察,当 左右子树高度发生变化时,我们需要怎么调整。
这里我们需要分情况来讨论:
在这里插入图片描述
当插入节点在 8 的左边时,我们看到父节点平衡因子需要 -1, 除此之外,并不需要对 8 的父节点进行修改。
-1 后,我们发现,树的高度未发生变化,所以当平衡因子更新后,节点平衡因子绝对值小于等于 1 的,就不需要多余的修改。
如果是下面这种情况呢?
在这里插入图片描述
在 9 的右孩子位置插入节点。
9 的平衡发生变化,9 的平衡因子需要改变,同时影响了 8, 使 8 的平衡因子发生改变, 又向上影响,使 7 的平衡因子发生改变…
这样 9改变-> 8改变-> 7改变-> 5改变
这里我们在处理平衡因子时,要注意:

  • 如果子树的高度不变,就不会继续往上影响祖先
  • 如果子树的高度改变,就继续往上影响祖先
    新插入的节点平衡因子都是 0
    插在左孩子节点,bf–, 插在右孩子节点 bf++

平衡因子的修改大概是这个道理,但是平衡因子的绝对值 >= 2 了怎么办?
此时说明树的结构出现问题,就需要我们修改树的结构。
我们先写出修改平衡因子的这段代码
平衡因子的修改分为下面 3 种情况。

  1. 父亲 bf 更新后 为 0, 父亲所在的子树高度不变,不需要往上更新,插入结束,在插入节点前,父亲的平衡因子为 -1/1 ,一边高一边低,新插入的节点填上了低的那一边。
  2. 父亲 bf 更新后 为 1/-1 ,插入前平衡因子为 0, 父亲所在的高度发生变化,必须往上继续更新
  3. 父亲 bf 更新后为 2/-2 , 父亲所在的子树违反规则,需要调整处理
while(parent)
{
	if(cur == parent->_left)
	{
		parent->_bf--;
	}
	else
	{
		parent->_bf++;
	}
	
	if(parent->_kv == 0)
	{
		break;
	}
	else if(parent->_bf == 1 || parent->_bf == -1)
	{
		cur = parent;
		parent = parent->_parent;
	}
	else
	{
		//开始修改
	}
}

这里判断循环的条件不是很好取,所以我们取极端点的情况,如果 parent 一直向上找,找到了 _root, 但是 _root 的 parent 是空指针,_root 的左右子树中所有节点,其_parent 都不为空,只有 _root 的 _parent 为空,所以我们就按照 parent == nullptr 作为循环的出口。

2.2.1. 左单旋

上面我们把平衡因子处理后,发现结构出现了问题,此时就需要去修改结构。
这里先看第一种情况:
在这里插入图片描述
当出现某一子树的结构是这样时,1 的平衡因子为 2,就需要旋转
旋转一定是 左右两边不均衡(平衡因子绝对值大于1)
旋转目的:

  1. 左右均衡
  2. 保持搜索树的规则

这里我们需要找一个数,让这个数作为这棵子树的根,同时还要满足上面旋转的目的。
此时我们看见这里的 2,好像比较适合,把 1 放在 2 的左子树, 3 放在 1 的右子树
在这里插入图片描述
这样旋转一下就可以了,然后把 1, 2 的平衡因子修改一下即可(3本来就是 2 的右子树,所以 3 可以不动,3也就不需要修改平衡因子)
这种简单的大概知道怎么旋转后,我们看个稍微复杂点的
在这里插入图片描述
这棵树,9 的平衡因子为 2 ,结构出现问题。
首先还是,先找到需要这里谁最适合当根,9 的左子树比 右子树低 2, 所以不可能在 9 的左子树去找,只能去右子树上找, 18 右子树高度也只有 1 ,不合适,所以我们还是选 15。
此时就要注意怎么旋转
这里,要把 15 的左子树给 9 的右子树,再让 15 的左孩子指针指向 9,转化成这个结构
在这里插入图片描述
这就是左单旋,把新父亲的左孩子指向原父亲,把新父亲的左子树给原父亲的右子树。
其实上面 那个简单的情况也能这样处理(n看做空指针)
在这里插入图片描述
新节点插入较高右子树的右侧 – 右右左单旋
我们研究一下,为什么右边多插入一个节点就能平衡了,是不是对所有在 满足“右右”的树 都适合
在这里插入图片描述
插入前, a/b/c 是高度为 h 的 AVL子树(h>=0)
h == 0 :
在这里插入图片描述
60 的右节点是新插入的节点。
h == 1:
在这里插入图片描述
h == 1 和 h == 0 的情况还好,情况都比较固定,但是 h == 2 的 AVL 树 情况就复杂了,光底层的节点情况就有 3 种
在这里插入图片描述
先把这三种情况设为 x, y, z。
在这里插入图片描述
对应这个图中,a,b 可以是 x,y, z 中的任意一种,但是 c 一定是 x。
c 要满足的是,在右边插入一定会高
在这里插入图片描述

注:这里我们只关注插入在 c 位置的下方,主要是 60 的右子树高度发生了变化
这里a, b 的位置 x, y,z 都可以,所以要是对这些情况排列组合,起码要 9 种情况。同时 c 如果是 x 的结构,在 c 下面插入节点,还分了 4 种情况
在这里插入图片描述
合计h == 2 的情况,有 36 种树的结构。
但是不管高度为多少,我们主要关注的还是 30 和 60 这两个节点。
在这里插入图片描述
通过旋转,让 30 这个节点的左树向下移动,让30原本的右子树整体向上移动。
旋转细节:

  1. b 变成 30 的右子树
  2. 30 变为 60 的左子树
  3. 60 称为这棵树的根

2.2.2. 左单旋实现

简单知道实现逻辑,下面就开始实现左单旋
在这里插入图片描述

void Rotalel(Node* parent)
{
	Node* subR = parent;
	Node* subRL = subR->_left;
	
	subR->_left = parent;
	parent->_right = subRL;
}

按照逻辑我们先这样写,但是这样写对吗?
我们一方面要考虑代码正确性,同时还要思考,会不会出现特殊情况需要特殊处理。

  1. 这里我们用的是三叉连,除了直接改变节点,还要改变节点的各个指针。
subR->_left = parent;
parent->_parent = subR;

parent->_right = subRL;
subRL->_parent = parent;

三叉连在这里这样处理
但是还存在特殊情况,subRL 为 空
subRL 为空时,我们直接使用了 subRL 的 _parent 就会导致代码崩溃,所以这里要特殊处理一下。
除此之外,parnet 可能是子树,也可能是_root 节点
这里也需要特殊处理

void Rotatel(Node* parent)
{
	Node* subR = parent;
	Node* subRL = subR->_left;
	
	subR->_left = parent;
	if(parnet != _root)
	{
		parent->_parent = subR;
	}
	parent->_right = subRL;
	if(subRL != nullptr)
	{
		subRL->_parent = parent;
	}
}

在这里插入图片描述
经过处理,我们的结构变成了这个样子,但是 subR 的 parent 节点怎么处理?
在旋转中,30 可能是 root, 也可能不是,如果是 root,直接把 subR 的 parent 置空,如果不是空,就需要修改。

void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;
	
	parent->_right = subRL;
	subR->_left = parent;
	
	parent->_parent = subR;
	if(subRL)
	{
		subRL->_parent = parent;
	}
	if(parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if(parentParent->_left == parent)
		{
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}
	parent->_bf =subR->_bf = 0;
}

注意这里的细节:

  1. subRL 是否为空
  2. parent 是不是根节点
  3. subR 要插在根节点的哪个位置
  4. 旋转后平衡因子的 更新

这里平衡因子的更新要留意
我们进入左旋时,是 parent->_bf == 2,subR->_bf == 1
这种情况才能进行左旋,所有路径高度我们都能确定,所以这里我们旋转后的高度,就按照上图所示的来判断即可。

2.2.3. 右单旋

在这里插入图片描述
还是相同的图,还是相同的逻辑,只不过这次是
左左右单旋- 左子树中左边高的位置插入节点,需要右单旋。
这里因为和 右右左单旋 类似,就是个镜面翻转,不做细讲。

void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

	subL->_right = parent;
	parent->_parent = subL;
	
	parent->_left = subLR;
	if(subLR)
	{
		subLR->_parent = parent;
	}
	if(parent == _root)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if(parentParent->_left == parent)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}
	parent->_bf = subL->_bf = 0;
}

2.2.4. 左旋右旋条件

上面我们把左旋右旋的代码写好了,但是在 insert 函数里,什么时候去使用这两个函数呢?
我们看到左单旋的条件是 parent->_bf == 2, subR->_bf = 1。
而在 insert 函数里我们没有 subR,我们有 cur,所以进入左单旋的条件

else if(parent->_bf == 2 || parent->_bf == -2)
{
	if(parent->_bf == 2 && cur->_bf == 1)
	{
		RotateL(parent);
	}
}

满足上面这个条件,这里一定是左单旋。
右单旋镜面翻转一下

else if(parent->_bf == 2 || parent->_bf == -2)
{
	if(parent->_bf == 2 && cur->_bf == 1)
	{
		RotateL(parent);
	}
	if(parent->_bf== -2 && cur->_bf == -1)
	{
		RotateR(parent);
	}
}

旋转目的:

  1. 从不平衡,变成了平衡子树
  2. 旋转的本质,降低了高度
    在这里插入图片描述

2.2.5.左右双旋

上面两种情况很简单,左树左边高左边插节点,右树右边高右边插节点,但是如果是下面的情况呢?
在这里插入图片描述
这里,我们是在右树的左子树插入节点
这里的 b 变成了 h + 1
我们进行左单旋先看看
在这里插入图片描述
旋转完后,我们发现,b 的位置并没有改变,不仅如此,60 的左子树高度为 h + 2, 右子树高度为 h,高度差从2变为了 -2。
这里左单旋并不能改变不平衡的问题,不过按照这里的结构,我们把 b 细分一下
在这里插入图片描述
注:我们注重的是在 90 的左子树插入节点导致平衡改变,不止是在 b, c 上插入节点。
这里,a,d是高度为 h 的 AVL 树,b, c 高度是 h - 1 的 AVL树(也可以是空树h>=1)
如果 h == 0
在这里插入图片描述
这种情况就和上面左单旋看起来类似,只是此时 60 就是新插入的节点。
在这里插入图片描述

但是 h == 1, h == 2
首先,上图中的 a,d 可以是 x, y, z 中的任意一种,但是 c, d 只能是 x(因为要保证在c,d任意位置插入节点都能去旋转)
所以,当 h == 2 时,这里可能组成的树有 3 x 3 x 4 = 36种。
在这里插入图片描述
如果 h == 3 时,情况更多,所以我们还是需要找规律来旋转。
h == 1 时,60 这个节点就是新增的
h == 2 使,b,c就是新插入的节点。
在这里插入图片描述
我们采用的方法:

  1. 90 为旋转点进行右单旋
  2. 20 为旋转点进行左单旋
    在这里插入图片描述
    第一次 90 左单旋,使所有高的位置都在节点的右边,然后再从 30 这里进行右单旋。
    旋转后,我们发现 60 的左子树分到了 30 的右边,60 的右子树分到了 90 的左边。
    如果这里是 c 节点下面插入 在这里处理方式一样,只要是在 60 下面插入(包括60),都要 进行左右双旋
    在这里插入图片描述
    最简单的情况,60 就是新插入的节点,还是相同的思路,先对 90 进行左单旋,然后对 30 进行右单旋。
    理论成立,开始实现
    注:因为需要调整平衡因子,所以这里我们通过函数来实现左右双旋和右左双旋
void RotateRL(Node* parent)
{
	RotateR(parent->_right);
	RotateL(parent);
}

前面我们实现了左单旋和右单旋,这里我们直接复用就行了,最主要的还是平衡因子的处理
观察上图变化过程,我们知道,参与旋转的有三个节点,parent, parent->_right, parent->_right->_left。
所以我们主要修改这三个节点的平衡因子

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	RotateR(subR);
	RotateL(parent);
	//...
}

该处理平衡因子了,第一个问题:插入的节点在 b 还是在 c 上?
在这里插入图片描述

这里我们看见,当插入在b,c 上,结果是不同的
所以在修改前,我们要想办法记录在哪个位置插入的节点。
最简单的方法就是,看平衡因子,插入节点后,我们对应的 subRL 的平衡因子会改变,我们可以记录插入后的 subRL 的平衡因子来判断在什么位置插入的节点。

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateR(subR);
	RotateL(parent);
	if(bf == 0)
	{
		parent->_bf = subR->_bf = subRL->_bf = 0;
	}
	else if(bf == -1)
	{
		parent->_bf = 0;
		subRL->_bf = 0;
		subR->_bf = 1;
	}
	else if(bf == 1)
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

注意这里分的四种情况:
第2种和 第3种根据上面图片可以观察出来。
第1种,bf == 0,cur 就是新增节点,就不存在 cur 哪边高的问题
第4种,当 bf 不属于上面任何一种情况,说明 bf 出现了问题,出现了不该出现的值,直接报错处理。

else if(parent->_bf == 2 || parent->_bf == -2)
{
	if(parent->_bf == 2 && cur->_bf == 1)
	{
		RotateL(parent);
	}
	else if(parent->_bf == -2 && parent->_bf == -1)
	{
		RotateR(parent);
	}
	else if(parent->_bf == 2 && cur->_bf == 1)
	{
		RotateLR(parent);
	}
	else if(parent->_bf == 2 && cur->_bf == 1)
	{
		RotateLR(parent);
	}
	//...
	break;
}
  1. 旋转让这棵子树平衡
  2. 旋转降低了树的高度,恢复到和插入前一样的高度,所以对上一层没有影响,不需要向上继续更新。

2.2.6.左右双旋

左右双旋和右左双旋的逻辑基本一致,就是右左双旋的一个对称处理
在这里插入图片描述
情况二和情况三,需要注意平衡因子的变化,剩下两种情况和上面右左双旋一致

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR;
	RotateL(subL);
	RotateR(parent);
	if(bf == 0)
	{
		parent->_bf = subL->_bf = subLR->_bf = 0;
	}
	else if(bf == -1)
	{
		parent->_bf = 1;
		subLR->_bf = 0;
		subL->_bf = 0;
	}
	else if(bf == -1)
	{
		parent->_bf = 0;
		subLR->_bf = 0;
		subL->_BF = -1;
	}
	else
	{
		assert(false);
	}
}

insert 全部代码

    bool Insert(const pair<K, V>& kv)
    {
        if (_root == nullptr)
        {
            _root = new Node(kv);
            return true;
        }
        Node* cur = _root;
        Node* parent = nullptr;
        while (cur)
        {
            if (kv.first > cur->_kv.first)
            {
                parent = cur;
                cur = cur->_right;
            }
            else if (kv.first < cur->_kv.first)
            {
                parent = cur;
                cur = cur->_left;
            }
            else
            {
                return false;
            }
        }
        cur = new Node(kv);
        if (parent->_kv.first < kv.first)
        {
            parent->_right = cur;
            cur->_parent = parent;
        }
        else
        {
            parent->_left = cur;
            cur->_parent = parent;
        }

        while (parent)
        {
            if (cur == parent->_left)
            {
                parent->_bf--;
            }
            else
            {
                parent->_bf++;
            }

            if (parent->_bf == 0)
            {
                break;
            }
            else if (parent->_bf == 1 || parent->_bf == -1)
            {
                cur = parent;
                parent = parent->_parent;
            }
            else if (parent->_bf == 2 || parent->_bf == -2)
            {
                //旋转
                if (parent->_bf == 2 && cur->_bf == 1)
                {
                    //左单旋
                    RotateL(parent);
                }
                else if (parent->_bf == -2 && cur->_bf == -1)
                {
                    RotateR(parent);
                }
                else if (parent->_bf == 2 && cur->_bf == -1)
                {
                    RotateRL(parent);
                }
                else if (parent->_bf == -2 && cur->_bf == 1)
                {
                    RotateLR(parent);
                }
                //
                break;
            }
            else
            {
                assert(false);
            }
        }
        return true;
    }
 
    void RotateL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;

        parent->_right = subRL;
        subR->_left = parent;

        Node* parentParent = parent->_parent;

        parent->_parent = subR;
   
        if (subRL)
        {
            subRL->_parent = parent;
        }

        if (_root == parent)
        {
            _root = subR;
            subR->_parent = nullptr;
        }
        else
        {
            if (parentParent->_left == parent)
            {
                parentParent->_left = subR;
            }
            else
            {
                parentParent->_right = subR;
            }
            subR->_parent = parentParent;
        }
        parent->_bf = subR->_bf = 0;
    }

    void RotateR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;

        parent->_left = subLR;
        if (subLR)
        {
            subLR->_parent = parent;
        }

        Node* parentParent = parent->_parent;

        subL->_right = parent;
        parent->_parent = subL;

        if (parent == _root)
        {
            _root = subL;
            subL->_parent = nullptr;
        }
        else
        {
            if (parentParent->_left == parent)
            {
                parentParent->_left = subL;
            }
            else
            {
                parentParent->_right = subL;
            }
            subL->_parent = parentParent;
        }
        subL->_bf = parent->_bf = 0;
    }
    void RotateRL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        int bf = subRL->_bf;
        RotateR(parent->_right);
        RotateL(parent);
        if (bf == 0)
        {
            //subRL 自己就是新增
            parent->_bf = subR->_bf = subRL->_bf = 0;
        }
        else if (bf == -1)
        {
            // subRL 左子树新增
            parent->_bf = 0;
            subRL->_bf = 0;
            subR->_bf = 1;
        }
        else if (bf == 1)
        {
            //subRL 右子树新增
            parent->_bf = -1;
            subR->_bf = 0;
            subRL->_bf = 0;
        }
        else
        {
            assert(false);
        }
    }
    void RotateLR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        int bf = subLR->_bf;
        RotateL(parent->_left);
        RotateR(parent);
        if (bf == 0)
        {
            parent->_bf = subL->_bf = subLR->_bf = 0;
        }
        else if (bf == -1)
        {
            parent->_bf = 1;
            subLR->_bf = 0;
            subL->_bf = 0;
        } 
        else if (bf == 1)
        {
            parent->_bf = 0;
            subL->_bf = -1;
            subLR->_bf = 0;
        }
        else
        {
            assert(false);
        }
    }

2.3. 测试输出

2.3.1. 简单遍历

还是先遍历一遍,看看有没有什么问题
遍历还是中序遍历,注意写法即可

void InOrder()
{
	_InOrder(_root);
}
private:
void _InOrder()
{
	if(root == nullptr)
	{
		return;
	}
	_InOrder(root->_left);
	cout << root->_kv.first << "  ";
	_InOrder(root->_right);
}	
test_AVLTree()
{
	int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
	AVLTree<int, int> at1;
	for(auto e : a)
	{
		at1.Insert(make_pair(e,e));
	}
	at1.InOrder();
}

在这里插入图片描述
我们可以看到这里的输出结果,是按照升序来的,这能说明我们的树是 AVL 树吗?
不能,从遍历结果来看,这只能说明树是一搜索二叉树,是不是 AVL树还需要验证。

2.3.2. AVL树的检查

我们需要一个函数来判断,当前的树是不是 AVL树
每个节点都有平衡因子。能不能直接通过平衡因子来判断?
不行,平衡因子是一个辅助因素,只能作为参考高度的依据,不能作为直接判断高度的依据。如果我们代码中修改平衡因子的地方出现问题,全部都是1,能说明我们的树是AVL树吗?
所以这里我们需要计算高度的函数,通过这个函数计算左右子树的高度,来判断当前的树是不是 AVL 树

bool isbalance()
{
	return _isbalance();
}
private:
bool _isbalance(Node* root)
{
	if(root == nullptr)
	{
		return false;
	}
	int leftheight = _hieght(root->_left);
	int rightheight = _height(root->_right);
	return (abs(rightheight - leftheight) < 2 && _isbalance(root->_left) && _isbalance(root->_right));

只要我们的求高度函数写对了,这里检查 AVL 树的逻辑就不会有什么问题。(abs求绝对值)
下面开始写检查高度的代码

bool isbalance()
{
	return _isbalance(_root);
}
int height()
{
	return _height(_root);
}
private:
bool _isbalance(Node* root)
{
	if(root == nullptr)
	{
		return true;
	}
	int leftheight = _height(root->_left);
	int rightheight = _height(root->_right);
	if(rightheight - leftheight != root->_bf)
	{
		cout << root->_kv.first << "平衡因子异常" << endl;
		cout << rightheight - leftheight << "  " << root->_bf <<endl;
		return false;
	}
	return (abs(rightheight - leftheight) < 2 && _isbalance(root->_left) && _isbalance(root->_right));
}
int _height(Node* root)
{
	if(root == nullptr)
	{
		return 0;
	}
	int leftheight = _height(root->_left);
	int rightheight = _height(root->_right);
	return (leftheight < rightheight) ? (rightheight + 1) : (leftheight + 1);
}

我们现在来测试一下 树 是不是 AVL树

void test_AVLTree1()
{
	int a1[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	xsz::AVLTree<int, int> t1;
	for (auto e : a1)
	{
		if (e == 7)
		{
			int i = 0;
		}
 		t1.insert(make_pair(e,e));
	}
	t1.InOrder();
	cout << "is balance:" << t1.isbalance() << endl;
}

在这里插入图片描述
可以看到,这里是 1 说明树确实是 AVL 树,但是这能说明我们的代码对吗?
要想证明我们的代码没问题,仅仅只有这一串数字组成的树是无法证明的,所以我们需要大量的数据来测试

void test_AVLTree2()
{
	const int N = 1000000;
	vector<int> a1;
	a1.reserve(N);
	srand(time(0));
	for (int i = 0; i < N; i++)
	{
		a1.push_back(rand());
	}
	xsz::AVLTree<int, int> t1;
	for (auto e : a1)
	{
		t1.insert(make_pair(e, e));
	}
	cout <<"is balance:" << t1.isbalance() << endl;
	cout <<"height:" << t1.height() << endl;
}

在这里插入图片描述
这里我们能看到,100w 的数据,这里也能平衡,同时树的高度是 18(这里的随机是伪随机,会产生相同的数据,但是相同数据不能插入,所以表面上是100w个数据,实际上少于100w的数据)

这里的 1w 个数据也才 16层
在这里插入图片描述
当我们插入 100w 个数据,测量插入数据,检测AVL树,查看树高的时间,我们看到,debug 下只用了 246ms,这速度,真的非常快

2.3.3. size

这个没什么说的,查看有多少个节点

size_t size()
{
	return _size(root);
}
private:
size_t _size(Node* root)
{
	if(root == nullptr)
	{
		return 0;
	}
	return _size(root->_left) +_size(root->_right) + 1;
}

除了这个递归的方法,其他方式都可以,比如在私有成员里加入 size 的成员变量,插入元素++,删除元素–。
在这里插入图片描述
这里能看到,100w 个数据,实际上就 3w 多个。

2.3.4. find

AVL树 也是搜索二叉树,查找逻辑就按照搜索二叉树来就行了

Node* find(const K& key)
{
	Node* cur = _root;
	while(cur)
	{
		if(cur->_kv.first < key)
		{
			cur = cur->_right;
		}
		else if(cur->_kv.first > key)
		{
			cur = cur->_left;
		}
		else
		{
			return cur;
		}
	}
	return nullptr;
}

在这里插入图片描述
只查找了1次,最多查找了18次,所以这里时间很快。

在这里插入图片描述
我们查找所有数据,debug 下用了 197ms,和插入速度不相上下。

2.3.5. erase

erase 主要作为了解内容
学习 erase 的难度比 insert 的难度更大,所以这里只做理解,不实现
首先,删除节点还是要先查找到节点,这里我们不建议使用find,因为我们删除节点后,还要对父节点进行操作,所以最好在 erase 内实现查找,并记录父节点。
在这里插入图片描述
比如这棵树,找到节点后,还要分为各种情况。
左为空,右为空,左右都不为空
在这里插入图片描述
删除 2
删除 2 后,1 的平衡因子变为 -1, 但是 1 这棵树高度不变,所以 3 及以上的节点平衡因子不需要更新。
在这里插入图片描述
删除 9
删除 9 后,8 的平衡因子发生改变,同时 8 这棵树的高度也发生改变,所以需要向上更新。
在这里插入图片描述

删除 6
删除 6 后,7 的平衡因子发生改变,变成 2,此时需要进行旋转,右右左单旋,旋转后, 8 这棵树的 bf == 0,向上更新,5 的平衡因子 -1,但是 5 这棵树整体高度不变,不需要向上更新。
AVL树的删除需要注意:
父节点的 平衡因子 与另一边的高度相关,删除后,根据 这棵树的另一棵子树结构来判断怎么旋转
AVL树在实践中用的不多,因为 AVL树 是 严格的高度搜索二叉树,由于只要出现 bf == 2/-2 的情况,就必须进行旋转,最后生成的树,最短路径和最长路径差值最多为 1 。
但是 AVL 树 插入,删除,查找的 时间复杂度都是 logN,这一点是完全优于一般的搜索二叉树的。

  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值