AVL树的插入与删除

Posted GuityCrown

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AVL树的插入与删除相关的知识,希望对你有一定的参考价值。

AVL树是一种高度平衡的二叉搜索树,其每一个结点的左树高和右树高相差不大于1。这个性质使得AVL树的搜索效率要比普通的二叉搜索树要高,因为对于一组递增的数组,其构成的二叉搜索树会是一个链表,搜索时间复杂度自然就是O(n),而其构成的AVL树则肯定是一个搜索效率为O(lg(n))的二叉树。也正因为此,为了保持其平衡的性质,AVL树的插入和删除要比普通二叉搜索树要复杂。

二叉搜索树可以参考二叉搜索树的插入与删除

1. 通过旋转保持AVL树的平衡

旋转分为左旋和右旋两种,这两种操作是对称的,如下图所示。

在插入或者删除结点后,AVL树保持平衡的方法是旋转。对于下图中一颗左子树要比右子树高大1的树来说,如果在其左子树中插入一个结点,则会引起该树不平衡。此时,可通过一次或两次旋转使树平衡。


当结点被插入到b结点的左子树下,只需要对树a进行一次右旋即可,如下图所示,旋转后的树a左子树和右子树高会达到平衡。

当结点被插入到b结点的右子树下,则需要先对树b进行一次左旋转变成上一种情况,再对树a进行一次右旋,如下图所示。

对树b进行左旋后,树b的左子树比右子树高1,即转变为上一种情况(将结点插入到树b的左子树中),然后再对树a进行一次右旋即可将树调整回平衡状态。为什么要对树b进行一次左旋呢?假设我们部队树b左旋,直接对树a右旋,得到的树会是如下图所示。

旋转后的树并没有变得平衡!原因很简单,因为右旋树a的时候,树b的右子树被转移到树a上了,这导致旋转前后导致树不平衡的这颗子树的高没有变化,树依然不平衡,因此我们需要先通过对树b进行一次左旋将树b变成一颗左子树比右子树高的树。

对于结点a的右子树比左子树高,然后结点被插入到结点a的右子树中的情况则是与上述情况完全对称。

下面是AVL树左旋和右旋的代码。

void AVLLeftRotate(Tree *tree, Node *x)
{
	leftRotate(tree, x);
	x->height = max(getHeight(x->left), getHeight(x->right)) + 1;
	x->parent->height = x->height + 1;
	if (x->parent->parent != NULL)
	{
		x->parent->parent->height = max(getHeight(x->parent->parent->left), getHeight(x->parent->parent->right)) + 1;
	}
}

void AVLRightRotate(Tree *tree, Node *x)
{
	rightRotate(tree, x);
	x->height = max(getHeight(x->left), getHeight(x->right)) + 1;
	x->parent->height = x->height + 1;
	if (x->parent->parent != NULL)
	{
		x->parent->parent->height = max(getHeight(x->parent->parent->left), getHeight(x->parent->parent->right)) + 1;
	}
}

void leftRotate(Tree *tree, Node *x)
{
	Node *y = x->right;

	x->right = y->left;
	if (y->left != tree->nil && y->left != NULL)
	{
		y->left->parent = x;
	}

	y->parent = x->parent;
	if (x->parent == tree->nil || x->parent == NULL)
	{
		tree->root = y;
	}
	else if (x == x->parent->left)
	{
		x->parent->left = y;
	}
	else
	{
		x->parent->right = y;
	}

	y->left = x;
	x->parent = y;

	y->size = x->size;
	x->size -= ((y->right != tree->nil && y->right != NULL) ? y->right->size : 0) + 1;
}

void rightRotate(Tree *tree, Node *x)
{
	Node *y = x->left;

	x->left = y->right;
	if (y->right != tree->nil && y->right != NULL)
	{
		y->right->parent = x;
	}

	y->parent = x->parent;
	if (x->parent == tree->nil || x->parent == NULL)
	{
		tree->root = y;
	}
	else if (x == x->parent->left)
	{
		x->parent->left = y;
	}
	else
	{
		x->parent->right = y;
	}

	y->right = x;
	x->parent = y;

	y->size = x->size;
	x->size -= ((y->left != tree->nil && y->left != NULL) ? y->left->size : 0) + 1;
}

