概念:
二叉搜索树是一种节点值之间具有一定数量级次序的二叉树。对于树中的每个节点:
- 若其左子树存在,则其左子树中每个节点的值都不大于该节点的值。
- 若其右子树存在,则其右子树中每个节点的值都不小于该节点的值。
- 该节点的左右子树都是二叉搜索树。
- 没有键值相等的节点。
public class BSTree<T extends Comparable<T>> {
private BSTNode<T> mRoot;
@Data
public class BSTNode<T extends Comparable<T>> {
T key;
BSTNode<T> left;
BSTNode<T> right;
BSTNode<T> parent;
public BSTNode() {
super();
}
public BSTNode(T key, BSTNode<T> left, BSTNode<T> right, BSTNode<T> parent) {
this.key = key;
this.left = left;
this.right = right;
this.parent = parent;
}
}
}
前驱与后继:
前驱:该节点左子树中最大的节点。数值紧挨着小于的值。
public BSTNode<T> predecessor(BSTNode<T> x) {
// 如果x存在左孩子,则"x的前驱结点"为 "以其左孩子为根的子树的最大结点"。
if (x.left != null)
return maximum(x.left);
// 如果x没有左孩子。则x有以下两种可能:
// (01) x是"一个右孩子",则"x的前驱结点"为 "它的父结点"。
// (01) x是"一个左孩子",则查找"x的最低的父结点,并且该父结点要具有右孩子",找到的这个"最低的父结点"就是"x的前驱结点"。
BSTNode<T> y = x.parent;
while ((y != null) && (x == y.left)) {
x = y;
y = y.parent;
}
return y;
}
后继:该节点右子树中最小的节点。数值紧挨着大于的值。
public BSTNode<T> successor(BSTNode<T> x) {
// 如果x存在右孩子,则"x的后继结点"为 "以其右孩子为根的子树的最小结点"。
if (x.right != null)
return minimum(x.right);
// 如果x没有右孩子。则x有以下两种可能:
// (01) x是"一个左孩子",则"x的后继结点"为 "它的父结点"。
// (02) x是"一个右孩子",则查找"x的最低的父结点,并且该父结点要具有左孩子",找到的这个"最低的父结点"就是"x的后继结点"。
BSTNode<T> y = x.parent;
while ((y != null) && (x == y.right)) {
x = y;
y = y.parent;
}
return y;
}
查找时间复杂度:
O(log2(n))~O(n)
- 针对第一种极限情况:完全二叉树中树的深度与节点的个数为n=2^(d+1)-1。设二叉树中的节点格式为nc。那么nc<=2^(d+1)-1。那么就有,d+1>=log2(nc+1),因为d+1为查找次数,完全二叉树中查找次数为(log2(nc+1))。
- 针对第二种极限情况:查找过程类似于数组的遍历,所以查找次数为O(n)。
递归查找:
private BSTNode<T> search(BSTNode<T> x, T key) {
if (x == null)
return x;
int cmp = key.compareTo(x.key);
if (cmp < 0)
return search(x.left, key);
else if (cmp > 0)
return search(x.right, key);
else
return x;
}
public BSTNode<T> search(T key) {
return search(mRoot, key);
}
非递归查找:
private BSTNode<T> iterativeSearch(BSTNode<T> x, T key) {
while (x != null) {
int cmp = key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else if (cmp > 0)
x = x.right;
else
return x;
}
return x;
}
public BSTNode<T> iterativeSearch(T key) {
return iterativeSearch(mRoot, key);
}
构造过程:
二叉树的构造过程与二叉树的搜索过程大致相同。
不同的地方为:
- 查询的过程为,比较元素值是否相等,相等则返回,不相等则判断大小情况,迭代查询左右子树,直到找到相等的元素,或子节点为空,返回节点不存在。
- 插入的过程为,比较元素值是否相等,相等则返回表示已存在,不相等则判断大小情况,迭代查询左右子树,直到找到相等的元素,或子节点为空,则将节点插入该空节点位置。
private void insert(BSTree<T> bst, BSTNode<T> z) {
int cmp;
BSTNode<T> y = null;
BSTNode<T> x = bst.mRoot;
// 查找z的插入位置
while (x != null) {
y = x;
cmp = z.key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else
x = x.right;
}
z.parent = y;
if (y == null)
bst.mRoot = z;
else {
cmp = z.key.compareTo(y.key);
if (cmp < 0)
y.left = z;
else
y.right = z;
}
}
public void insert(T key) {
BSTNode<T> z = new BSTNode<T>(key, null, null, null);
// 如果新建结点失败,则返回。
if (z != null)
insert(this, z);
}
删除过程:
二叉搜索树的删除具有三种情况:
- 删除度为0的节点。
- 删除度为1的节点。
- 删除度为2的节点。
删除度为0的节点:
度为0的节点,这种节点没有左右子树,是叶子节点直接删除即可。
删除度为1的节点:
度为1的节点,这种节点只有左子树或者右子树的一个,这种节点删除后,为了满足二叉搜索树的结构特性,需要将其的左子树或者右子树上移。
删除度为2的节点:
度为2的节点,这种节点同时具备左子树和右子树,删除后,为了维持二叉搜索树的结构特性,需要在其的左子树中查找出来一个最大值来填充删除的节点。
实际操作为:
- 查找出左子树中的最大值节点Nm。
- 替换待删除节点的值N为Nm。
- 删除Nm节点。
因为Nm作为左子树的最大值节点,必定是度为0或者1的节点,这样就转化为上面的情况。
个人认为这里也可以用右子树的最小值。
private BSTNode<T> remove(BSTree<T> bst, BSTNode<T> z) {
BSTNode<T> x = null;
BSTNode<T> y = null;
if ((z.left == null) || (z.right == null))
y = z;
else
y = successor(z);
if (y.left != null)
x = y.left;
else
x = y.right;
if (x != null)
x.parent = y.parent;
if (y.parent == null)
bst.mRoot = x;
else if (y == y.parent.left)
y.parent.left = x;
else
y.parent.right = x;
if (y != z)
z.key = y.key;
return y;
}
public void remove(T key) {
BSTNode<T> z, node;
if ((z = search(mRoot, key)) != null)
if ((node = remove(this, z)) != null)
node = null;
}
操作:
查找最大的节点:
private BSTNode<T> maximum(BSTNode<T> tree) {
if (tree == null)
return null;
while (tree.right != null)
tree = tree.right;
return tree;
}
public T maximum() {
BSTNode<T> p = maximum(mRoot);
if (p != null)
return p.key;
return null;
}
查找最小的节点:
private BSTNode<T> minimum(BSTNode<T> tree) {
if (tree == null)
return null;
while (tree.left != null)
tree = tree.left;
return tree;
}
public T minimum() {
BSTNode<T> p = minimum(mRoot);
if (p != null)
return p.key;
return null;
}
打印二叉搜索树:
private void print(BSTNode<T> tree, T key, int direction) {
if (tree != null) {
if (direction == 0) // tree是根节点
System.out.printf("%2d is root\n", tree.key);
else // tree是分支节点
System.out.printf("%2d is %2d's %6s child\n", tree.key, key, direction == 1 ? "right" : "left");
print(tree.left, tree.key, -1);
print(tree.right, tree.key, 1);
}
}
public void print() {
if (mRoot != null)
print(mRoot, mRoot.key, 0);
}
销毁二叉搜索树:
private void destroy(BSTNode<T> tree) {
if (tree == null)
return;
if (tree.left != null)
destroy(tree.left);
if (tree.right != null)
destroy(tree.right);
tree = null;
}
public void clear() {
destroy(mRoot);
mRoot = null;
}
总结:
二叉搜索树的查询,构造和删除的性能与树的高度有关。相对于数组只存储数值,二叉树多了存储指针占用的空间,二叉树是一种以空间换时间的思路。如果能保持二叉树尽可能的平衡,避免向类似于数组的斜树发展,其时间复杂度会有显著降低。
来源:oschina
链接:https://my.oschina.net/linqiankun/blog/3197451