红黑树,,

由来

为解决频繁插入 / 删除场景下平衡树的维护效率问题而诞生的自平衡二叉搜索树,核心是用宽松平衡 + 颜色约束减少旋转开销。

和AVL树的区别

AVL树任何一个节点的左右节点的所有子树高度差的绝对值小于等于1,而红黑树要求任何一个节点的最长路径 ≤ 最短路径 × 2(左右子树的高度差不超过二倍)。

如果要经常使用插入和删除操作,那么用红黑树合适,因为他对平衡的定义没那么严格。

如果数据一插入就不再删除了,经常使用查找操作,这时候使用AVL树,因为他对平衡的定义更严格,AVL树最终生成的结果相较于红黑树来说,高度一定小于红黑树,更占有优势,但是查找效率在一个量级上,都是O(logn)。

定义

红黑树是一棵自平衡的二叉搜索树,每个节点带有红 / 黑颜色,并满足以下 5 条性质:

四个基本性质(重点,面试)

  1. 首先要求是一棵二叉搜索树(BST)(口诀:左根右)
  2. 根节点和叶节点必须是黑色(红黑树的叶节点指的是空节点,不是度为0的节点)(口诀:根叶黑)
  3. 要求红色节点的两个孩子必须都是黑色,换句话说,红黑树要求不能出现连续的两个红色节点(口诀:不红红)
  4. 红黑树要求任何一个节点到其叶节点上所有路径上的黑色节点数量相同(口诀:路黑同)

一个核心性质(重点,面试)

  1. 红黑树要求根节点出发的 最长路径 ≤ 最短路径 × 2

最短路径特点

纯黑色,一个红都不要。

最长路径特点

拥有的黑色节点数量一定等于最短路径拥有的,然后再尽可能多的掺入红色节点。而且由于性质二和性质三的限制,掺入的红色节点不能连续,也不能做开头。所以最长路径一定是从黑色节点开始,一黑一红一黑一红的样子。

假设最短路径长度是n,最长路径应该是多少?

答:2n-1

所以最长路径一定不会超过最短路径的二倍。

结构体设计

cpp 复制代码
typedef enum {Red,Black}ColorType;

typedef int ELEMTYPE;

//红黑树的有效节点结构体设计
typedef struct RBNode
{
	ELEMTYPE data;//1.数据域
	struct RBNode* leftchild;//2.左孩子指针
	struct RBNode* rightchild;//3.右孩子指针
	struct RBNode* parent;//4.双亲指针
	ColorType color;//bool color;//5.当前节点的颜色
}RBNode;

//红黑树的辅助接点结构体设计
typedef struct RBTree
{
	RBNode* root;
}RBTree;

工具函数

购买新节点

cpp 复制代码
RBNode* BuyNode()
{
	RBNode*pnewnode= (RBNode*)malloc(1 * sizeof(RBNode));
	if (NULL == pnewnode)
		exit(EXIT_FAILURE);
	memset(pnewnode, 0, sizeof(RBNode));
	return pnewnode;
}

插入的平衡调整

