【算法导论】 二叉搜索树、AVL树、和红黑树

二叉搜索树

二叉搜索树是一颗二叉树或一颗空树且满足以下性质:
1)根节点 x的key值大于任意左子树上节点的key值,小于右子树上任意节点的key值 ;
2)其左右子树也分别是一颗二叉搜索树。

    使用二叉搜索树进行查找时间复杂度为O( h ),且 n ≥ h ≥ log(n+1);那么时间复杂度上限为O(n)、下限为Ω(log n),且 h 趋于 n 的情况远远小于趋于 log n 的情况,那么渐进时间复杂度为Θ(log n) .

树的插入只需依次向下进行比较,直到走到叶子结点为止,插入到叶子节点之后。

删除节点(分下面两种情况):

  1. 若该节点左子树或右子树为空,用其右子树或左子树替换此节点位置;
  2. 该节点左右子树均不为空,用其后继替换该节点位置,后继的右子树替换后继的位置。
/**
 * zhanw15 @2018/4/12
 * 
 * 二叉搜索树
 * 算法导论 (第三版)  @P286
 * @Operation = { Search, Min, Max, Delete, Insert,  PredecessOR, SuccessOR};
 */ 
#include 
#include

typedef struct binaryTree
{
	int key;
	binaryTree *p;		  // parent
	binaryTree *left;	  // left_son
	binaryTree *right;	  // right_son
}binaryTree;

/* 访问节点 */ 
void visit( binaryTree *a)
{
	printf( "%d  ", a->key);
}

/* 中序遍历 时间复杂度 n */ 
void Inorder_Tree( binaryTree *a)
{
	if( a==NULL) return;
	
	Inorder_Tree( a->left);
	visit( a);
	Inorder_Tree( a->right);
}

