二叉查找树是具有如下性质的一种二叉树:对于任一结点x,x的左子树结点的关键字均不大于x,右子树结点的关键字均不小于x。
二叉查找树的特点是位置决定了顺序,所以在不对关键字进行排序的情况下,通过位置关系就能找到特定大小的关键字结点。例如,对二叉查找树进行中根遍历就能按升序输出所有的关键字。
二叉查找树的搜索:从根节点开始,对于树中任一结点k,若k的关键字大于待搜索的值x,则搜索k的左子树;若小于,则搜索k的右子树;若等于,则找到。
最大关键字和最小关键字:最大关键字是二叉查找树的最右叶结点,最小关键字是最左叶结点。
前趋和后继:某结点的前趋是指关键字比该结点小的所有结点中最大的一个,而后继则是关键字比该结点大的所有结点中最小的一个。就后继而言:
1. 若结点x的右子树非空,则x的后继是x的右子树的最左叶结点;
2. 若结点x的右子树为空,则x的后继是x的最低祖先结点,且该后继结点的左儿子也必须是x的祖先。
前趋则跟后继正好对称。
插入:与二叉查找树的搜索相类似,小于当前结点的值则向左,大于当前结点的值则向右,最终将待插入的值作为满足当前二叉查找树性质的某个叶结点。
删除:对于待删结点z:
1. 若z没有左右子结点(即z为叶结点),直接删除z;
2. 若z只有一个子结点,删除z,并将z的子结点接到z的父结点的对应位置;
3. 若z左右子结点都有,则找到z的后继y,删除y(以1或2中的方式,因为后继必然至多有一个子结点),再将y的数据拷贝到z中。
二叉查找树的好处是它的上述7种操作(搜索、最大、最小、前趋、后继、插入、删除)的时间复杂度都是O(h),其中h是二叉树的树高。因此它的最差时间复杂度也才只有O(n),平均和最佳时间复杂度有只有O(lgn)。
如下是二叉查找树的一种Java实现:
package cn.liujian;
public class BinaryTreeNode<E extends Comparable<E>> {
private E value;
private BinaryTreeNode<E> left;
private BinaryTreeNode<E> right;
private BinaryTreeNode<E> parent;
public BinaryTreeNode(E e) {
if (e == null) {
throw new IllegalArgumentException();
}
value = e;
left = null;
right = null;
parent = null;
}
public E getValue() {
return value;
}
public void setValue(E value) {
if (value == null) {
throw new IllegalArgumentException();
}
this.value = value;
}
public BinaryTreeNode<E> getLeft() {
return left;
}
public void setLeft(BinaryTreeNode<E> left) {
this.left = left;
}
public BinaryTreeNode<E> getRight() {
return right;
}
public void setRight(BinaryTreeNode<E> right) {
this.right = right;
}
public BinaryTreeNode<E> getParent() {
return parent;
}
public void setParent(BinaryTreeNode<E> parent) {
this.parent = parent;
}
// 为以本结点为根的子树插入新结点
public void addChild(BinaryTreeNode<E> child) {
if (child.getValue() == null) {
throw new IllegalArgumentException();
}
if (child.getValue().compareTo(value) <= 0) {
if (left == null) {
left = child;
child.setParent(this);
} else {
left.addChild(child);
}
} else {
if (right == null) {
right = child;
child.setParent(this);
} else {
right.addChild(child);
}
}
}
// 二叉树搜索
public BinaryTreeNode<E> searchNode(E value) {
if (value == null) {
return null;
}
// 递归实现
if (this.value.compareTo(value) == 0) {
return this;
} else if (this.value.compareTo(value) > 0) {
return (left != null) ? left.searchNode(value) : null;
} else {
return (right != null) ? right.searchNode(value) : null;
}
// // 迭代实现
// BinaryTreeNode<E> target = this;
// while (target != null && target.getValue().compareTo(value) != 0) {
// if (target.getValue().compareTo(value) > 0) {
// target = target.getLeft();
// } else {
// target = target.getRight();
// }
// }
// return target;
}
// 从以本结点为根的子树中删除结点
public BinaryTreeNode<E> deleteNode(E value, BinaryTreeNode<E> root) {
BinaryTreeNode<E> node = searchNode(value);
BinaryTreeNode<E> target = node;
if (node != null) {
if (node.getLeft() != null && node.getRight() != null) {
target = node.successor();
}
BinaryTreeNode<E> child = (target.getLeft() != null) ? target.getLeft() : target.getRight();
if (child != null) {
child.setParent(target.getParent());
}
if (target.getParent() == null) {
root = child;
} else {
if (target.getParent().getLeft() == target) {
target.getParent().setLeft(child);
} else {
target.getParent().setRight(child);
}
}
if (target != node) {
node.setValue(target.getValue());
}
}
return target;
}
// 返回本子树中的最小结点
public BinaryTreeNode<E> getMinNode() {
BinaryTreeNode<E> target = this;
while (target.getLeft() != null) {
target = target.getLeft();
}
return target;
}
// 返回本子树中的最大结点
public BinaryTreeNode<E> getMaxNode() {
BinaryTreeNode<E> target = this;
while (target.getRight() != null) {
target = target.getRight();
}
return target;
}
// 返回本结点的后继结点
public BinaryTreeNode<E> successor() {
if (right != null) {
return right.getMinNode();
}
BinaryTreeNode<E> target = parent;
BinaryTreeNode<E> child = this;
while (target != null && child == target.getRight()) {
child = target;
target = target.getParent();
}
return target;
}
// 返回本结点的前趋结点
public BinaryTreeNode<E> predecessor() {
if (left != null) {
return left.getMaxNode();
}
BinaryTreeNode<E> target = parent;
BinaryTreeNode<E> child = this;
while (target != null && child == target.getLeft()) {
child = target;
target = target.getParent();
}
return target;
}
}
基于这样一棵二叉查找树,可以很容易根据位置关系找到一组数据的顺序中间值(第m = size / 2 + 1小的值)。当然,需要对上述代码进行修改,以得到二叉查找树的大小size、根结点左子树结点数a和根结点右子树结点数b。思路是:
1. 若a = m – 1,则根结点就是中间值;
2. 若a > m – 1,则中间值在根结点的左子树中,且是根结点的第a – m + 1个前趋;
3. 若a < m – 1,则中间值在根结点的右子树中,且是根结点的第b + m – size个后继。
可见,如果用二叉查找树来查找中间值,其运行时间始终是线性的,而且如果树平衡度高的话,运行时间也更少。