AVL树和红黑树

目录

前言

一、AVL树

1、AVL树概念

2、AVL树节点的定义

3、AVL树的插入 

 4、AVL树的旋转

 5、AVL树的删除

二、红黑树 

1、红黑树的概念

2、红黑树节点的定义

3、红黑树的插入操作 

三、红黑树与AVL树比较


前言

哈喽,小伙伴们大家好。之前我们介绍了二叉搜索树用于搜索数据,但是二叉搜索树具有一些缺陷,比如在大多数节点的子节点都只有一个时,那搜索二叉树就会近似成一条线,搜索的时间复杂度就会从O(logN)退化成O(N)。针对这个问题,一些人对二叉搜索树进行了升级改造,也就是我们今天要学习的AVL树与红黑树。


一、AVL树

1、AVL树概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis(AVL树正是由这两位数学家的名字命名的)在1962年发明了一种解决上述问题的方法:当向一棵搜索二叉树中插入新节点时,要通过调整,使每个节点的左右子树高度差的绝对值不超过1,这样就能使二叉树接近满二叉树,进而提高搜素效率。

一棵AVL树具有以下性质:

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

AVL树和红黑树_第1张图片

2、AVL树节点的定义

我们定义AVL树的节点时,不但有左右指针,为了方便还增加了父节点的指针。其中bf代表平衡因子。

template
struct AVLTreeNode
{
	AVLTreeNode(const pair& kv)
		:left(nullptr)
		,right(nullptr)
		,parent(nullptr)
		,bf(0)
		,_kv(kv)
	{}

    AVLTreeNode* left;
	AVLTreeNode* right;
	AVLTreeNode* parent;
	int bf;  //banlance factor 平衡因子
	pair _kv;
};

3、AVL树的插入 

AVL树就是在二叉搜索树的基础上引入了平衡因子,因为AVL树也可以看作是二叉搜索树。AVL树的插入分为两步:

  • 按照二叉搜索树的方式去插入
  • 调整平衡因子并根据平衡因子的值旋转二叉树

调正平衡因子:

按照二叉搜索树的方法插入后,需要调整平衡因子。平衡因子的计算方法为右子树的高度减左子树的高度。插入节点后一定会影响该节点父节点的平衡因子,可能会影响祖先节点的平衡因子,如果该节点在父节点的左侧插入,则父节点的平衡因子减1,如果在右侧,父节点的平衡因子加1,计算出父节点的平衡因子后分为以下三种情况:

  • 如果新插入节点后父节点的平衡因子为0,则说明之前父节点之前某侧有一个节点,在另一侧新插入了一个节点。父节点外这棵树的高度并未改变,所以调整到此处即可。
  • 如果新插入节点后父节点的平衡因子为1或-1,则说明父节点之前没有节点,此时在下方插入了一个节点,父节点的这棵树高度发生变化,影响了祖父的平衡因子,所以要继续向上调整。把父节点看成子节点,把祖父节点看成父节点,重复该过程即可。
  • 如果新插入节点后父节点的平衡因子为2或-2,则说明父节点的平衡因子已经违反了AVL树的性质,需要通过旋转来调整。
//调整平衡因子
		while (cur != _root)
		{
			if (parent->_left = cur)
			{
				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)
			{
				//旋转
			}
			else
			{
				//在插入之前二叉树已经不平衡了
				return false;
			}
		}

 4、AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,
使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

(1)左左插入:右单旋

新节点插在较高左子树的左侧时要使用右旋转。

AVL树和红黑树_第2张图片

在旋转后要同时满足搜索二叉树和平衡因子不大于一的性质,以图中为例,我们要把父节点右侧向下压,看作左侧节点的右子树,然后把左侧节点原本的右子树变成父节点的左子树。旋转完成后更新平衡因子即可。在这个过程中可能遇到两种特殊情况:

  • .30节点的右孩子可能存在,也可能不存在
  •  60可能是根节点,也可能是子树。 如果是根节点,旋转完成后,要更新根节点。如果是子树,可能是某个节点的左子树,也可能是右子树。

旋转完毕后要更新平衡因子,图中只有60和30两个节点的平衡因子发生了变化,且都变为了0。

