【C++】AVL树的旋转等

AVL树的旋转

右单旋

关于这几个旋转涉及到的结点,在命名时我们可以遵循一定的规律,最好不要随意命名。

比如subL就是parent的左孩子的意思,L代表left;subLR就是subL的右孩子的意思。

cpp 复制代码
void Rotate(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	subL->_right = parent;
}

但是,我们的结点现在是三叉链的,所以_parent也要进行修改,而现在我们还没有改。

cpp 复制代码
void Rotate(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	subL->_right = parent;

	subL->_parent = parent->_parent;
	parent->_parent = subL;
	subLR->_parent = parent;
}

这样还不对,因为subLR可能为空,其他两个则不可能。因为这种情况是h为0的情况。

所以我们还要预防空指针的解引用:

cpp 复制代码
void Rotate(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	subL->_right = parent;

	subL->_parent = parent->_parent;
	parent->_parent = subL;
	if (subLR)
		subLR->_parent = parent;
}

但这样还忽视了parent为根的情况,如果这种情况,subL要更新为新的根,所以:

cpp 复制代码
void Rotate(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	subL->_right = parent;

	subL->_parent = parent->_parent;
	if (parent->_parent == nullptr)
		_root = subL;

	parent->_parent = subL;
	
	if (subLR)
		subLR->_parent = parent;
}

这样还是不行,因为如果parent的parent不为空,我们还没有将它的孩子结点更新为subL。

所以我们不能直接上面这样写,而是要将parent为根与parent不为根分开讨论。

所以:

cpp 复制代码
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	subL->_right = parent;

	if (parent == _root)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		Node* pparent = parent->_parent;
		subL->_parent = pparent;
		if (pparent->_left == parent)
			pparent->_left = subL;
		else
			pparent->_right = subL;
	}

	parent->_parent = subL;
	
	if (subLR)
		subLR->_parent = parent;
}

图示看起来很简单的旋转,代码却有很多坑,并不简单。

核心的代码虽然只有最开始的这两句:

cpp 复制代码
parent->_left = subLR;
subL->_right = parent;

却衍生出其他很多

这是因为这个树有_parent,如果没有会简单很多

++但若没有parent,更新平衡因子就会变麻烦++。有这个parent,我们更新平衡因子就会像链表一样倒着遍历。

而且想跟parent的_parent链接时就会找不到

有这个_parent我们就得维护

注意此时,++虽然旋转完了,我们的平衡因子还没有更新++。

++平衡因子发生变化的原则是子树发生变化++

所以a和b和c树平衡因子是没有变化的,只有5和10这两个结点的平衡因子变了,现在它们都是0

完整的右单旋代码:

cpp 复制代码
void RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    subL->_right = parent;

    if (parent == _root)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        Node* pparent = parent->_parent;
        subL->_parent = pparent;
        if (pparent->_left == parent)
            pparent->_left = subL;
        else
            pparent->_right = subL;
    }

    parent->_parent = subL;

    if (subLR)
        subLR->_parent = parent;

    subL->_bf = 0;
    parent->_bf = 0;
}

这倒不是复杂,是细节多

然后思考一个问题:这整颗子树旋转完,++parent往上的结点的平衡因子还要再改变吗++?

我们看到插入结点之前与旋转之后的高度是一样的都为h+2,所以不用再往上更新了。

旋转把一切问题搞定了

现在我们写完了这个右单旋的函数,可以把它写入插入函数的更新平衡因子的那部分里:

左单旋

方法和右单旋完全类似

左单旋的关键在于b这棵子树在10-15之间,把b变成10的右子树,然后让10成为15的左子树。

左右双旋

单旋只是细节多,真正复杂的是双旋。

单旋是纯粹的一边高而双旋就是不够纯粹。

来看看插入前h=1的情况:

这就是单旋,很简单。把5的右边给10,把10变成5的右边。

什么是双旋?双旋对于10来说是左边高,但不是纯粹的左边高:

我们会发现用单旋的方法来解决这个形状的,则是无效的。把5的右边给10的左边,再把10变成5的右边:

这只是把5的平衡因子从-2变成了2,左边高变成了右边高。

我们把这种情况放到h=1的条件下观察:

我们看到插入后5的平衡因子是1,10的平衡因子是-2,正负号相反,也就是不是纯粹的一边高。

如果我们还是用单旋的逻辑:把8变成10的左边再把10变成5的右边就会变成图3这样,还是没有降低高度,对于现在的"根"结点5来说,还是平衡因子不符合AVL树。只不过从-2变成2.

将abc抽象出来概括h的所有情况,就是这样的图:

单双旋都是在10本身就已经不平衡的情况,再高一点平衡因子就不符合AVL性质了,只不过双旋不是单纯的一边高。

双旋怎么旋呢?先以5为旋转点左单旋一次(5是右边高),把8的左边给5的右边,再把5变成8的左边:

