文章目录
一:概念
1.使用场景
二叉搜索树既可以作为一个字典又可以作为一个优先队列
2.定义
- 二叉搜索树是一颗符合如下性质的二叉树:
- 任何节点,左子树中的任何一个关键字都不大于该节点,右子树中的任何一个关键字都不小于该节点 - 可以用链表来表示,其中的每一个节点都是一个对象,节点属性包括key,left,right,parent,卫星数据(key的一些附属信息)。
- 如果对应的节点不存在的时候,则相应属性的值为nil。
- 根节点是整棵树中唯一一个父指针为nil的节点
class TreeNode{
// 该节点存储的关键字
object key
// 左节点
TreeNode left;
// 右节点
TreeNode right;
// 父节点
TreeNode parent;
}
3. 示例
对于任何节点,左子树中的任何一个关键字都不大于该节点,右子树中的任何一个关键字都不小于该节点
如上图所示,整个树的root的关键字是6,root的左子树中包含的关键字2,5,5,他们均不大于6。而在右子树种的关键字7和8,他们均不小于6.
鉴于二叉搜索树左<中<右的特点,所以一般我们使用中序遍历,这样遍历的结果就天然的是升序
4.遍历
INORDER-TREE-WALK(x)
if x!=NIL
INORDER-TREE-WALK(x.left)
print x.key
INORDER-TREE-WALK(x.right)
时间复杂度:O(n)
4.1 前驱和后继
在中序遍历下,前驱指的是前一个节点,后继指的是后一个节点
如果遍历之后的顺序是dxsf,那么d是x是前驱,s是x的后继
5.基本操作
5.1 查找
查找指在一个二叉搜索树中查找一个具有给定关键字的节点,如果存在返回节点,如果不存在返回nil。
// 参数x指的是查找的当前节点。参数k指的是查找的目标关键字
TREE-SEARCH(x,k)
// 当x为nil的时候,那么k必然不存在,所以返回nil即x就好;
// 当x节点存储的关键字刚好是目标关键字的时候,那么返回x
if x==NIL or k==x.key
return x
// 当目标关键小于当前节点存储的key的时候,就去x节点的左子树中查找。因为二叉搜索树的特点就是右子树存储的关键字均大于当前节点的关键字,所以在这种情况下可以直接忽略右子树
if k<x.key
return TREE-SEARCH(x.left,k)
// 当目标关键不小于当前节点存储的key的时候,就去x节点的右子树中查找,因为二叉搜索树的特点就是右子树存储的关键字均大于当前节点的关键字。
else
return TREE-SEARCH(x.right,k)
假设二叉搜索树的高度是h,那么根据上面的伪代码可以知道整个递归次数应该是<=h的,所以时间复杂度为O(h)
这里还有一种非递归写法
TREE-SEARCH(x,k)
y=x.key
while x!=NIL and k!=y
if k<x.key
x=x.left
else
x=x.right
return x
5.2 最小关键字
TREE-MININUM(x)
while x.left!=NIL
x=x.left
return x
因为二叉搜索树的规则是左子树小于节点小于右子树,所以从上至下,遍历到左节点为nil的时候即为存储最小关键值的节点
5.3 最大关键字
TREE-MININUM(x)
while x.right!=NIL
x=x.right
return x
因为二叉搜索树的规则是左子树小于节点小于右子树,所以从上至下,遍历到右节点为nil的时候即为存储最大关键值的节点
5.4 插入
插入操作是会导致搜索二叉树的动态集合发生变化,所以在修改数据结构的时候需要注意,要保持二叉树性质的成立
工作流程: 从根节点出发,通过对每一个节点存储关键字和目标关键字的比较来确定路径的走向
// 在树T中插入节点z
TREE-INSERT(T, z)
// 创建一个临时节点,用来存储while循环结束的叶子节点
y=nil
// 从根节点开始
x=T.root
// 从上至下,直到遇到叶子节点
while x!=NIL
y=x
//如果插入节点小于当前节点,那么就往左子树上去寻找位置
if z.key<x.key
x=x.left
//如果插入节点大于当前节点,那么就往左子树上去寻找位置
else
x=x.right
// 将插入节点的父亲设置为叶子节点y
z.p=y
//如果y==nil,那就说明没有进入while循环,即T.root是nil,所以直接将该节点设置为root
if y==nil
T.root=z
// 判断要将插入节点作为叶子节点的左子树还是右子树
else if y.key<z.key
y.left=z
else
y.right=z
从上述伪代码中可以看到,消耗时间的大头在于while循环,从上至下的查找路径,可知时间复杂度为O(h)
从上面的分析中可以看到,插入的节点都是被放在叶子节点的子节点,可以对于整棵树的上层部分是没有影响的
5.5 删除
删除操作与上面的操作不同,而且比较复杂,是因为当删除一个内部节点(非叶子节点)的时候,需要调整整棵树,在调整的时候也需要注意满足搜索二叉树的性质
首先我们需要考虑如下几种情况:
假设现在要从树T种删除节点z
5.5.1 z没有子节点
那么只需要删除叶子节点z,并将z的父节点的子节点设置为nil就好
5.5.2 z只有一个右子节点
那么只需要删除叶子节点,并用z的子节点来代替z的位置
5.5.3 z只有一个左子节点
那么只需要删除叶子节点,并用z的子节点来代替z的位置
5.5.4 z有两个子节点的时候
我们需要去寻找z的后继y来根据情况分析
4.1. 如果z的后继y是z的右子节点的话,那么y的左子树必然是nil。因为当y的左子树不是nil的时候,那么在中序遍历下,遍历z之后,必然会遍历y的左子树,然后再到y,也就是下面介绍的4.2那种情况。
删除z节点,将z节点左节点设置为y的左节点
4.2 z的后继y不是z的右子节点的话,
如图介绍,节点r表示一颗高度为h1的子树,那么z的后继y就是z的右子树中最左路径的终点,也可以理解为右子树中的最小节点,即y的左子树是nil
在删除节点z有两个子节点的时候,树调整的原则就是使用后继节点y替换删除节点(因为后继节点是删除节点的右子树中最小的节点,所以用这个节点来进行替换,才不会破坏搜索二叉树左<中<右的性质)。面对4.2的情况,因为后继节点y没有左子节点,所以可以用将删除节点z的左子节点l作为y的左子节点。那么这个时候就会两个被破坏的右子节点,一个是删除节点z的右子节点r,另外一个是后继节点y的右子节点t。
那么现在的问题就是节点r和节点y和节点t之间的关系。
为了不违反搜索二叉树的性质,我们先来看一下他们的大小关系
y<t<r
然后考虑t和r只能在y的右子树上
所以有如下几种情况
可以看到,这两种情况都是符合条件的,区别的t和r的层级关系。第一种情况,因为t节点下面还有子树,如果我们把r节点作为t的子子节点的话,必然要多遍历几层,而且也更改了叶子节点,所以用第二种方式相对来说节点移动简单而且对叶子节点的修改比较少
对于删除节点,可能的树调整方案上面已经分析过,那么接下来就看一下伪代码
首先看到,不管是哪种情况,都会有节点y来替换节点z的这一步,那么先实现一个这样的功能
这个方法实现的是更换节点和父节点的一种关系置换,不修改子节点的关系
// 将二叉搜索树T上面的u节点替换为v节点
TRANSPLANT(T,u,v)
// 当u.p==Nil的时候,那就说明u是root,那么就直接设置root为v就可以
if u.p==NIL
T.root=v
// 如果u是左子节点的话,那么就设置父节点的左子节点为v
else if u.p.left=u
u.p.left=v
// 如果u是右子节点的话,那么就设置父节点的右子节点为v
else
u.p.right=v
// 设置新节点的父节点
if v!=NIL
v.p=u.p
节点的删除过程
// 从二叉搜索树T中删除节点z
TREE-DELETE(T,z)
// z的左子节点为nil的情况,直接用右子节点替换z就可以
if z.left==Nil
TRANSPLANT(T,z,z.right)
// z的左子节点不为nil,右子节点为nil的情况,直接用左子节点替换z就可以
else if z.right==Nil
TRANSPLANT(T,z,z.left)
// z的左右子节点都不为nil的情况
else
// 找到z的后继,即右子树的最小值,那么就可以确认y的左子节点是nil
y=TREE-MININUM(T,z.right)
// 第4.1种情况,即后继是z的右子节点的情况:
if y.p==z
//使用y来替换z
TRANSPLANT(T,z,y)
// 将z的左子节点设置y的左子节点
y.left=z.left
z.left.p=y
// 第4.2种情况,即后继不是z的右子节点的情况:
else
// 其实在4.2情况下,我们需要调整的节点关系有两个,一个是z的左子节点和y的关系;一个是z的右子节点和y的右子节点的关系
TRANSPLANT(T,y,y.right)
y.right=z.right
y.right.p=y
TRANSPLANT(T,z,y)
y.left=z.left
z.left.p=y
可以看到上面的代码有一部分还是重复调用的,所以我们可以调整一下
TREE-DELETE_v2(T,z)
if z.left==Nil
TRANSPLANT(T,z,z.right)
else if z.right==Nil
TRANSPLANT(T,z,z.left)
else
y=TREE-MININUM(T,z.right)
if y.p!=z
TRANSPLANT(T,y,y.right)
y.right=z.right
y.right.p=y
TRANSPLANT(T,z,y)
y.left=z.left
z.left.p=y
时间复杂度:从上面的代码中可以看出,时间的消耗在两个地方TRANSPLANT和TREE-MININUM,但是TRANSPLANT这里只涉及到两个节点之间关系指针的变化,所以使用的是常数时间。TREE-MININUM这里需要根据指定节点从上至下寻找最小关键值,和树的高度h有关,所以时间复杂度为O(h)