代码如下: 

void rotateR(node* parent)
	{
		node* subL = parent->_left;
		node* subLR = subL->_right;
		//保存父节点的父节点
		node* parentparent = parent->_parent;
		
		//判断左节点的右节点是否为空
		if (subLR)
		{
			subLR->_parent = parent;
		}
		parent->_left = subLR;
		subL->_right = parent;
		parent->_parent = subL;
		//判断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)右右插入:左单旋

新节点插在较高右子树的右侧时要使用左旋转。

AVL树和红黑树_第3张图片

实现及情况与右单旋相似。

(3)左右插入:先左单旋再右单旋(左右双旋)

新节点插在较高左子树的右侧,先对30进行左单旋,再对90进行右单旋,旋转完后再考虑平衡因子的更新。

AVL树和红黑树_第4张图片


平衡因子的调整可以根据插入节点位置的不同分为三种情况,我们要再插入后提前记录好subLR平衡因子的值,来方便判断是哪种情况:

  • 节点插入到b下面,subLR的平衡因子值为-1。
  • 节点插入到c下面,subLR的平衡因子值为1。
  • 插入的节点为subLR本身,subLR的平衡因子值为0。

旋转后60的平衡因子为0,30和90的平衡因子根据以上三种情况的不同做出调整。

代码如下:

void rotateLR(node* parent)
	{
		node* subL = parent->_left;
		node* subLR = subL->_right;
		//提前把平衡因子存起来
		int bf = subLR->_bf;
		rotateL(subL);
		rotateR(parent);
        //判断情况调整平衡因子
		if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

(4)右左插入:先右单旋再左单旋(右左双旋)

新节点插在了较高右子树的左侧,使用右左双旋,具体实现方法参考左右双旋。

AVL树和红黑树_第5张图片

 5、AVL树的删除

AVL树的删除可以参考二叉搜素树的删除,删除完毕后更新平衡因子,再根据平衡因子的值做出调成。

AVL树的性能:

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

二、红黑树 

1、红黑树的概念

概念:红黑树也是一种二叉搜索树,在二叉搜索树的基础上,给每个节点分配了一个颜色:红色或黑色。根据颜色的不同,对红黑树的排列进行限制,保证红黑树任何一条由根到叶子节点的路径不会比其它路径长出两倍,进而达到近似平衡。

AVL树和红黑树_第6张图片

性质: ​​​​​​​ 一棵红黑树需要具备以下性质,换句话说只有满足了以下性质,才能保证最长路径不超过最短路径的两倍。

  • (1)每个节点不是红色就是黑色
  • (2)根节点一定是黑色的
  • (3)红色节点的两个叶子节点必须是黑色,换句话说,在一条路径下红色节点不可以相邻。
  • (4)任意节点到叶节点的几条路径中黑色节点的个数必须相等。
  • (5)空节点(空指针)看成是黑色的。

思考一下,为什么具备了以上性质,就可以保证最长路径不超过最短路径的两倍?

根据性质(4),任意节点到叶节点的几条路径中黑色节点的个数必须相等,假设一棵红黑树中只有黑色节点,那这棵树必然是满二叉树。再根据性质(3),两个红色节点不能相邻,那么红色节点只能插入到两个黑色节点之间。现在存在一个黑色满二叉树,假设高度为N,我们向里面随机插入一些红色节点。以根节点为例,最短路径一定是全为黑色节点的那条路径,长度为N,而最长路径一定是红黑相间的那条路径,长度为2N,其它路径的长度都在N和2N之间,不会超过最短路径的两倍。

2、红黑树节点的定义

在定义节点时候,我们一般默认把节点定义成红色。因为定义成黑色的话在插入过程中又可能打破性质(4),而定义成红色在插入时有可能打破了性质(3),显然打破性质(3)比打破性质(4)更加容易调整。

enum Color
{
	RED,
	BLACK
};

template
class RBTreeNode
{
public:
	RBTreeNode(pair& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)
		,_kv(kv)
	{}
private:
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;
	Color _col;
	pair _kv;
};

3、红黑树的插入操作 

插入操作分为两步:

  • 第一步是按照搜索二叉树的方法进行插入
  • 第二步是检查有没有破坏平衡二叉树的性质,如果破坏了要做出相应调整。插入新节点默认为红色,如果检查到新节点的父节点为黑色则不需要调整,如果父节点为红色,则违反了性质(3),需要根据情况做出调整。

调整过程:

约定:cur为当前节点,p为父节点,g为祖父节点,u为uncle节点。且cur默认为红,既然需要调整,那p肯定也为宏,g一定为黑。所以情况的不同关键取决于叔叔节点。

情况一:uncle存在且为红

AVL树和红黑树_第7张图片

 解决方法:将p和u改为黑,将g改为红,然后把g改成cur,继续向上调整。要注意,这里绝对不能仅仅简单的把p改成黑,这样会使各路径上的黑色节点不相等,违反性质(4),所以必须要对g和u做出调整。

情况二:uncle不存在或为黑,cur与p在同一侧

AVL树和红黑树_第8张图片

  • 如果uncle不存在,则cur一定是新插入节点,如果不是新插入的,那么cur为黑,不满足每条路径上黑色节点个数相同。
  • 如果uncle存在,则cur一定不是新插入节点,如果cur是新插入的,那么c一定不存在,显然g的右侧黑色节点个数大于左侧。所以cur变红色的原因一定是cur的子树在调整过程中把它由黑色变成了红色。

解决办法:根据p和cur在左侧还是右侧,以p为轴,进行右旋或左旋。然后p、g变色--p变黑,g变红。

情况二:uncle不存在或为黑,cur与p在两侧

AVL树和红黑树_第9张图片

解决办法:p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,p为g的右孩子,cur为p的左孩子,则针对p做右单旋转。则转换成了情况2。按照情况2旋转后进行颜色改变。

我们也可以尝试一下手撕红黑树的感觉,方便以后和同学吹牛,下面模拟实现红黑树的插入操作,代码如下:

pair insert(pair kv)
	{
		//插入操作
		if (nullptr == _root)
		{
			_root = new node(kv);
			_root->_col = BLACK;
			return make_pair(_root, true);
		}

		node* cur = _root;
		node* parent = cur;
		while (cur)
		{
			parent = cur;
			if (kv.second < cur->_kv.second)
			{
				cur = cur->_left;
			}
			else if (kv.second > cur->_kv.second)
			{
				cur = cur->_right;
			}
			else
			{
				return make_pair(cur, false);
			}
		}
		node* newnode = new node(kv);
		if (kv.second < parent->_kv.second)
		{
			parent->_left = newnode;
			newnode->_parent = parent;
		}
		else
		{
			parent->_right = newnode;
			newnode->_parent = parent;
		}
		cur = newnode;
		//开始调整,如果父亲存在,并且父亲颜色为红,则需要调整。
		//并且在这种情况下,祖父是一定存在的,因为根节点为黑色,父亲一定不是根节点。
        while (parent&&parent->_col==RED)
		{
			node* grandfather = parent->_parent;

			//分情况讨论,看看叔叔在左还是在右
			if (grandfather->_left == parent)
			{
				node* uncle = grandfather->_right;
				//情况1,叔叔存在并且为红色
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
                //情况2加3,叔叔不存在或者叔叔存在且为黑
				else
				{
					//parent和cur在同一侧
					if (parent->_left == cur)
					{
						rotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					//parent和cur在两侧
					else
					{
						rotateL(parent);
						rotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
                }
			}
            else
			{
				node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
                else
				{
					if (cur == parent->_right)
					{
						rotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}

					else
					{
						rotateR(parent);
						rotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return make_pair(newnode,true);
	}

三、红黑树与AVL树比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。map和set的底层都是使用红黑树实现的。​​​​​​​


 

总结

以上就是本文的全部内容,通过对AVL树和红黑树的学习,我们可以对map和set容器有更加深刻的认识。今天的内容就到这里啦,如果觉得博主写的不错的话可以点赞支持一下,江湖路远,来日方长,我们下次见~

你可能感兴趣的:(数据结构与算法,linux,操作系统)