二叉搜索树是一颗二叉树或一颗空树且满足以下性质:
1)根节点 x的key值大于任意左子树上节点的key值,小于右子树上任意节点的key值 ;
2)其左右子树也分别是一颗二叉搜索树。
使用二叉搜索树进行查找时间复杂度为O( h ),且 n ≥ h ≥ log(n+1);那么时间复杂度上限为O(n)、下限为Ω(log n),且 h 趋于 n 的情况远远小于趋于 log n 的情况,那么渐进时间复杂度为Θ(log n) .
树的插入只需依次向下进行比较,直到走到叶子结点为止,插入到叶子节点之后。
删除节点(分下面两种情况):
/**
* 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
二叉搜索树可能会产生一些极端情况,比如传进来的是已经排序好的数列,那么建立的二叉搜索树就变成了类似的一个单向链表。这样查找的时间复杂度为O(h),即O(n)。
面对这种情况,建立好二叉树之后,以后每次查找时间复杂度都为O(n),大大降低了时间效率。为此我们可以对这种情况做一下改进,每次插入节点时,将树调整到足够平衡,这样无论多少次插入节点这棵树将保持 h = O(log n)。下面引入平衡因子概念:
平衡因子:某节点左子树的高度与右子树的高度差 为 该节点的平衡因子。
AVL树要求所有节点的平衡因子为1,-1 或 0 . 当插入或删除节点导致平衡因子不为±1或0时,则进行调整。
调整分下面四种情况(盗图@PulsPuls1):
LL型(右单旋):
RR型(左单旋):同LL型,只不过方向相反。
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;
}
红黑树是对二叉搜索树的另一种改进。虽然AVL树使得二叉搜索树足够平衡,但是在插入和删除时为了维护AVL树的性质,需要从下自上回溯调整平衡,因此最多可能需要调整O( logn)次。红黑树没有AVL树那么平衡,但当删除或插入节点使得红黑树性质遭到破坏后,最多通过3次旋转即可完成调整。
红黑树定义
红黑树是一种二叉搜索树,且其左右子树也是一颗红黑树,并满足以下性质:
细细品味以上定义,会发现一个有意思结论:从根节点到任意叶子节点,没有一条路径比其它路径长处两倍。这也正是红黑树五条约束的实质(仔细品味4、5性质)。
在性质中所有的NULL节点也被标为黑色,我们可以选择忽略这些NULL节点,那么可以用如下方式进行唯一的表示一颗红黑树:
(一个神奇的网站~~~ http://www.cs.usfca.edu/~galles/visualization/Algorithms.html)