cpp 复制代码
void Insert_Adjust(RBTree* pTree, RBNode* node)
{
	//0.assert
	assert(pTree != NULL);
	//1.先排除node节点没有违反任何性质的情况:其父节点存在,且父节点是黑的
	if (node->parent != NULL && node->color == Black)
		return;
	//2.此时node节点如果违反了性质,只可能违反了性质二(根叶黑)或者性质三(不红红)

	//3.先去判断是否违反了性质2(根叶黑)
	if (node->parent == NULL)
	{
		node->color = Black;
		return;
	}
	//4.如果没有违反性质二,就一定违反了性质三( 不红红)
	//这里先找其 父亲 叔叔 爷爷这三个节点

	RBNode* father = node->parent;
	RBNode* grandfather = node->parent->parent;
	RBNode* uncle = father == grandfather->leftchild ? grandfather->rightchild : grandfather->leftchild;
	//他叔叔存在,且是红色,叔父爷变色,将其爷爷节点当新的节点继续判定
	if (uncle != NULL && uncle->color == Red)
	{
		uncle->color = Black;
		father->color = Black;
		grandfather->color = Red; 

		Insert_Adjust(pTree,grandfather );
		return;
	}
	//5.此时它叔叔一定是黑色(但是其叔叔存不存在不确定)=》根据三个节点(自己,爸爸,爷爷)来判定失衡形态,然后调用对应的旋转策略+变色即可
	if (father == grandfather->leftchild)//L
	{
		if (node == father->leftchild)//LL,单右旋,变色即可(谁绕着谁转)
		{
			RBNode* great_grandfather = grandfather->parent;//有可能为空,太爷赋值一定要在旋转函数之前,因为旋转会改变双亲
			RBNode*ptr= Right_Rotate(grandfather);
			//变色
			father->color = Black;
			grandfather->color = Red;
			Receive_PtrNode(ptr, great_grandfather, pTree);
			return;
		}
		else//LR,先左旋,再右旋+变色(以第二次旋转为基准,来找谁绕着谁转)
		{
			//先变色
			node->color = Black;
			grandfather->color = Red;

			RBNode* great_grandfather = grandfather->parent;//有可能为空,太爷赋值一定要在旋转函数之前,因为旋转会改变双亲
			//先左旋
			grandfather->leftchild= Left_Rotate(father);
			//再右旋
			RBNode*ptr= Right_Rotate(grandfather);
			Receive_PtrNode(ptr, great_grandfather, pTree);
			return;
		}
		
	}
	if (father == grandfather->rightchild)//R
	{
		if (node == father->rightchild)//RR,单左旋,变色即可(谁绕着谁转)
		{
			//变色
			father->color = Black;
			grandfather->color = Red;

			//再旋转
			RBNode* great_grandfather = grandfather->parent;//有可能为空,太爷赋值一定要在旋转函数之前,因为旋转会改变双亲
			RBNode* ptr = Left_Rotate(grandfather);
			
			Receive_PtrNode(ptr, great_grandfather, pTree);
			return;
		}
		else//RL,先右旋,再左旋+变色(以第二次旋转为基准,来找谁绕着谁转)
		{
			//先变色
			node->color = Black;
			grandfather->color = Red;

			RBNode* great_grandfather = grandfather->parent;//有可能为空,太爷赋值一定要在旋转函数之前,因为旋转会改变双亲
			//先右旋
			grandfather->rightchild = Right_Rotate(father);
			//再左旋
			RBNode* ptr = Left_Rotate(grandfather);
			Receive_PtrNode(ptr, great_grandfather, pTree);
			return;
		}
	}
}

void Receive_PtrNode(RBNode* ptr, RBNode* great_grandfather, RBTree* pTree)
{
	if (great_grandfather == NULL)
	{
		pTree->root = ptr;
		ptr = NULL;
		return;
	}
	if (ptr->data < great_grandfather->data)
		great_grandfather->leftchild = ptr;
	else
		great_grandfather->rightchild = ptr;
	ptr->parent = great_grandfather;
	return;
}

删除的平衡调整

