引言
计算机系统中,经常遇到的树是分级文件结构。文件的路径和根等术语来自于树的理论。我们接下来对二叉搜索树进行整理。注意到,分级文件结构和接下来讨论的树有明显不同,文件结构中,子目录中不含有数据:他们只有其他子目录或者文件的引用,只有文件中才包含数据。而在书中,每个节点都包含数据,以及其他节点的引用(除叶子节点)。
二叉搜索树特征定义可以这样说:二叉树中,一个节点的左子节点的关键字值小于这个节点,右子节点的关键字值大于或者等于这个父节点。
二叉搜索树工作过程
查找节点
查找思路:判断当前节点与查找节点值。若等于,则返回当前节点值;若小于当前节点,查找当前节点左子树;否则,查找当前节点右子树。
public Node find(int key)
{
Node current = root;
while(current.data!=key)
{
if(key<current.data)
current = current.left;
else
current = current.right;
if(current==null)
return null;
}
return current;
}
插入节点
插入节点必须满足插入之后不会破坏原来二叉搜索树的规律。插入节点首先得找到插入的地方。
public void insert(int id, double dd)
{
Node newNode = new Node();
newNode.data = d;
if(root==null)
root = newNode;
else
{
Node current = root;
Node parent;
while(true)
{
parent = current;
if(d<current.data)
{
current = current.left;
if(current==null)
{
parent.left = newNode;
return;
}
}else
{
current = current.right;
if(current==null)
{
parent.right = newNode;
return;
}
}
}
}
}
遍历树
分为前序(preorder)、中序(inorder)、后序(postorder)。二叉搜索树最常用的遍历方法是中序遍历。所以先介绍中序,后面才简单介绍前序和后序遍历。
中序遍历会使得所有节点按关键字值升序被访问到。采用递归的方式遍历整棵树只需如下步骤即可:
1.调用自身来遍历节点的左子树。
2.访问这个节点。
3.调用自身来遍历节点的右子树。
private void inOrder(node localRoot)
{
if(localRoot!=null)
{
inOrder(localRoot.left);
Syso(localRoot.data+"");
inOrder(localRoot.right);
}
}
前序遍历步骤:
1.访问节点。
2.调用自身遍历该节点的左子树;
3.调用自身遍历该节点右子树。
后序遍历:
1.调用自身节点左子树。
2.调用自身节点右子树。
3访问该节点。
查找最大值和最小值
根据二叉搜索树的特点,最大值是右子树的最右叶子节点;最小值是左子树的最左叶子节点值。
删除节点
删除节点是最复杂的二叉搜索树操作。考虑以下三种情况:
1.该节点是叶子节点。
2.该节点有一个子节点。
3.该节点有两个子节点。
情况1:删除没有子节点的节点
直接删除即可。
public boolean delete(int key)
{
Node current = root;
Node parent = root;
boolean isLeftChild = true;
while(current.data!=null)
{
parent = current;
if(key<current.data)
{
isLeftChild = true;
current = current.left;
}
else
{
isLeftChild = false;
current = current.right;
}
if(current == null)
return false;
}
//找到节点后,要检查是否真的没有子节点,如果没有,还需要检查是不是根,这样就清空了整棵树;否则,就把父节点的left和right字段设置为null,断开父节点和要删除节点的连接。
if(current.left==null&¤t.right==null)
{
if(current == root)
root = null;
else if(isLefterChild)
parent.left = null;
else
parent.right = null;
}
}
第二种情况:删除有一个子节点的节点
这个节点只有两个连接:连向父节点的和连向他唯一的子节点。需要从这个序列中剪断这个节点,把它子节点直接连接到它的父节点上。这个过程要求改变父节点的引用,指向要删除的子节点。
删除子节点主要有四种情况:要删除的节点的子节点可能有左子节点或者右子节点,并且每种情况中要删除的节点也可能是自己父节点的左子节点或者右子节点。
//从前面删除没有子节点的代码片段处继续
//如果没有右孩子,用左子树代替
else if(current.right==null)
{
if(current==root)
root = current.left;
else if(isLeft)//left child of parent
parent.left = current.left;
else
parent.right = current.left;
}
//如果没有左孩子,用右子树代替
else if(current.left==null)
{
if(current==root)
root = current.right;
else if(isLeft)//父节点的左孩子
parent.left = current.right;
else
parent.right = current.right;
}
第三种情况:删除有两个子节点的节点
删除有两个子节点的节点,用它的中序的后继来代替该节点。因此需要先去寻找节点的后继,算法如下:
首先,程序找到初始节点的右子节点,它的关键字值一定比初始节点大。然后转到初始节点的右子节点的左子节点,以此类推,顺着左子节点的路径一直向下找,这个路径上最后一个左子节点就是初始节点的后继。这实际就是寻找比初始节点关键值大的节点集合中最小的一个节点。
如果初始节点右节点没有左子节点,这个右子节点本身就是后继。
寻找后继代码:
private node getSuccessor(node delNode)
{
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.right;
while(current!=null)
{
successorParent = successor;
successor = current;
current = current.left;
}
if(successor!=delNode.right)
{
successorParent.left = successor.right;
successor.right = delNode.right;
}
return successor;
}
现在我们重新讨论第三种情况,第三种情况中后继节点可能与current有两种位置关系,current就是要删除的节点。后继可能是current的右子节点,或者是current右子节点的左子孙节点。
如果是delNode的右子节点,情况相对简单,只需指向两个步骤:
1.把current从它父节点的right字段删掉,把这个字段指向后继。
2.把current的左子节点移出来,把它插入到后继的left字段。
parent.right = successor;
successor.left = current.left;
具体代码如下:
//连接前面的else if
else//删除的节点存在两个孩子,所以用中序的后继代替
{
//得到当前删除节点的后继
Node successor = getSuccessor(current);
//将删除节点的父节点连接上后继节点
if(current==root)
{
root = successor;
}
else if(isLeft)
parent.left = successor;
else
parent.right = successor;
}
return true;
}//end delete
现在终于将删除部分说完了,回顾一下知道一共包括两步:
第一步:如果删除的节点current是根,它没有父节点,所以只需要把根置为后继。否则,要删除的节点或者是左子节点或者是右子节点。因此需要把它父节点的对应字段指向successor。当delete()方法返回,current失去了作用范围后,就没有指向current保存的节点,它就会被Java的垃圾收集机制销毁。
第二步:把successor的左子节点指向的位置设为current的左子节点。
总结:最复杂的就是删除操作,需要不断复习理解。