然后再以10为旋转点进行一次右单旋(因为10是左边高),所以这种双旋叫做左右双旋。把8的右边给10的左边,再把10变成8的右边。

这样的本质是我们先把不纯粹的一边高变为纯粹的一边高:当我们以5为旋转点进行左单旋时就是其变为纯粹的一边高。

这棵树最后就变得很平衡了

++双旋的旋转并不麻烦,麻烦的是平衡因子的更新++,要分三种大情况来讨论

我们可以直接去复用单旋代码来写双旋的代码:

cpp 复制代码
void RotateLR(Node* parent)
{
	//先左旋
	RotateL(parent->_left);
	//再右旋
	RotateR(parent);
}

但是引发左右双旋的插入情况不止这种,还可以这样:

插入在8的左边而不是右边。

这样的话8的平衡因子就是-1而不是1

这两种情况会有什么差别呢?

差别在于,旋转完新插入结点所在的位置不同。所以旋转完的"根"结点8的左右孩子的平衡因子不同。

如果我们不去看中间的单旋的过程,直接看结果:8要变成整棵树的根,5和10分别要变成8的左右子树,然后8的左右要分别分给5的右和10的左。

把这个树切成3个部分来看:

8变为根节点,8的左边分给了5的右边,8的右边分给了10的左边。

再看插入到8的左边的情况

8变成根节点,8的左边分给了5的右边,8的右边分给了10的左边。

所以在8的左边还是右边插入都会引发双旋,但是因为8的左边分给5的右边,8的右边分给10的左边,所以就会影响5和10的平衡因子。

我们怎么区分是插入到了8的左边还是右边呢?

我们看8的平衡因子,如果是1就是右边,-1就是左边。

还有一种情况,就是h为0 ,这种情况也是非纯粹的一边高所以也不能单旋解决,而且++这种情况的5和10的平衡因子最后都为0++。

也是8变为根节点,8的左边给5的右边,8的右边给10的左边。

所以我们要分为三种场景:

这三种场景可以通过抽象图统一地看(但我们要先看具体的情况才能更好地理解)

场景1

场景2

场景3

这三种的区分可以通过关注8的平衡因子

我们发现从结果的角度来看反而更清晰,总之就是让8成为根,而5和10分别作为它的左右孩子,因为5肯定是比8要小,10肯定比8要大。

我们调用了两个单旋,而我们的单旋代码是会更改平衡因子的。

比如这里,单旋的调用会使我们最后8、5 、10的平衡因子都变为0,而实际情况并非如此。

所以我们要先记录 下parent subL subLR++,只有这3个结点的平衡因子会更新++:

而a e f c在旋转的过程中是没有动的:

虽然我们把e变成了5的右边,把f变成了10的左边,aefc我们是没有去动的。

我们动的是5 8 10的孩子。

8的平衡因子会影响其他的平衡因子。

我们记录我们要记录的结点,同时还要记录subLR的平衡因子

cpp 复制代码
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;//要先记录下来因为在单旋过程中会被改变掉
	//先左单旋
	RotateL(parent->_left);
	//再右单旋
	RotateR(parent);
    //旋完分三种情况讨论即可
    //我们抓住8的平衡因子进行讨论就行了
    
}

我们可以看到,当8的平衡因子为-1时,最后subL平衡因子为0,parent平衡因子为1;

同理,当8的平衡因子为1时,最后subL平衡因子为-1,parent平衡因子为0;

当8的平衡因子为0时,subL平衡因子就为0,parent也为.

cpp 复制代码
if (bf == -1)
{
	subLR->_bf = 0;
	subL->_bf = 0;
	parent->_bf = 1;
}
else if (bf == 1)
{
	subLR->_bf = 0;
	subL->_bf = -1;
	parent->_bf = 0;
}
else if(bf == 0)
{
	subLR->_bf = 0;
	subL->_bf = 0;
	parent->_bf = 0;
}
else//小心bug,断死
{
	assert(false);
	//也可以抛异常
}

写成这样,无论单旋结果是否相同也要更新,降低耦合度

虽然只有三种情况,但还是要写这个else比较好,这样出问题了不用调试看到断言就明白了,这叫防御式编程

同样的,在下图中的要进行哪种旋转的平衡因子的判断逻辑里我们也不把最后的右左双旋直接写成else而是仍用else if

cpp 复制代码
//旋转
//右单旋
if (parent->_bf == -2 && cur->_bf == -1)
{
				RotateR(parent);
}
//左单旋
else if (parent->_bf==2 && cur->_bf == 1)
{
				RotateL(parent);
}
//左右双旋
else if (parent->_bf == -2 && cur->_bf == 1)
{
				RotateLR(parent);
}
//右左双旋
else if (parent->_bf == 2 && cur->_bf == -1)
{
				RotateRL(parent);
}
//断死
else
{
				assert(false);
}