cpp 复制代码
void Delete_Adjust2(RBTree* pTree, RBNode* node,bool tag)
{
	//0.assert
	assert(pTree != NULL);
	//0.5先申请两个指针father,child,来指向node节点的父节点和其孩子节点
	RBNode* father = node->parent;//有可能为空
	RBNode*child= node->leftchild != NULL ? node->leftchild : node->rightchild;
	//1.首先观察是不是单分支 = 让其单红色孩子顶替上来,变黑,然后释放待删除节点(注意其自身是不是根节点)
	//写法1:
	/*if (child!=NULL)
	{
		if (father == NULL)
		{
			pTree->root = child;
			child->color = Black;
			node->parent = NULL;
		}
		else
		{
			if (node->data < father->data)
				father->leftchild = child;
			else
				father->rightchild = child;
			child->parent = father;
		}
		free(node);
		return;
	}*/
	//写法2:
	if (tag&&child != NULL)
	{
		node->data = child->data;
		free(child);
		node->leftchild = node->rightchild = NULL;
		return;
	}
	//2.此时能执行到这里待删除节点一定是0分支,再分三种情况:
	//3.情况一:它既是0分支又是根节点=》修改pTree->root,然后释放node
	if (tag && father==NULL)
	{
		pTree->root=NULL;
		free(node);
		return;
	}
	//4.情况二:非根且是红色=》释放直接结束
	if (tag && node->color == Red)
	{
		if (father->leftchild == node)
			father->leftchild = NULL;
		else
			father->rightchild = NULL;
		free(node);
		return;
	}
	//5.情况三:非根且是黑色=》
	//6.后面要用到其兄弟节点,这里先申请一个变量sibling指向其兄弟
	RBNode* sibling = node == father->leftchild ? father->rightchild : father->leftchild;//sibling一定存在,但是颜色不一定
	//7.如果其兄弟节点颜色是黑色
	if (sibling->color == Black)
	{
		//一旦确定待删除结点的兄弟节点是黑色,则后序的步骤和待删除结点的自身就没有什么关系了
		//则此时可以将待删除节点释放掉
		if(tag)
		{
			if (father->leftchild == node)
				father->leftchild = NULL;
			else
				father->rightchild = NULL;
			free(node);
		}
		//7.0临时申请一个指针sibling_redchild
		RBNode* sibling_redchild = NULL;
		if (sibling->leftchild != NULL && sibling->leftchild->color == Red)
		{
			sibling_redchild = sibling->leftchild;
		}
		else if (sibling->rightchild != NULL && sibling->rightchild->color == Red)
		{
			sibling_redchild = sibling->rightchild;
		}
		else
		{
			sibling_redchild = NULL;
		}
		//7.1进一步判断这个黑兄弟是否有红孩
		if (sibling_redchild != NULL)
		{
			//7.2黑兄弟一定有红孩=》通过三个节点来判断型号+对应的旋转+变色
			//判断型号通过待删除结点他爸,他兄弟,他兄弟的红孩。
			//(能判断成LL,RR型就不要判断成LR,RL型)
			//注意事项:如果黑兄弟有两个红孩,咱们sibling_redchild默认指向左边的红孩,所以在判定是RR、还是RL的时候要小心
			
			//后面的四种情况都需要grandfather指向father的双亲
			RBNode* grandfather = father->parent;

			if (father->leftchild == sibling)//L
			{
				if (sibling->leftchild == sibling_redchild)//LL
				{
					//变色
					sibling_redchild->color = sibling->color;//r变s
					sibling->color = father->color;//s变p
					father->color=Black ;//p变黑

					//RBNode* grandfather = father->parent;
					//+单右旋
					RBNode*ptr=Right_Rotate(father);
					Receive_PtrNode(ptr, father->parent, pTree);
					return;
				}
				else//LR(红孩存在的情况下,不是左边,就是一定是单侧的右红孩)
				{
					//先变色(r变p,p变黑)
					sibling_redchild->color = father->color;
					father->color = Black;

					//先左旋
					father->leftchild = Left_Rotate(sibling);

					//RBNode* grandfather = father->parent;
					//再右旋
					RBNode* ptr = Right_Rotate(father);
					Receive_PtrNode(ptr, grandfather, pTree);
					return;
				}
			}
			else//R
			{
				//首先小心黑兄弟节点是双红孩,但是我们的sibling_redchild默认指向左边了
				if (sibling->rightchild != NULL && sibling->rightchild->color == Red)
					sibling_redchild = sibling->rightchild;
				        
				if (sibling_redchild == sibling->rightchild)//RR
				{
					//变色
					sibling_redchild->color = sibling->color;//r变s
					sibling->color = father->color;//s变p
					father->color = Black;//p变黑

					//RBNode* grandfather = father->parent;
					//+单左旋
					RBNode* ptr = Left_Rotate(father);
					Receive_PtrNode(ptr, grandfather, pTree);
					return;
				}
				else//RL型(红孩存在的情况下,不是右边就一定是左边)
				{
					//先变色(r变p,p变黑)
					sibling_redchild->color = father->color;
					father->color = Black;
					 
					//先右旋
					father->rightchild = Right_Rotate(sibling);

					//RBNode* grandfather = father->parent;
					//再左旋
					RBNode* ptr = Left_Rotate(father);
					Receive_PtrNode(ptr, grandfather, pTree);
					return;
				}
			}
			return;
		}
		//7.2如果黑兄弟一个红色孩子都没有,全是黑色孩子(没有孩子也是全是黑色孩子)
		else
		{
			//兄弟变红
			sibling->color = Red;
			//在对其父节点的情况进一步分析
			if (father->parent == NULL)//情况一:是根节点,直接结束
				return;
			else if (father->color == Red)//情况二:父节点非根,且是红色=》变黑结束
			{
				father->color = Black;
				return;
			}
			else//情况三:父节点非根且黑色=>兄弟变红,再对其存在的黑色节点情况进行判断 
			{
				Delete_Adjust2(pTree,father,false);//再加一个参数,用来控制第二个参数指向的节点是否需要销毁或者删除
			}
		}
	}
	//8.如果其兄弟节点颜色是红色
	else
	{
		//父兄变色
		father->color = Red;
		sibling->color = Black;
		//让父节点朝着待删除一侧进行单旋,此时待删除结点就会出现一个新兄弟节点,对这个新兄弟节点进一步判断
		if (father->leftchild == node)
		{
			sibling->leftchild = Left_Rotate(father);
		}
		else
		{
			sibling->rightchild = Right_Rotate(father);
		}
		Delete_Adjust2(pTree, node,true);//再加一个参数,用来控制第二个参数指向的节点是否需要销毁或者删除
	}
}

