今年暑假参加了某C9院校的夏令营,面试时在算法方面被问到了二叉搜索树和堆结合的问题,当时太紧张了没答出来T T,也可以说是自己太菜了吧,因此下来之后又温习了下二叉树和堆的知识。写个笔记就当记录下自己的悔恨吧T T,同时树、图和堆在各种面试场合都是重点!
二叉搜索树
二叉搜索树,是将二分搜索过程的动机应用于基于树的数据结构中,以更有效地支持更新操作。
二叉搜索树的特点就是存储在v的左子树的元素值均小于v的值e,存储在v的右子树的元素值均大于等于v的值e,因此二叉搜索树的中序遍历便是以非降序的顺序访问这种树中的元素。
二叉搜索树的基本操作
创建
二叉搜索树的创建根据其定义就可以简单知道,判断待插入结点t的值和当前结点n的值,小于就对结点n的左子树递归判断,大于就对结点n的右子树递归判断,直到当前结点为NULL时插入。所以我们很容易发现创建二叉搜索树的过程就是不断地执行插入操作,并且都是插入在叶子节点的孩子节点。
//创建一个二叉搜索树
void Creat_BSTree(BSTree *T,NodeType c)
{
if((*T) == NULL)
{
(*T) = (BSTNode*)malloc(sizeof(BSTNode));
(*T)->val = c;
(*T)->rchild = (*T)->lchild = NULL;
}else if(c <= (*T)->val)
Creat_BSTree(&(*T)->lchild,c);
else
Creat_BSTree(&(*T)->rchild,c);
}
查找
二叉搜索树的基础查找过程和插入过程逻辑是一样的,因此就不详细说啦。(我们详细说一些升级的查找)
//二叉搜索树基础查找
bool BSTree_Search(BSTree T,NodeType c)
{
if(T == NULL)
{
return false;
}else if(T->val == c)
{
return true;
}else if(T->val > c)
{
return BSTree_Search(T->lchild,c);
}else
{
return BSTree_Search(T->rchild,c);
}
}
二叉搜索树——范围查找
二叉搜索树的范围查找就是给出存储在二叉搜索树T中关键字满足k1≤k≤k2的所有元素。
例如:在销售某个东西时,顾客想要询问某一个价格范围内的产品,那么这种操作就是有意义的。
算法思路:
如果当前节点v的值在(k1,k2)范围内,则对v节点的左、右子树进行递归查找,并且将该点并入集合S中(集合S存储的是满足条件的节点的值),如果节点v的值小于k1,则在k1的右子树递归查找,如果节点v的值大于k2,则在k2的左子树递归查找,当当前节点v为NULL时,返回一个空集。
伪代码如下:
RangeQueries(k1,k2,v):
输入:范围k1,k2,二叉搜索树T的节点v
输出:存储在树T中以v为根的子树中、关键字在[k1,k2]范围内的元素
if V==NULL then
return 空集
if k1<= key(v) <=k2 then
L <- RangeQueries(k1,k2,v->Lchild)
R <- RangeQueries(k1,k2,v->Rchild)
return R∪v∪L
else if key(v)<k1 then
return RangeQueries(k1,k2,v->Rchild)
else if key(v)>k2 then
return RangeQueries(k1,k2,v->Lchild)
C语言代码实现如下:
注:我在这里的集合用的是自己定义的结构类型,说到底还是数组存储的。
结构类型定义
typedef int NodeType;
typedef struct Node
{
NodeType val;
int index; //添加索引域,用于线索二叉树的索引搜索
struct Node *lchild;
struct Node *rchild;
}BSTNode, * BSTree;
typedef struct Gather
{
NodeType val[MAX];
int numbers;
}Gather, *Gather_p;
//二叉搜索树范围查找
Gather RangeQueries(BSTree T,NodeType k1,NodeType k2)
{
Gather G,L1,L2;//初始化三个集合
InitGather(&G);
InitGather(&L1);
InitGather(&L2);
if(T == NULL)
return G;
if(k1 <= T->val && T->val <= k2)
{
L1 = RangeQueries(T->lchild,k1,k2);
L2 = RangeQueries(T->rchild,k1,k2);
G = Merge(L1,L2);
G.val[G.numbers++] = T->val;
return G;
}else if(T->val < k1)
return RangeQueries(T->rchild,k1,k2);
else if(T->val > k2)
return RangeQueries(T->lchild,k1,k2);
}
集合相关操作:
//打印集合
void Print_Gather(Gather G)
{
for(int i=0;i<G.numbers;i++)
{
printf("%d ",G.val[i]);
}
}
//初始化集合
void InitGather(Gather *G)
{
G->numbers = 0;
}
//合并集合
Gather Merge(Gather L1,Gather L2)
{
bool flag;
for(int i=0;i<L1.numbers;i++)
{
flag = true;
for(int j=0;j<L2.numbers;j++)
{
if(L2.val[j] == L1.val[i])
{
flag = false;
break;
}
}
if(flag)
{
L2.val[L2.numbers++] = L1.val[i];
}
}
return L2;
}
二叉搜索树——基于索引查找
我们知道,对于有序数组的搜索,我们可以直接通过索引单元A[i]来快速找出数组中的第i个小的元素。但是数组的缺点是不支持高效的更新,而二叉搜索树允许高效的插入和删除。但是当切换到二叉搜索树时,我们就失去了快速找到集合中第i小项的能力。
但是,我们可以通过增加结点的一个域来使得二叉搜索树重新获得这种ability。
实现方法的主要思想是,扩充T的每个结点v,加入一个新的域index,其中index表示以当前节点为根的子树中存储的项的数量(包括其本身),同时我们定义NULL的index为0,因此也就不需要为NULL存储index。
如下图所示:
扩充二叉搜索树的index域更新
-
如果在T的一个节点w处(之前为NULL)进行插入,呢们我们设置w的index为1,并且递增w的每个祖先v的index值,节点v在从w到T的根节点的路径上。
-
如果删除T的一个节点,那么我们递减从w的双亲到T的根节点的路径上的每个节点v的index值。
扩充二叉搜索树的搜索
主要思想就是向下搜索树T,同时保持i的值以便我们寻找仍在搜素的子树中的关键字第i小的项。
IndexSelect(i,v):
输入:索引i(第i小的项),二叉搜索树T的节点v
输出:以v为根的T的子树上的关键字第i小的项
令w <- v.lchild
if i <= w.index then
return IndexSelect(i,w)
else if i=w.index + 1 then
return key(v)
else
return IndexSelect(i-w.index-1,v.rchild)
C语言实现代码:
//二叉搜索树插入/创建 结点后更新索引项
void UpdateIndex_Creat(BSTree * T,NodeType c)
{
BSTNode * node;
node = (*T);
while (node)
{
if(node->val == c)
break;
node->index += 1;
if(node->val > c)
{
node = node->lchild;
}else
{
node = node->rchild;
}
}
}
//创建二叉搜索树(带索引)
void CreatBSTree_index(BSTree *T,NodeType c)
{
if((*T) == NULL)
{
(*T)=(BSTree)malloc(sizeof(BSTNode));
(*T)->val = c;
(*T)->index = 1;
(*T)->lchild = (*T)->rchild = NULL;
}else if(c <= (*T)->val)
CreatBSTree_index(&(*T)->lchild,c);
else
CreatBSTree_index(&(*T)->rchild,c);
}
//按照索引搜索值
NodeType Search_Index(BSTree *T,int index)
{
BSTNode * node = (*T)->lchild;
int temp;
if(node == NULL)//如果是NULL(空),则index = 0
{
temp = 0;
}else
{
temp = node->index; //temp暂存结点的index
}
if(index <= temp)
{
return Search_Index(&node,index);
}else if(index == temp + 1)
{
return (*T)->val;
}else
{
return Search_Index(&(*T)->rchild,index-temp-1);
}
}
该算法的正确性基于以下事实:我们总是将i保持为正在搜索的子树中的第i小的项的索引,使得返回的项是正确的索引。这也就是为什么我们在递归搜索右子树时首先减去存储在左子树和双亲中的项的数量(index)。