上述代码中,结点的size属性记录该结点的子树中一共有多少个结点,height属性为该结点的树高。

2. 插入新结点

插入结点后的AVL树可能会不平衡,我们需要用旋转来保持树的平衡。程序的工作流程是:

1). 和普通的二叉搜索树一样,将新结点z插入到合适的位置作为叶子结点;

2). 从结点z出发向上回溯,如果z的父结点的父结点zpp不为空,则判断zpp是否平衡,即zpp的左子树和右子树的高相差是否不大于1,如果平衡则从结点z的父结点zp继续往上回溯,否则则通过旋转调整zpp的平衡;

3). 旋转zpp的方法是,加入zpp的左子树zl比右子树zr高2,而且zl的左子树比右子树高,则只需要对zpp进行一次右旋即可,如果zl的左子树比右子树低,则要先对zl进行一次左旋,再对zpp右旋。zpp的左子树比右子树低2的情况对称处理即可。

得到某一结点的树高的方法如下。

int getHeight(Node *node)
{
	if (node == NULL)	return -1;
	return node->height;
}

插入新节点的方法和普通的二叉搜索树插入结点的方法一样,代码如下。

void insertAVLTree(Tree *tree, int value)
{
	Node *node = createAVLNode(value);

	if (tree->root == NULL)
	{
		tree->root = node;
		return;
	}

	Node *n = tree->root;
	while (1)
	{
		n->size++;
		if (value < n->value)
		{
			if (n->left == NULL)
			{
				n->left = node;
				node->parent = n;
				break;
			}
			n = n->left;
		}
		else
		{
			if (n->right == NULL)
			{
				n->right = node;
				node->parent = n;
				break;
			}
			n = n->right;
		}
	}

	AVLInsertFixup(tree, node);
}

上述代码的关键在于在插入结点完成后,需要从新插入的结点node开始向上回溯调整树的平衡。AVLInsertFixUp方法的代码如下。

// 从插入的新节点开始向上更新树高并通过旋转结点保持平衡
void AVLInsertFixup(Tree *tree, Node *z)
{
	Node *tmp = z;

	while (z != NULL)
	{
		z->height = max(getHeight(z->left), getHeight(z->right)) + 1;

		if (z->parent != NULL && z->parent->parent != NULL)
		{
			Node *zp = z->parent;
			Node *zpp = zp->parent;
			zp->height = max(getHeight(zp->left), getHeight(zp->right)) + 1;
			zpp->height = max(getHeight(zpp->left), getHeight(zpp->right)) + 1;

			if (getHeight(zpp->left) - getHeight(zpp->right) == -2)
			{
				// 左旋zpp之前确保zp的右树高大于等于左树高
				if (getHeight(zp->left) > getHeight(zp->right))
				{
					AVLRightRotate(tree, zp);
				}
				AVLLeftRotate(tree, zpp);
			}
			else if (getHeight(zpp->left) - getHeight(zpp->right) == 2)
			{
				// 右旋zpp之前确保zp的左树高大于等于右树高
				if (getHeight(zp->left) < getHeight(zp->right))
				{
					AVLLeftRotate(tree, zp);
				}
				AVLRightRotate(tree, zpp);
			}
		}
		z = z->parent;
	}
}

上述代码在向上回溯的同时,一边按需调整树的平衡,一边更新各个结点的高度。

3. 删除结点

删除结点可能会导致该结点所在的树高发生变化,从而引起树的不平衡。AVL树删除结点先执行和普通二叉搜索树一样的过程,然后再从最开始因为删除结点而树高发生变化的结点向上回溯调整树高。该操作的关键在于如何确定“最先发生树高变化的结点”。因为普通二叉搜索树的删除结点有3种情况,所以AVL树对“最先发生树高变化的结点”的确定也有3种情况。假设被删除的结点为x