左旋

cpp 复制代码
RBNode* Left_Rotate(RBNode* node)
{
	RBNode* child = node->rightchild;
	RBNode* grandchild = node->rightchild->leftchild;

	node->rightchild = grandchild;
	if (grandchild != NULL)
		grandchild->parent = node;

	child->leftchild = node;
	node->parent = child;

	return child;
}

右旋

cpp 复制代码
RBNode* Right_Rotate(RBNode* node)
{
	RBNode* child = node->leftchild;
	RBNode* grandchild = node->leftchild->rightchild;

	node->leftchild = grandchild;
	if (grandchild != NULL)
		grandchild->parent = node;

	child->rightchild = node;
	node->parent = child;

	return child;
}

普通函数

初始化

cpp 复制代码
void Init_RBTree(RBTree* pTree)
{
	pTree->root = NULL;
}

插入

插入逻辑

新购买节点默认应该是红色(++红色++ 不破坏"路黑同",仅有可能破坏"不红红",有一定的概率不需要调整;++黑色++破坏"路黑同",一定需要调整。)

例子:按顺序插入:97 58 49 21 75 96

插入97:一开始是空树,所以直接购买节点赋值97,然后让辅助节点抓住,并且变为黑色即可(变黑是因为违反了"不红红")。

插入58:首先让58落位,然后发现其父节点存在且是黑色,则无需调整。

插入49:其父节点存在且是红色,违反了"不红红",此时需要去看其叔叔节点是什么颜色,其叔叔节点不存在,是空节点,也就是黑色。

黑叔叔的调整策略:判定失衡型号,调用对应的旋转+变色

变色规则:把旋转点和旋转中心点进行变色

判定型号是LL型,需要单右旋

插入21:其父节点存在且是红色,违反"不红红",且其叔叔节点是存在的红色节点。

红叔叔的调整策略:叔父爷变色,再将其爷爷节点当成新的判定节点接着处理。

插入75:先让75落位,其父节点存在,且是黑色,无需调整。

插入96:其父节点存在但是是红色,违反"不红红",所以看其叔叔节点的颜色,发现其叔叔不存在,不存在默认是黑色,则执行判定失衡型号+旋转+变色

判断是LR型,先左旋(对三个节点的中间节点说的),再右旋(对三个节点的最上面的节点说的)

再变色,是以第二次旋转为基准,让旋转节点和旋转中心变色。

插入情况总结

  1. 如果插入节点是作为根节点存在,则一定违反"根叶黑"=>跟变黑,结束
  2. 如果插入节点的父节点存在且是红色,立马去观察其叔叔:其叔叔节点是黑色,根据插入节点自身,其父节点,其爷爷节点这三个节点判定失衡型号,然后调用对应的旋转+变色(如果是双旋,以第二次旋转为基准来找旋转点和旋转中心点进行变色);其叔叔节点是红色,叔父爷变色,再将其爷爷节点当成新的判定节点接着判定。
