二叉搜索树(BST)
基础知识
- 树通常用来存储已排序或已有序的数据。在树中存储数据,常见的就是二叉搜索树(Binary Search Tree,BST)
- BST中的数据是按值排序的:一个节点所有的左侧子孙节点都小于或等于该节点,所有的右侧子孙节点都大于或等于该节点。
- 当面试官说树的时候,通常二叉树,再确认是否为二叉搜索树
- 如果使用中序遍历二叉搜索树,遍历的顺序会按照节点关键字的大小关系从小到大依次进行。
定义
typedef struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode* parent;
};
插入
将关键字k插入到二叉搜索树
若是已经存在,就不再插入
所以插入都是插入在叶子节点
比如已经有了5->6->9,
当插入7的时候,7并不会插入在6和9之间,而是插入在9的左边
- 找到要插入的位置节点,当前位置为空时
- 新建一个节点,给其赋当前值,再使其指向当前的父节点
int BST_Insert(TreeNode *root, int k, TreeNode* parent=NULL)
{
if(root==NULL)
{
TreeNode* tempNode=(TreeNode*)malloc(sizeof(TreeNode));
tempNode->val=k;
tempNode->left=NULL;
tempNode->right=NULL;
tempNode->parent=parent;
return 1;
}
else if(k==root->val)
return 0;//树中存在相同的关键字
else if(k<root->val)
return BST_insert(root->left,k,root);
else
return BST_insert(root->right,k,root);
}
构造
用数组arry[]创建二叉查找树
void create_BST(TreeNode* root,int arr[],int n){
root=NULL;//初始为空树
for(int i=0;i<n;i++)
BST_insert(root,arry[i]);
}
查找
递归:
TreeNode* BST_Search(TreeNode* root,int key){
if (root==NULL||key==root->val)
return root;
if (key<root->val)
return BST_Search(root->left,key);
else
return BST_Search(root->right,key);
}
非递归:
TreeNode* BST_Search_NonRecur(TreeNode* root, int key)
{
while(root != NULL && key != root->val)
{
if(key < root->val)
root = root->val;
else
root = root->val;
}
return T;
}
最大值与最小值
TreeNode* BST_Minnum(TreeNode* root)
{
while(root->left != NULL)
root = root->left;
return root;
}
TreeNode* BST_Maxnum(TreeNode* root)
{
while(root->right != NULL)
root = root->right;
return root;
}
最近共同祖先(LCA)
二叉搜索树的最近共同祖先(LowestCommonAncestor)
假定树中存在这两个点的值
检查当前节点
If value1和value2都小于当前节点的值
检查左子节点
If value1 和value2 都大于当前节点的值
检查右子节点
否则
当前节点就是最近共同祖先
非递归:
TreeNode* find_LCA(TreeNode* root,int value1,int value2){
while(root!=NULL){
int value=root->val;
if(val>value1&&val>value2)
root=root->left;
else if(val<value1&&val<value2)
root=root->right;
elsee
return root;
}
return NULL;//only if empty tree;
}
前驱和后继
问题:给定一个二叉查找树的节点,求出它在中序遍历中的前驱和后继。
后继:
两种情况
- 若结点 x 的右子树不为空,则 x 的后继就是它的右子树中关键字值最小的结点;
- 若结点 x 的右子树为空,为了找到其后继,从结点 x 开始向上查找,直到遇到一个祖先结点 y,它的左儿子也是结点 x 的祖先,则结点 y 就是结点 x 的后继。如下图
TreeNode* BST_successor(TreeNode* node){
if(node->right!=NULL)
return BST_Minnum(node->right);
TreeNode* p =node->parent;
while(p!=NULL&&p->right==node){
node=p;
p=p->parent;
}
}
前驱
两种情况
- 若结点 x 的左子树不为空,则 x 的前驱是它的左子树中关键字值最大的结点;
- 若结点 x 的左子树为空,为了找到其前驱,从结点 x 开始向上查找,直到遇到一个祖先结点 y,它的右儿子也是结点 x 的祖先,则结点 y 就是结点 x 的前驱。
TreeNode* BST_predecessor(TreeNode* node){
if(node->left!=NULL)
return BST_Maxnum(node->left);
TreeNode* p=node->parent;
while(p!=NULL&&p->left==node){
node=p;
p=p->parent;
}
return p;
}
BST的删除
二叉查找树的删除操作相对复杂一点,它要按3种情况来处理:
1.若被删除结点z是叶子结点,则直接删除,不会破坏二叉排序树的性质;
2. 若结点z只有左子树或只有右子树,则让z的子树成为z父结点的子树,代替z的位置;
3. 若结点z既有左子树,又有右子树,则用z的后继(没有左孩子)代替z(先删除后继结点,再将后继的内容去替换z),而删除后继结点的过程就是第一种或第二种情况。
说明:第三种情况,因为z有两个子女,所以它的后继肯定是它右子树中的最左边(最小值),也就是说它的后继要么是叶子,要么只有一个右孩子
void BST_delete(TreeNode* root,TreeNode* z){
if(z->left==NULL&&z->right=NULL){
if(z->parent!=NULL){
if(z->parent->left==z)
z->parent->left=NULL;
else
z->parent->right=NULL;
}
else
root->NULL;
free(z);
}
else if(z->left!=NULL&&z->right==NULL){
z->left->parent=z->parent;//将z的左子树的父亲设为z的父亲
if(z->parent!=NULL){
if(z->parent->left==z)
z->parent->left=z->left;//将z父亲的左孩子指向z的左子树
else
z->parent->right=z->left;
}
else
root=z->left; //删除左斜单支树的根结点
free(z);
}
else if(z->right!=NULL&&z->left==NULL){
z->right->parent=z->parent;
if(z->parent!=NULL){
if(z->parent->left==z){
z->parent->left=z->right;
}
else
z->parent->right=z->right;
}
else
root=z->right; //删除右斜单支树的根结点
free(z);
}
else{
TreeNode* s=BST_Successor(z);
z->val=s->val; //后继s的关键字替换为z的关键字
BST_Delete(T,s); //转为第一或第二种情况
}
}
对于一个高度为h的二叉查找树来说,删除操作和插入操作一样,都是O(h)
红黑树
http://taop.marchtea.com/03.01.html
二叉查找树回顾
由于红黑树本质上就是一棵二叉查找树,所以在了解红黑树之前,咱们先来看下二叉查找树。
二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
- 若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若任意结点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 任意结点的左、右子树也分别为二叉查找树。
- 没有键值相等的结点(no duplicate nodes)。
因为,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的执行时间为O(lgn).(至于n个结点的二叉树高度为lgn的证明,可参考算法导论 第12章 二叉查找树 第12.4节)。
红黑树概述
前面我们已经说过,红黑树,本质上来说就是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。
但它是如何保证一棵n个结点的红黑树的高度始终保持在h = logn的呢?这就引出了红黑树的5条性质:
- 每个结点要么是红的,要么是黑的。
- 根结点是黑的。
- 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
- 如果一个结点是红的,那么它的俩个儿子都是黑的。
- 对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。
正是红黑树的这5条性质,使得一棵n个结点是红黑树始终保持了logn的高度,从而也就解释了上面我们所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论的原因。
如下图所示,即是一颗红黑树(下图引自wikipedia: http://t.cn/hgvH1l ):
上文中我们所说的 “叶结点” 或”NULL结点”,它不包含数据而只充当树在此结束的指示,这些结点以及它们的父结点,在绘图中都会经常被省略。
树的旋转知识
当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。
为了继续保持红黑树的性质,我们可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即修改树中某些结点的颜色及指针结构,来达到对红黑树进行插入或删除结点等操作后,继续保持它的性质或平衡。
树的旋转,分为左旋和右旋,以下借助图来做形象的解释和介绍:
1.左旋
如上图所示:
当在某个结点pivot上,做左旋操作时,我们假设它的右孩子y不是NIL[T],pivot可以为任何不是NIL[T]的左孩子结点。
左旋以pivot到y之间的链为“支轴”进行,它使y成为该孩子树新的根,而y的左孩子b则成为pivot的右孩子。
左旋操作的参考代码如下所示(以x代替上述的pivot):
B树
http://taop.marchtea.com/03.02.html