针对二叉查找树的操作(增删改查)的时间和树的高度成正比,比如都有10个节点的一个树,树高为4和树高为10的操作时间肯定是不同的,这个时间实际上是O(lgn),二叉查找树的期望的树高是lgn,从而基本动态集合的操作平均时间为θ(lgn)。
通常二叉查找树基于链表实现,每个节点保存左,右子节点,如果想更方便的实现前后查找,可以增加个一个父节点属性。由于二叉查找树的特点,采用中序遍历可以按照从小到大的顺序将树的所有元素输出,一般的中序遍历都是通过递归来实现。
下面自己实现一个二叉查找树的插入和中序遍历:
package tree;
/**
* 二叉搜索树
* @author Administrator
*
*/
public class BinarySearchTree {
private Node root;
/**
* 将一个新节点插入到二叉搜索树中
* @param value
*/
public void insert(int value)
{
if(null == root)
{
root = new Node(null,null,null,value);
}
else
{
insertNotRoot(value);
}
}
/**
* 中序遍历
* @param n
*/
public void midOrderWalk(Node n)
{
if(null == n)
{
return;
}
midOrderWalk(n.left);
System.out.println(n.value);
midOrderWalk(n.right);
}
private void insertNotRoot(int value) {
Node cur = root;
Node parent = root;
while(null != cur)
{
parent = cur;
//插入左子树
if(value < cur.value)
{
if(null == cur.left)
{
Node n = new Node(null,null,parent,value);
parent.left = n;
break;
}
cur = cur.left;
}
//插入右子树
else
{
if(null == cur.right)
{
Node n = new Node(null,null,parent,value);
parent.right = n;
break;
}
cur = cur.right;
}
}
}
public Node getRoot() {
return root;
}
public void setRoot(Node root) {
this.root = root;
}
//节点类,保存左右子孩子和父类节点的索引
private final class Node
{
private Node left;
private Node right;
private Node parent;
private int value;
public Node(Node left, Node right, Node parent, int value) {
this.left = left;
this.right = right;
this.parent = parent;
this.value = value;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
}
对于二叉查找树来说,删除的动作比插入还要复杂一些,因为删除需要考虑的情况更多些,现在就来实现删除的动作,大概考虑下有哪些情况:
首先肯定如果我们删除一个节点,该节点没有左孩子也没有右孩子,则删除它完全不会影响我们当前的二叉查找树,本来是一棵二叉查找树现在还会是一棵二叉查找树。比如上图中的节点48。
其次,考虑要删除的节点只有左孩子或者只有右孩子,比如节点37只有左孩子,由于只有一个孩子,非左即右,左边的都比该节点小,右边的都不比该节点小,因此大小并没有二义性,取该节点的左孩子或者右孩子节点取代该节点即可。
最麻烦的是有两个孩子节点的情况,比如图中的节点47,针对47的删除,我们可以有下面的两种情况:
考虑删掉47我们可以拿哪个节点来替代47从而保证树的连接性,考虑到47左边的子树节点都比它小,如果在左子树找,则肯定要找左子树中最大的节点,这个节点肯定需要沿着左子树一直找右节点,左图就是这样一种情况。同理右图,沿着右子树一直找左节点。
先不急看删除的代码,先看下两个概念,后继和前驱,一个节点的前驱是指小于该节点的最大节点,也就是中序遍历该节点的前一个节点,后继就是指大于该节点的最小节点,也就是中序遍历该节点的后一个节点。比如,左图,其实就是寻找47前驱的过程,右图就是寻找47后继的过程。
先不急写寻找某个节点的前驱和后继的代码,寻找前驱和后继其实就是寻找子树最大和最小节点的过程,先来写寻找一棵树的最大和最小节点的实现,如下:
/**
* 寻找一棵树的最大节点
* 不断寻找右节点,直到该节点没有右孩子节点
* @param root
* @return
*/
public Node getMaxNode(Node root)
{
if(null == root)
{
return null;
}
else
{
Node maxNode = root;
while(null != maxNode.right)
{
maxNode = maxNode.right;
}
return maxNode;
}
}
public int getMaxValue(Node root)
{
return getMaxNode(root).getValue();
}
/**
* 寻找一棵树的最小节点
* @param root
* @return
*/
public Node getMinNode(Node root)
{
if(null == root)
{
return null;
}
else
{
Node minNode = root;
while(null != minNode.left)
{
minNode = minNode.left;
}
return minNode;
}
}
public int getMinValue(Node root)
{
return getMinNode(root).getValue();
}
逻辑不难,就不详细讲解了。有了上面的逻辑,再来看下找出一个节点前驱和后继的代码:
/**
* 获取一个节点的后继
* @param curNode
* @return
*/
public Node successor(Node curNode)
{
if(null == curNode)
{
System.out.println("the curNode is null");
return null;
}
else
{
//如果有右子树
if(null != curNode.right)
{
return getMinNode(curNode.right);
}
else
{
//如果没有右子树,表明该节点是某棵子树除根节点外的最大值,找出该根节点即可,
//需要向上回溯,如果当前节点是父节点的左节点,则父节点即是后继,否则继续向上回溯。
Node parent = curNode.parent;
while(null != parent && curNode == parent.right)
{
curNode = parent;
parent = parent.parent;
}
return parent;
}
}
}
/**
* 获取一个节点的前驱
* @param curNode
* @return
*/
public Node precessor(Node curNode)
{
if(null == curNode)
{
System.out.println("the curNode is null");
return null;
}
else
{
if(null != curNode.left)
{
return getMaxNode(curNode.left);
}
else
{
//如果没有左子树,表明该节点是某棵子树除根节点外的最小值,找出该根节点即可,
//需要向上回溯,如果当前节点是父节点的右节点,则父节点即是后继,否则继续向上回溯。
Node parent = curNode.parent;
while(null != parent && curNode == parent.left)
{
curNode = parent;
parent = parent.parent;
}
return parent;
}
}
}
在写删除逻辑之前再写最后一个方法,根据值获取node节点:
/**
* 查找元素
* @param value
* @return
*/
public Node searchNode(int value)
{
Node node = root;
while(null != node && value != node.value)
{
if(value < node.value)
{
node = node.left;
}
else
{
node = node.right;
}
}
return node;
}
好了,做了那么多铺垫,终于可以来完成删除的逻辑了,实际上我们之前的代码,基本完成了一个搜索二叉树提供的功能(不考虑非法数据)。
/**
* 删除操作,分三种情况
* @param value
*/
public void remove(int value)
{
Node node = searchNode(value);
if(null == node)
{
System.out.println("there is no item of value");
}
else
{
//第一种情况,没有子节点
if(null == node.left && null == node.right)
{
if(node == node.parent.left)
{
node.parent.left = null;
}
else
{
node.parent.right = null;
}
}
//第二种情况
else if(null != node.left && null == node.right)
{
//如果是根节点
if(null == node.parent)
{
root = node.left;
}
else
{
if(node == node.left)
{
node.parent.left = node.left;
}
else
{
node.parent.right = node.left;
}
}
}
//第二种情况
else if(null == node.left && null != node.right)
{
if(null == node.parent)
{
root = node.right;
}
else
{
if(node == node.left)
{
node.parent.left = node.right;
}
else
{
node.parent.right = node.right;
}
}
}
else
{
//两个节点都不为空,则选择查询右子树最小的节点
Node precessor = precessor(node);
//继续删除前驱,其实这里可以不用递归,因为删除的动作最多递归两次,某个节点的前驱肯定不会有两个孩子节点。
remove(precessor.value);
node.value = precessor.value;
}
}
}
删除的代码逻辑分支比较多,仔细想想也不是很难理解,只是需要考虑的场景多一些。我们跟着第一张图来走一遍代码,假设删除的是头结点62,进入删除代码,首先searchNode找出该节点,root左右孩子都有进入最后一个else分支,查找62的前驱,很明显,进入precessor(62),左子树不为空,则进入getMaxNode(58),58没有右节点直接返回58,58就是62的直接前驱,调用remove(58),因为58只有左子树,只需要将62的左孩子指针指向58的左孩子指针即可。此时将58删除了,但是我们要删除的是62头结点,只需要将头结点的value赋值为前驱的节点值即可。