cpp 复制代码
bool Insert_RB(RBTree* pTree, ELEMTYPE val)
{
	//0.assert
	assert(pTree != NULL);
	//0.5先排除其本身是棵空树的情况
	if (pTree->root == NULL)
	{
		pTree->root = BuyNode();
		pTree->root->data = val;
		pTree->root->color = Black;

		return true;
	}

	//1.从根节点出发,逐个去判断,当前节点是否存在
	RBNode* p = pTree->root;
	RBNode* pp = NULL;
	//2.如果存在,则判断其值是否等于待插入的val,如果等于说明里面已经有了val值
	while (p != NULL && p->data != val)
	{
		pp = p;
		if (val < p->data)
		{
			p = p->leftchild;
		}
		else
		{
			p = p->rightchild;
		}
	}
	if (p != NULL && p->data == val)
	{
		return true;
	}

	//3.如果不相等,则去判断val值节点应该插入到当前节点的哪一边
	//让指针p向下移动一层

	//4.如果p遇到了NULL,说明val值应该插入在当前这个位置
	RBNode* pnewnode = BuyNode();
	pnewnode->data = val;
	if (val < pp->data)
	{
		pp->leftchild = pnewnode;
	}
	else
	{
		pp->rightchild = pnewnode;
	}
	pnewnode->parent = pp;
	//5.插入完成之后,直接调用插入平衡函数,判断是否失衡
	Insert_Adjust(pTree, pnewnode);

	return true;          
}

删除

删除操作:

1.查找val值是否存在,如果存在则观察是几分支

2.如果是0分支:直接删除

3.如果是1分支:孩子顶替上来

4.如果是2分支:狸猫换太子(转换为删除0分支或者1分支)

红黑树中,如果一个节点是单分支,则一定是单一个孩子,并且它两的颜色一定是一黑一红。

例子:

删除第一个值:18

首先18存在,且是双分支,狸猫换太子,找其后继节点,可用23来顶替待删除节点,删除18,25顶上去,25红变黑。

删除第二个值:25

首先25存在,且是0分支,先观察它的颜色,如果该节点是红色,直接删除无需调整;如果该叶子节点是黑色,此时应该去观察其兄弟节点情况,发现其兄弟节点是34,是一个存在且是黑色的节点,此时需要去观察其黑兄弟是否有红色孩子,然后进行型号判定,调用对应的旋转+变色。(三个节点:待删除结点,它兄弟,它兄弟的红孩)RR型=>单左旋,再变色。

变色规则(s=sibling r=redchild p=parent=father ):

LL/RR:r变s,s变p,p变黑。

LR/RL:r变p,p变黑。

删除第三个值:15

15存在,且是双分支,则里狸猫换太子,可以找直接后继(17),继而转化为去删除17,发现17为叶子节点且是黑色,观察其兄弟节点(34),其兄弟存在且是红色,父兄变色,然后父节点朝着待删除结点一侧进行单旋。此时,其待删除结点就会出现一个新的兄弟节点,此时,根据新的兄弟节点情况进行判定。其新的兄弟节点是黑色(27),且没有红孩=>兄弟变红,紧接着对其父节点进行判定,其父节点只会出现三种情况:

1.父节点是根节点=>直接结束

2.父节点是非根红色节点=>变黑,直接结束

3.父节点是非根黑色节点=>对其兄弟重复上面的判断逻辑

总结:

待删除结点找到后,首先去确定是2分支、1分支还是0分支。

一旦确定是双分支,则狸猫换太子,转换为删除单分支或者零分支。

如果是单分支(单一个孩子)=>让这个孩子顶替上来,然后变黑

如果是叶节点(0分支)=>1. 首先排除根节点=>直接释放,然后修改辅助接点pTree->root为NULL

2.是非根红节点=>直接删除,无需调整

3.是非根黑节点=>(1)它兄弟节点是红色=>父兄变色,然后父朝着到待删除一侧单旋,此时待删除结点就会出现一个新的兄弟节点,然后再对这个新兄弟判定即可。

(2)它兄弟节点是黑色=>①其黑兄弟全是黑色孩子=>兄弟变红,然后对其父节点接着判定(其父节点只可能有三种情况:1.父节点是根节点=>直接结束。2.父节点是非根红色节点=>变黑,直接结束。3.父节点是非根黑色节点=>对其兄弟重复上面的判断逻辑)

