二叉查找树(Binary Search Tree)
一、二叉查找树的定义
----或是一棵空树;或者是具有如下性质的非空二叉树:
(1)左子树的所有结点均小于根的值;
(2)右子树的所有结点均大于根的值;
结论:中序遍历一棵二叉查找树可以得到一个按关键字递增的有序序列。
1、查找
查找的递归实现 :
private Node binTSearchRe(BinTreeNode rt, Object ele) {
if (rt == null)
return null;
switch (strategy.compare(ele, rt.getData())) {
case 0:
return rt; // 等于
case -1:
return binTSearchRe(rt.getLChild(), ele);// 小于
default:
return binTSearchRe(rt.getRChild(), ele);// 大于
}
}
调用
// 返回查找表中与元素ele关键字相同的元素位置;否则,返回null
public Node search(Object ele) {
return binTSearch(root, ele);
}
查找的非递归实现
private Node binTSearch(BinTreeNode rt, Object ele) {
while (rt != null) {
switch (strategy.compare(ele, rt.getData())) {
case 0:
return rt; // 等于
case -1:
rt = rt.getLChild();
continue;// 小于
default:
rt = rt.getRChild(); // 大于
}
}
return null;
}
查找分析
含有n个结点的二叉查找树的平均查找长度和树的形态有关。
在具有n个结点的二叉树中,树的最小高度为log n ,即在最好的情况下二叉查找树的平均查找长度与折半查找一样与log n 成正比。具有n个结点的二叉树可以退化为一个单链表,其深度为n-1,此时其平均查找长度为(n+1)/2,与顺序查找相同,这是最差的情况。在平均情况下,如果随机生成二叉查找树,其平均查找长度和log n 是等数量级的。
2、最大最小值
在二叉查找树中,最小元素总是能够通过根结点向左不断深入,直到到达最左的一个叶子结点找到;而最大元素总是能够通过根结点向右不断深入,直到到达最右的一个叶子结点找到。
Max
public Node max(BinTreeNode v) {
if (v != null)
while (v.hasRChild())
v = v.getRChild();
return v;
}
3、前驱和后续
在二叉查找树中确定某个结点v 的后续结点的算法思想如下:如果结点v 有右子树,那么v 的后续结点是v 的右子树中关键字最小的;如果结点v 右子树为空,并且v 的后续结点存在,那么v 的后续结点是从v(包含v)到根的路径上第一个作为左孩子结点的父结点。
代码:求v在中序遍历序列中的后续结点
// 返回结点v在中序遍历序列中的后续结点
private BinTreeNode getSuccessor(BinTreeNode v) {
if (v == null)
return null;
if (v.hasRChild())
return (BinTreeNode) min(v.getRChild());
while (v.isRChild())
v = v.getParent();
return v.getParent();
}
代码:求v在中序遍历序列中的前驱结点
// 返回结点v在中序遍历序列中的前驱结点
private BinTreeNode getPredecessor(BinTreeNode v) {
if (v == null)
return null;
if (v.hasLChild())
return (BinTreeNode) max(v.getLChild());
while (v.isLChild())
v = v.getParent();
return v.getParent();
}
4、插入
为了判定新结点的插入位置,需要从根结点开始逐层深入,判断新结点关键字与各子树根结点关键字的大小,若新结点关键字小,则向相应根结点的左子树深入,否则向右子树深入;直到向某个结点的左子树深入而其左子树为空,或向某个结点的右子树深入而其右子树为空时,则确定了新结点的插入位置。
// 按关键字插入元素ele
public void insert(Object ele) {
BinTreeNode p = null;
BinTreeNode current = root;
while (current != null) { // 找到待插入位置
p = current;
if (strategy.compare(ele, current.getData()) < 0)
current = current.getLChild();
else
current = current.getRChild();
}
if (p == null)
root = new BinTreeNode(ele); // 树为空
else if (strategy.compare(ele, p.getData()) < 0)
p.setLChild(new BinTreeNode(ele));
else
p.setRChild(new BinTreeNode(ele));
}
5、删除算法
对于二叉查找树,删除树上一个结点相当于删除有序序列中的一个记录,删除后仍需保持二叉查找树的特性。在二叉查找树中删除的结点不总是叶子结点,因此在删除一个非叶子结点时需要处理该结点的子树。
如何删除一个结点?
下面我们分三种情况讨论结点v 的删除:
⑴如果结点v为叶子结点,即其左右子树Pl和Pr均为空,此时可以直接删除该叶子结点v,而不会破坏二叉查找树的特性,因此直接从树中摘除v即可。
⑵如果结点v只有左子树Pl或只有右子树Pr,此时,当结点v是左孩子时,只要令Pl或Pr为其双亲结点的左子树即可;当结点v是右孩子时,只要令Pl或Pr为其双亲结点的右子树即可。
⑶如果结点v既有左子树Pl又有右子树Pr,此时,不可能进行如上简单处理。为了在删除结点v之后,仍然保持二叉查找树的特性,我们必须保证删除v之后,树的中序序列必须仍然有序。为此,我们可以先用中序序列中结点v的前驱或后序替换v,然后删除其前驱或后序结点即可,此时v的前驱或后序结点必然是没有右孩子或没有左孩子的结点,其删除操作可以使用前面规定的方法完成。
情况一举例:由于结点2 是叶子结点,则直接摘除即可。
情况2举例:例如在图所示的二叉查找树中删除结点3 和7,由于3 是左孩子,因此在删除结点3 之后,将其右子树设为其父结点6 的左子树即可;同样,因为7 是右孩子,因此在删除结点7之后,将其右子树设为其父结点6 的右子树即可。
情况3举例:例如在图所示的树中删除结点15,由于结点15既有左子树又有右子树,则此时,可以先找到其前驱结点13,并用13替换15,然后删除结点13即可。
public Object remove(Object ele) {
BinTreeNode v = (BinTreeNode) binTSearch(root, ele);
if (v == null)
return null; // 查找失败
BinTreeNode del = null; // 待删结点
BinTreeNode subT = null; // 待删结点的子树
if (!v.hasLChild() || !v.hasRChild()) // 确定待删结点
del = v;
else {
del = getPredecessor(v);
Object old = v.getData();
v.setData(del.getData());
del.setData(old);
}
// 此时待删结点只有左子树或右子树
if (del.hasLChild())
subT = del.getLChild();
else
subT = del.getRChild();
if (del == root) { // 若待删结点为根
if (subT != null)
subT.sever(); //断开与父亲的关系
root = subT;
} else if (subT != null) {
// del为非叶子结点
if (del.isLChild())
del.getParent().setLChild(subT);
else
del.getParent().setRChild(subT);
} else
// del为叶子结点
del.sever();
return del.getData();
}
将数据元素构造成二叉查找树的优点:
①查找过程与顺序结构有序表中的折半查找相似,查找效率高;
②中序遍历此二叉树,将会得到一个关键字的有序序列(即实现了排序运算);
③如果查找不成功,能够方便地将被查元素插入到二叉树的叶子结点上,而且插入或删除时只需修改指针而不需移动元素。
总结:二叉查找树既有类似于折半查找的特性,又采用了链表存储,它是动态查找表的一种适宜表示。若数据元素的输入顺序不同,则得到的二叉查找树形态也不同。
附:全部代码
package dsa.adt;
import dsa.adt.BinaryTreeLinked;
import dsa.adt.SearchTable;
import dsa.strategy.Strategy;
import dsa.strategy.DefaultStrategy;
public class BSTree extends BinaryTreeLinked implements SearchTable {
protected BinTreeNode startBN; // 在AVL树中重新平衡的起始结点
// 构造方法
public BSTree() {
this(new DefaultStrategy());
}
public BSTree(Strategy strategy) {
this.root = null;
this.strategy = strategy;
startBN = null;
}
// 查询查找表当前的规模
public int getSize() {
return root == null ? 0 : root.getSize();
}
// 判断查找表是否为空
public boolean isEmpty() {
return getSize() == 0;
}
// 返回查找表中与元素ele关键字相同的元素位置;否则,返回null
public Node search(Object ele) {
return binTSearch(root, ele);
}
private Node binTSearchRe(BinTreeNode rt, Object ele) {
if (rt == null)
return null;
switch (strategy.compare(ele, rt.getData())) {
case 0:
return rt; // 等于
case -1:
return binTSearchRe(rt.getLChild(), ele);// 小于
default:
return binTSearchRe(rt.getRChild(), ele);// 大于
}
}
private Node binTSearch(BinTreeNode rt, Object ele) {
while (rt != null) {
switch (strategy.compare(ele, rt.getData())) {
case 0:
return rt; // 等于
case -1:
rt = rt.getLChild();
break;// 小于
default:
rt = rt.getRChild(); // 大于
}
}
return null;
}
// 返回所有关键字与元素ele相同的元素位置
public Iterator searchAll(Object ele) {
LinkedList list = new LinkedListDLNode();
binTSearchAll(root, ele, list);
return list.elements();
}
public void binTSearchAll(BinTreeNode rt, Object ele, LinkedList list) {
if (rt == null)
return;
int comp = strategy.compare(ele, rt.getData());
if (comp <= 0)
binTSearchAll(rt.getLChild(), ele, list);
if (comp == 0)
list.insertLast(rt);
if (comp >= 0)
binTSearchAll(rt.getRChild(), ele, list);
}
// 按关键字插入元素ele
public void insert(Object ele) {
BinTreeNode p = null;
BinTreeNode current = root;
while (current != null) { // 找到待插入位置
p = current;
if (strategy.compare(ele, current.getData()) < 0)
current = current.getLChild();
else
current = current.getRChild();
}
startBN = p; // 待平衡出发点
if (p == null)
root = new BinTreeNode(ele); // 树为空
else if (strategy.compare(ele, p.getData()) < 0)
p.setLChild(new BinTreeNode(ele));
else
p.setRChild(new BinTreeNode(ele));
}
// 若查找表中存在与元素ele关键字相同元素,则删除一个并返回;否则,返回null
public Object remove(Object ele) {
BinTreeNode v = (BinTreeNode) binTSearch(root, ele);
if (v == null)
return null; // 查找失败
BinTreeNode del = null; // 待删结点
BinTreeNode subT = null; // del的子树
if (!v.hasLChild() || !v.hasRChild()) // 确定待删结点
del = v;
else {
del = getPredecessor(v);
Object old = v.getData();
v.setData(del.getData());
del.setData(old);
}
startBN = del.getParent(); // 待平衡出发点
// 此时待删结点只有左子树或右子树
if (del.hasLChild())
subT = del.getLChild();
else
subT = del.getRChild();
if (del == root) { // 若待删结点为根
if (subT != null)
subT.sever();
root = subT;
} else if (subT != null) {
// del为非叶子结点
if (del.isLChild())
del.getParent().setLChild(subT);
else
del.getParent().setRChild(subT);
} else
// del为叶子结点
del.sever();
return del.getData();
}
// 返回以v为根的二叉查找树中最小(大)元素的位置
public Node min(BinTreeNode v) {
if (v != null)
while (v.hasLChild())
v = v.getLChild();
return v;
}
public Node max(BinTreeNode v) {
if (v != null)
while (v.hasRChild())
v = v.getRChild();
return v;
}
// 返回结点v在中序遍历序列中的前驱结点
private BinTreeNode getPredecessor(BinTreeNode v) {
if (v == null)
return null;
if (v.hasLChild())
return (BinTreeNode) max(v.getLChild());
while (v.isLChild())
v = v.getParent();
return v.getParent();
}
// 返回结点v在中序遍历序列中的后续结点
private BinTreeNode getSuccessor(BinTreeNode v) {
if (v == null)
return null;
if (v.hasRChild())
return (BinTreeNode) min(v.getRChild());
while (v.isRChild())
v = v.getParent();
return v.getParent();
}
}