右左双旋

同样的还是三种场景

h>=1的情况

将b展开:

就分为在e插入和在f插入两种情况:

这个也是一样的,现在我们的12就相当于我们在左右双旋里的8,同样也是将12变为根节点,然后10和15分别变为12的左和右,因为15比12大所以变为右,10比12小所以变为左。然后同样的,cefa也是旋转时不去动,只不过要把12的左和右分别给给15的左和10的右(因为要符合二叉树性质)

框架加insert完整代码参考:

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;

	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}

};

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:

	bool insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* cur = _root;
		Node* parent = _root;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		cur->_parent = parent;
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else

		{
			parent->_left = cur;
		}

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

			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)//单纯的左边高,同号
				{
					RotateR(parent);
				}
				//左单旋
				else if (parent->_bf == 2 && cur->_bf == 1)//单纯的右边高,同号
				{
					RotateL(parent);
				}
				//左右双旋
				else if (parent->_bf == -2 && cur->_bf == 1)//先左边高然后右边高,异号
				{
					RotateLR(parent);
				}
				//右左双旋
				else if (parent->_bf == 2 && cur->_bf == -1)//先右边高然后左边高,异号
				{
					RotateRL(parent);
				}
				//断死
				else
				{
					assert(false);
				}

				break;
			}
			else//这是一定不能出现的情况,直接断死
			{
				assert(false);
			}
		}

		return true;

	}
	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		subL->_right = parent;

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			Node* pparent = parent->_parent;
			subL->_parent = pparent;
			if (pparent->_left == parent)
				pparent->_left = subL;
			else
				pparent->_right = subL;
		}

		parent->_parent = subL;

		if (subLR)
			subLR->_parent = parent;

		subL->_bf = 0;
		parent->_bf = 0;
	}
	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* pparent = parent->_parent;

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

		if (_root == parent)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == pparent->_left)
				pparent->_left = subR;
			else
				pparent->_right = subR;
			
			subR->_parent = pparent;
		}
		parent->_parent = subR;
		if (subRL)
		{
			subRL->_parent = parent;
		}

		//平衡因子
		subR->_bf = parent->_bf = 0;

	}
	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		//先左旋------可以 直接复用
		RotateL(parent->_left);
		//再右旋
		RotateR(parent);

		if (bf == -1)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 0;
		}
		else//小心bug,断死
		{
			assert(false);
			//也可以抛异常
		}
	}
	//右左双旋
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		//都是先右旋再左旋,这部分直接复用
		//右单旋
		RotateR(parent->_right);
		//左单旋
		RotateL(parent);//此时parent已经更新过了
		//分情况讨论平衡因子更新------讨论的关键在于subRL的平衡因子的三种情况
		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}

private:
	Node* _root = nullptr;
};

AVL树的旋转代码到此结束,以下是对AVL树其他内容的补充。

中序遍历

cpp 复制代码
class AVLTree
{
    //......
public:
    void InOrder()
        {
            _InOrder(_root);
            cout << endl;
        }

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

private:
	Node* _root = nullptr;
};

和以前讲过的搜索树一样,因为不好拿到_root(私有),所以在 _InOrder(私有)外面套一层InOrder(公有)。

类外面拿不到_root,但是类里面可以拿得到。

怎么确定这棵树是不是AVL树呢?能通过观察平衡因子来确认吗?

不能。

而是应该去计算左右子树各自的高度。然后算高度差。

然后我们还可以反向验证一下平衡因子,如果高度差(也是右减左)不等于平衡因子,说明平衡因子异常。

然后再去把左树和右树都检查一下。

只有当根节点以及它的左右子树都满足 AVL 树的性质时,这棵树才是 AVL 树。这样可以确保树中的每个节点及其子树都符合 AVL 树的平衡要求。

如果只检查根节点和它直接连接的左右子树高度差,而不检查左右子树内部是否满足 AVL 树的性质,就可能会遗漏不符合 AVL 树定义的情况。

假设我们有一个树结构,根节点的左右子树高度差满足 AVL 树的条件,但是左子树本身不是 AVL 树(比如左子树的某个子树节点的左右子树高度差超过了 1)。那么从整体上看,这棵树就不是 AVL 树。

但这个检查方式是前序遍历的,不那么好。因为会重复算。++用后续遍历效率会更好++。

这个也需要套一层。

cpp 复制代码
class AVLTree
{
//......
public:
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}

private:
	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}
	bool _IsBalanceTree(Node* root)
	{
		//空树也算AVL树
		if (root == nullptr)
			return true;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int diff = rightHeight - leftHeight;
		if (abs(diff) >= 2)
		{
			cout << "高度差异常!" << endl;
			return false;
		}
		if (diff != root->_bf)
		{
			cout << "平衡因子异常!" << endl;
			return false;
		}
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}
private:
	Node* _root = nullptr;
}