②其黑兄弟有红色孩子=>型号判定,调用对应的旋转+变色即可(能单旋则不双旋)

cpp 复制代码
bool Delete_RB(RBTree* pTree, ELEMTYPE val)
{
	//0.assert
	assert(pTree != NULL);
	//1.去查找val值是否存在
	RBNode*p = Search_RB(pTree->root, val);
	//2.如果存在的话,首先看是不是双分支,如果是,则找其直接前驱或者直接后继
	if (NULL == p)
		return true;
	if (p->leftchild != NULL && p->rightchild != NULL)
	{
		RBNode* cat = p->rightchild;
		while (cat->leftchild != NULL)
			cat = cat->leftchild;
		p->data = cat->data;
		p = cat;
	}
	//3.执行到这里,此时要删除的节点一定是单分支或0分支,则直接调用删除调整函数即可
	Delete_Adjust(pTree, p);
	return true;
}

查找(递归和非递归)

cpp 复制代码
//递归
RBNode* Search_RB(RBNode* root, ELEMTYPE val)
{
	if (root == NULL)
		return NULL;
	if(val<root->data)
		return Search_RB(root->leftchild, val);
	else if (val > root->data)
		return Search_RB(root->rightchild, val);
	else
		return root;
}
//非递归
RBNode* Search_RB1(RBNode* root, ELEMTYPE val)
{
	assert(root != NULL);
	RBNode* p = root; 
	while (p->data != val && p != NULL)
	{
		if (val < p->data)
		{
			p = p->leftchild;
		}
		else
		{
			p = p->rightchild;
		}
	}
	return p;//没找到p就是空,找到了是val的地址
}

打印

cpp 复制代码
#include<stack>
//5.打印(中序)
void Show_inOrder(RBNode* root)
{
	std::stack<RBNode*>st;
	st.push(root);
	bool tag = true;
	while (!st.empty())
	{
		while (tag && st.top()->leftchild != NULL)
			st.push(st.top()->leftchild);

		RBNode* tmp = st.top();
		st.pop();
		printf("%d ", tmp->data);

		if (tmp->rightchild != NULL)
		{
			st.push(tmp->rightchild);
			tag = true;
		}
		else
			tag = false;
	}
}

让最终旋转之后的返回值ptr,被正常的接收

cpp 复制代码
void Receive_PtrNode(RBNode* ptr, RBNode* great_grandfather, RBTree* pTree)
{
	if (great_grandfather == NULL)
	{
		pTree->root = ptr;
		ptr = NULL;
		return;
	}
	if (ptr->data < great_grandfather->data)
		great_grandfather->leftchild = ptr;
	else
		great_grandfather->rightchild = ptr;
	ptr->parent = great_grandfather;
	return;
}
相关推荐
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-队列+宽搜》--70.N叉树的层序遍历,71.二叉树的锯齿形层序遍历,72.二叉树的最大宽度,73.在每个树行中找最大值
数据结构·c++·算法·队列
汀、人工智能2 小时前
[特殊字符] 第98课:数据流中位数
数据结构·算法·数据库架构··数据流·数据流中位数
努力努力再努力wz2 小时前
【C++高阶系列】告别内查找局限:基于磁盘 I/O 视角的 B 树深度剖析与 C++ 泛型实现!(附B树实现源码)
java·linux·开发语言·数据结构·c++·b树·算法
承渊政道2 小时前
【优选算法】(实战攻坚BFS之FloodFill、最短路径问题、多源BFS以及解决拓扑排序)
数据结构·c++·笔记·学习·算法·leetcode·宽度优先
郝学胜-神的一滴2 小时前
二叉树后序遍历:从递归到非递归的优雅实现
数据结构·c++·程序人生·算法·
汀、人工智能2 小时前
[特殊字符] 第71课:爬楼梯
数据结构·算法·数据库架构·图论·bfs·爬楼梯
海清河晏1116 小时前
数据结构 | 单循环链表
数据结构·算法·链表
skywalker_1111 小时前
力扣hot100-3(最长连续序列),4(移动零)
数据结构·算法·leetcode
_日拱一卒12 小时前
LeetCode:除了自身以外数组的乘积
数据结构·算法·leetcode