/* 查找二叉搜索树 迭代版本*/
binaryTree *Tree_Search( binaryTree *a, int k)
{
	while( a!=NULL && a->key!=k) {
		if( a->keyright;
		else
			a = a->left;
	}
	return a;
} 

/* 获取二叉搜索树最小值节点 */
binaryTree *Tree_Min_Key( binaryTree *a)
{
	if( a==NULL) return a;      // 空二叉树
	//if( a->right==NULL) return a;
	//return a->right;
	//滥用递归不是个好习惯
	while( a->left!=NULL) a = a->left; 
	return a;
}

/* 获取二叉搜索树的最大值节点 */ 
binaryTree *Tree_Max_Key( binaryTree *a)
{
	if( a==NULL) return a;
	while( a->right!=NULL) a = a->right;
	return a;
}

// 在某一遍历次序下,得到最后的遍历结果,若x1、x2相邻,
// 且x1在x2前,则称x1是x2的前驱,x2是x1的后继 
 
/* 中序遍历下二叉搜索树的前驱 */
binaryTree *PredecessOR( binaryTree *a)
{
	if( a->left!=NULL) return Tree_Max_Key( a->left);
	
	while( a->p!=NULL && a->p->left==a) {
		a = a->p;
	}
	return a->p;
}

/* 中序遍历下二叉搜索树的后继 */
binaryTree *SuccessOR( binaryTree *a)
{
	if( a->right!=NULL) return Tree_Min_Key( a->right);
	
	while( a->p!=NULL && a->p->right==a) {
		a = a->p;
	}
	return a->p;
} 

/* 插入节点 时间复杂度: h(树高度) */
binaryTree *Tree_Insert( binaryTree *Head, binaryTree *a)
{
	if( a==NULL) return Head;
	
	//找到插入节点的位置 
	binaryTree *t = Head, *temp = Head;
	while( t!=NULL) {
		temp = t;
		if( t->key>a->key) t = t->left;
		else t = t->right;
	}
	
	//插入节点 
	if( temp==NULL) Head = a;
	else if( a->key > temp->key) {
		temp->right = a;
	}
	else {
		temp->left = a;
	}
	a->p = temp;
	
	return Head; 
} 

/* 将u位置子树替换为v子树 */ 
binaryTree *Transplant( binaryTree *Head, binaryTree *u, binaryTree *v)
{
	
	if( u->p==NULL) Head = v;
	else {
		if( u==u->p->left)
			u->p->left = v;
		else
			u->p->right = v;
	}
	if( v!=NULL) v->p = u->p;
	
	return Head;
}

/* 删除节点 时间复杂度: h*/ 
binaryTree *Tree_Delete( binaryTree *Head, binaryTree *a)
{
	if( a==NULL) return Head;
	
	//若节点左或右无子树,则拿右或左子树替换掉当前节点 
	if( a->left==NULL)	Head = Transplant( Head, a, a->right);
	else if( a->right==NULL) Head = Transplant( Head, a, a->left);
	else { 
		// 若左右子树均存在,则使用其后继替换此节点 
		binaryTree *y = Tree_Min_Key( a->right);
		/** 
		 *  若后继节点不是其右孩子,那么后继节点一定是其右子树的最小值,\
		 *  且其后继一定没有左子树(参看后继定义) 
		 */ 
		if( y->p!=a) {
			Head = Transplant( Head, y, y->right);
			y->right = a->right;
			y->right->p = y;
		}
		Head = Transplant( Head, a, y);
		y->left = a->left;
		y->left->p = y;
	}
	free( a);
	return Head;
}

int main()
{
	int a[17] = { 14, 32, 43, 4, 23, 7, 64, 13, \
	90, 70, 12, 24, 1, -4, 48, 22, 65};
	
	binaryTree *Head=NULL;
	
	for( int i=0; i<17; i++) {      //插入元素 
		binaryTree *temp = new binaryTree;
		(*temp) = { a[i], NULL, NULL, NULL};
		
		Head = Tree_Insert( Head, temp);
	}
	
	Inorder_Tree( Head);
	
	int k = 7;             //测试:查找、前驱、后继 
	printf( "\nkey = %d  Suce: %d  Pred: %d\n", k, SuccessOR\
	( Tree_Search( Head, k))->key, PredecessOR( Tree_Search( Head, k))->key);
	
	int k2 = 4;           //测试: 删除值为4的节点 
	Inorder_Tree( Head = Tree_Delete( Head, Tree_Search( Head, k2)));
	
	return 0;
}

// output: (mingw gcc 4.7.2 32-bit)
// -4  1  4  7  12  13  14  22  23  24  32  43  48  64  65  70  90
// key = 7  Suce: 12  Pred: 4
// -4  1  7  12  13  14  22  23  24  32  43  48  64  65  70  90


平衡二叉树  AVL树

    二叉搜索树可能会产生一些极端情况,比如传进来的是已经排序好的数列,那么建立的二叉搜索树就变成了类似的一个单向链表。这样查找的时间复杂度为O(h),即O(n)。

    面对这种情况,建立好二叉树之后,以后每次查找时间复杂度都为O(n),大大降低了时间效率。为此我们可以对这种情况做一下改进,每次插入节点时,将树调整到足够平衡,这样无论多少次插入节点这棵树将保持 h = O(log n)。下面引入平衡因子概念:

平衡因子:某节点左子树的高度与右子树的高度差 为 该节点的平衡因子。
    AVL树要求所有节点的平衡因子为1,-1 或 0 . 当插入或删除节点导致平衡因子不为±1或0时,则进行调整。

调整分下面四种情况(盗图@PulsPuls1):

LL型(右单旋):

LL型

RR型(左单旋):同LL型,只不过方向相反。

LR型(先左旋,后右旋):

LR型

RL型(先右旋,后左旋):同LR型,只不过先进行右旋,再进行左旋。


AVL树除插入和删除外其他操作同平衡二叉树,插入和删除时先确定位置,然后插入或删除完毕后回溯确定节点的平衡因子,并根据以上四种情况进行调整。

/**
 * zhanw15 @2018/5/26
 *
 * AVL树:插入、删除、旋转
 */ 
#include 
#include 

/**
 *
 * 结构体内加入父节点后发现代码有些繁琐,
 * 不利于阅读, 所以删去了指向父节点的指针;
 *
 * 若要加入父节点指针, 则左单旋转与右单旋转时\
 * 注意调整父指针, 插入和删除时也应如此
 *
 */
typedef struct AVLTree
{
	int key;
	int h;
	AVLTree *left;	  // left_son
	AVLTree *right;	  // right_son
}AVLTree;

/** 以 a 为树根 get AVLTree 高度 */
int getHigh( AVLTree *a) {
	return (a==NULL)?0:a->h;
}

/** 重新调整 a 为树根 树高度 */
int adjustHigh( AVLTree *a)
{
	if( a==NULL) return 0;

	int lh = getHigh( a->left);    // 左子树高度
	int rh = getHigh( a->right);   // 右子树高度
	
	return lh>rh?lh:rh +1;
}

/** 调整LL型, 进行右单旋 */
AVLTree *LL_Adjust( AVLTree *Head)
{
	if( Head==NULL || Head->left==NULL) return Head;
	AVLTree *temp = Head->left;    // 指向调整节点左孩子
	
	// 调整左右孩子指针
	Head->left = temp->right;
	temp->right = Head;
	
	// 重新计算树的高度
	Head->h = adjustHigh( Head);
	temp->h = adjustHigh( temp);
	
	return temp;
}

/** 调整RR型, 进行左单旋 */
AVLTree *RR_Adjust( AVLTree *Head)
{
	if( Head==NULL || Head->right==NULL) return Head;
	AVLTree *temp = Head->right;    // 指向调整节点左孩子
	
	Head->right = temp->left;
	temp->left = Head;
	
	Head->h = adjustHigh( Head);
	temp->h = adjustHigh( temp);
	
	return temp;
}

/** 调整LR型, 先左旋, 后右旋 */
AVLTree *LR_Adjust( AVLTree *Head)
{
	Head->left = RR_Adjust( Head->left);
	return LL_Adjust( Head);
}

/** 调整RL型, 先右旋, 后左旋 */
AVLTree *RL_Adjust( AVLTree *Head)
{
	Head->right = LL_Adjust( Head->right);
	return RR_Adjust( Head);
}

/** 根据根节点调整其为一颗AVL树 */
AVLTree *AVLTreeAdjust( AVLTree *a)
{
	if( a==NULL) return a;
	
	if( getHigh(a->left) - getHigh( a->right) ==2) {
		if( getHigh(a->left->right) > getHigh(a->left->left)) {
			a = LR_Adjust( a);
		}else {
			a = LL_Adjust( a);
		}
	}
	
	if( getHigh(a->right) - getHigh( a->left) ==2)
	{
		if( getHigh(a->right->left) > getHigh(a->right->right)) {
			a = RL_Adjust( a);
		}else {
			a = RR_Adjust( a);
		}
	}
	
	a->h = adjustHigh( a);
	return a;
}

/** 插入节点 时间复杂度: h(树高度) */
AVLTree *Tree_Insert( AVLTree *Head, AVLTree *a)
{
	if( a==NULL) return Head;
	if( Head==NULL) return a;
	
	if( a->key < Head->key)    // 节点 < 当前树根节点情况
		Head->left = Tree_Insert( Head->left, a);
	else                       // 节点 ≥ 当前树根节点情况
		Head->right = Tree_Insert( Head->right, a);

	return AVLTreeAdjust( Head);
}

/** 获取AVLTree的最小值节点, 同二叉搜索树 */   
AVLTree *Tree_Min_Key( AVLTree *a)  
{  
    if( a==NULL) return a;  
    while( a->left!=NULL) a = a->left;
    return a;
}  

/** delete node a form the AVLTree 删除节点 */
/** 借鉴: https://www.geeksforgeeks.org/avl-tree-set-2-deletion/ */
AVLTree *Tree_Delete( AVLTree *Head, AVLTree *a)
{
	if( a==NULL || Head==NULL) return Head;
	
	if( Head==a)     // 查找到了要删除的节点
	{
		if( Head->left==NULL || Head->right==NULL)
        {
            AVLTree *temp = Head->left? Head->left: Head->right;
			free(Head);
			Head = temp;
        }
		else
		{
			// leftSubTree's minkey, 即Head后继
			AVLTree * SuccessOR = Tree_Min_Key( Head->right);
			
			// 头结点替换为后继节点, 复制后继节点
			Head->key = SuccessOR->key;
			
			/** 一种复杂的写法, 但是会完全copy SuccessOR中信息
				AVLTree * l = Head->left;
				AVLTree * r = Head->right;
				*Head = *SuccessOR;
				Head->right = r;
				Head->left  = l;
			*/
			Head->right = Tree_Delete( Head->right, SuccessOR);    //删除后继节点
		}
	}
	else
	{
		if( a->key < Head->key)
			Head->left  = Tree_Delete(  Head->left, a);
		else
			Head->right = Tree_Delete( Head->right, a);
	}
	
	return AVLTreeAdjust( Head);
}

/** 查找节点 */
AVLTree *Tree_Search( AVLTree *Head, int key)
{
	while( Head!=NULL && Head->key!=key) {
		if( Head->key < key)
			Head=Head->right;
		else
			Head=Head->left;
	}
	
	return Head;
}

/** 中序遍历 时间复杂度 n */   
void Inorder_Tree( AVLTree *a)  
{  
    if( a==NULL) return;  

	// visit
	printf( "key:   %d\n", a->key);
	printf( "h:     %d\n", a->h);
	if( a->left !=NULL) printf( "left:  %d\n", a->left->key);
	if( a->right!=NULL) printf( "right: %d\n", a->right->key);
	printf( "\n\n");
	
	Inorder_Tree( a->left);  
    Inorder_Tree( a->right);  
}  

int main()
{
	AVLTree *Head = NULL;
	
	for( int i=0; i<10; i++) {       // Insert
		AVLTree *a = new AVLTree;
		(*a) = { i, 1, NULL, NULL};
		Head = Tree_Insert( Head, a);
	}
	Inorder_Tree( Head); 
	
	printf( "delete node 5\n\n");
	Tree_Delete( Head, Tree_Search( Head, 5));
	Inorder_Tree( Head);
	
	printf( "delete node 3\n\n");
	Tree_Delete( Head, Tree_Search( Head, 3));
	Inorder_Tree( Head);
	
	return 0;
}


红黑树( Red Black Tree)

    红黑树是对二叉搜索树的另一种改进。虽然AVL树使得二叉搜索树足够平衡,但是在插入和删除时为了维护AVL树的性质,需要从下自上回溯调整平衡,因此最多可能需要调整O( logn)次。红黑树没有AVL树那么平衡,但当删除或插入节点使得红黑树性质遭到破坏后,最多通过3次旋转即可完成调整。


红黑树定义

红黑树是一种二叉搜索树,且其左右子树也是一颗红黑树,并满足以下性质:

  1. 节点是红色或黑色;
  2. 根节点是黑色;
  3. 每个叶节点(NIL节点,空节点)是黑色的;
  4. 每个红色节点的两个子节点都是黑色;
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

    细细品味以上定义,会发现一个有意思结论:从根节点到任意叶子节点,没有一条路径比其它路径长处两倍。这也正是红黑树五条约束的实质(仔细品味4、5性质)。

    在性质中所有的NULL节点也被标为黑色,我们可以选择忽略这些NULL节点,那么可以用如下方式进行唯一的表示一颗红黑树:

【算法导论】 二叉搜索树、AVL树、和红黑树_第1张图片

(一个神奇的网站~~~   http://www.cs.usfca.edu/~galles/visualization/Algorithms.html)
















你可能感兴趣的:(算法导论)