1). x是叶子结点,“最先发生树高变化的结点”为x的父结点;

2). x只有一个孩子,“最先发生树高变化的结点”为x的父结点;

3). x有两个孩子,则“最先发生树高变化的结点”为x的后驱的父结点。

在确定了“最先发生树高变化的结点”之后,就需要从该结点出发向上回溯,与插入结点一样,对沿路经过的结点按需进行旋转。

删除结点以及删除结点后调整的代码如下。下面的代码使用了一个transplant方法通过“移植”树来删除结点。在像普通二叉搜索树一样删除结点后,就执行AVLDeleteFixup方法调整树高以使树达到平衡。

/**
* 将*tree1替换成tree2,若*tree1为根,则直接改变*tree1指向
*/
void transplant(Tree *tree, Node *tree1, Node *tree2)
{
	if (tree1->parent == NULL)
	{
		tree->root = tree2;
		tree2->parent = NULL;
		return;
	}

	if (tree2 != NULL)					tree2->parent = tree1->parent;
	if (tree1 == tree1->parent->left)	tree1->parent->left = tree2;
	else								tree1->parent->right = tree2;
}

void deleteFromAVLTree(Tree *tree, Node *node)
{
	if (node == NULL)	return;

	Node *d = node->parent;

	Node *n = node;
	while (n->parent != NULL)
	{
		n = n->parent;
		n->size--;
	}

	if (node->left == NULL)
	{
		transplant(tree, node, node->right);
	}
	else if (node->right == NULL)
	{
		transplant(tree, node, node->left);
	}
	else
	{
		Tree t;
		t.root = node->right;
		Node *min = minimum(&t);
		Node *end = min->right;
		min->size = node->size - 1;
		if (node == tree->root)	tree->root = min;

		if (node->right != min)
		{
			transplant(tree, min, min->right);
			min->right = node->right;
			node->right->parent = min;
			d = min->parent;
		}
		else
		{
			d = min;
		}
		min->left = node->left;
		node->left->parent = min;
		transplant(tree, node, min);

		n = min->right;
		while (n != end)
		{
			n->size--;
			n = n->left;
		}
	}

	AVLDeleteFixup(tree, d);
	free(node);
}

// 从实际被删除的结点的父结点(因为从该节点开始树高有可能发生变化)出发,调整树高并通过旋转保持平衡
void AVLDeleteFixup(Tree *tree, Node *z)
{
	while (z != NULL)
	{
		z->height = max(getHeight(z->left), getHeight(z->right)) + 1;

		if (getHeight(z->left) - getHeight(z->right) == -2)
		{
			Node *zr = z->right;
			if (getHeight(zr->left) > getHeight(zr->right))
			{
				AVLRightRotate(tree, zr);
			}
			AVLLeftRotate(tree, z);
		}
		else if (getHeight(z->left) - getHeight(z->right) == 2)
		{
			Node *zl = z->left;
			if (getHeight(zl->left) < getHeight(zl->right))
			{
				AVLLeftRotate(tree, zl);
			}
			AVLRightRotate(tree, z);
		}

		z = z->parent;
	}
}

4. AVL树操作的时间复杂度

插入操作需要O(lg(n))的遍历时间以及至多两次旋转的常数时间;

删除操作需要O(lg(n))的遍历时间以及若干次旋转的常数时间;

总的来说,AVL树的插入和删除都只需要O(lg(n))的时间,只是其常系数量不同。

最后,本博客已经介绍的和即将介绍的数据结构与算法的C实现代码都在这里 数据结构与算法

以上是关于AVL树的插入与删除的主要内容,如果未能解决你的问题,请参考以下文章

以AVL树为例理解二叉树的旋转(Rotate)操作

AVL学习笔记

了解数据结构之平衡二叉树 (AVL)-插入和删除

AVL树的插入和删除

红黑树的插入与删除

树:AVL树