更全面的测试:

cpp 复制代码
/插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void AVLTreeTest2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand()+i);//rand最多三万多个随机数,加上i可以增加数量但仍然可能有重复
	}

	size_t begin2 = clock();
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.insert({e,e});
	}
	size_t end2 = clock();
	cout << "Insert:" << end2 - begin2 << endl;
	cout << t.IsBalanceTree() << endl;
}

它插入的时间复杂度是 O ( l o g N ) O(logN) O(logN),因为这是一棵左右很均衡的树。 O ( l o g N ) O(logN) O(logN)是很快的,高度:1000个为10,100w才20,10亿才30。

更新平衡因子最坏情况也就更新到根为 O ( l o g N ) O(logN) O(logN),旋转可以认为是 O ( 1 ) O(1) O(1),常数次。

10w个数,插入一个数也就几十次的比较、调整、旋转。

可以看到,10w个数的插入也就消耗了22毫秒(debug模式下)。

我们再测试一下高度和size,并且也测试一下查找的速度.

Find效率很高,因为AVL树左右很均衡,接近完全二叉树。接近完全二叉树,时间复杂度一般就为高度,也就是 O ( l o g N ) O(logN) O(logN)。

Find无需用递归去写。

完整的AVLTreeTest2()代码:

cpp 复制代码
void AVLTreeTest2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand()+i);//rand最多三万多个随机数,加上i可以增加数量但仍然可能有重复
	}

	size_t begin2 = clock();
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.insert({e,e});
	}
	size_t end2 = clock();
	cout << "Insert:" << end2 - begin2 << endl;
	cout << t.IsBalanceTree() << endl;
	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;

	size_t begin1 = clock();
	/*for (auto e : v)
	{
		t.Find(e);
	}*/
	for (size_t i=0;i<N;i++)
	{
		t.Find((rand() + i));
	}
	size_t end1 = clock();
	cout << "Find:" << end1 - begin1 << endl;
}

要补充的函数:

cpp 复制代码
	int Height()
	{
		return _Height(_root);
	}

	int Size()
	{
		return _Size(_root);
	}

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

private:

	int _Size(Node* root)
	{
		if (root == nullptr)
			return 0;
		return _Size(root->_left) + _Size(root->_right) + 1;
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

打印结果:

一般来说插入的消耗是最大的,因为不仅要插入还有更新平衡因子,可能还要旋转,最重要的是还要new结点。

确定的值的搜索:

查找确定存在的值会更快,因为不存在的值得把高度走完。

删除

可以回忆一下搜索树的删除:

可以知道我们删除的结点都是偏下面的。(不一定是叶子)

删除结点要做的事:1.找到删除结点进行删除;2.更新平衡因子;3.旋转

删除结点会让高度降低,比如删除这个14结点:

10的平衡因子就会-1。原本我们插入时看是否要继续往上更新时,变为0说明原本1或者-1,我们插入到了矮的那边所以不用再往上更新了,而现在我们的删除, 是否要继续往上更新,我们看10平衡因子现在是0说明原本是1或者-1,本来就是一边高一边低,我们删除了高的那边,高度改变了,还要继续往上更新。这与插入的逻辑有些不相似。

如果我们现在删除的是7,6的平衡因子减成-1,还需要继续更新吗?因为原本是0才能变为1或者-1,原本是0说明左右一样高,删除一个结点后3的右子树高度并没有改变,所以3的平衡因子不会发生改变,不用再更新了。

如果我们现在再删除了10,那么8的平衡因子就变成-2了,就需要旋转了。要分情况讨论。

删除会比插入更复杂。

本文结束。

相关推荐
go_bai2 分钟前
数据结构——单链表
数据结构·经验分享·笔记·学习方法
奶油泡芙9318 分钟前
Powering the Hero (easy version)为英雄提供力量(简单版)
c++
孙鹏宇.20 分钟前
C++打地鼠游戏一小时极限开发
c++·游戏
伍贰什丿20 分钟前
C语言学习day22:URLDownloadToFile函数/开发文件下载工具
c语言·c++·学习
qq_3667406021 分钟前
ubuntu+ros新手笔记(五):初探anaconda+cuda+pytorch
pytorch·笔记·ubuntu
Cooloooo25 分钟前
Treap树堆【东北大学oj数据结构8-4】C++
开发语言·数据结构·c++
wks198912151 小时前
线程sleep的时候会释放锁吗
开发语言·c++·算法
伍贰什丿1 小时前
C语言学习day22:ReadProcessMemory函数/游戏内存数据读取工具开发
c语言·开发语言·学习·游戏
归寻太乙2 小时前
C++初阶:模版的进阶使用
android·java·c++
zmd-zk2 小时前
shuffle——spark
大数据·分布式·python·学习·spark