目录
1、定义
一棵二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个 Comparable的键(以 及相关联的值)且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点 的键。下面是对二叉查找树的图解和结点Node的代码实现。
2、特点
一棵二叉查找树代表了一组键(及其相应的值)的集合,而同 一个集合可以用多棵不同的二叉查找树表示(下图中的两棵二叉树对应的key的值都是一样的)。
3、查找(get)
在一个二叉查找树查找一个元素时,我们会根据二叉查找树的性质(每个父结点的左子结点中的任意结点都比它小,右子结点都比它大),从根节点出发,如果根节点比查找的元素大,则去根节点的左子树中寻找,反之从右子树查找,有该元素为命中,没有则是没命中。
4、插入(put)
在一个二叉查找树插入一个元素时,我们会根据二叉查找树的性质(每个父结点的左子结点中的任意结点都比它小,右子结点都比它大),从根节点出发,如果根节点比查找的元素大,则去根节点的左子树中寻找,反之从右子树查找,如果在树中找到相同的key就更新对应的value,如果没有,就在子结点的末尾添加一个新的结点。同时还需要从新结点出发,向上更新所有父结点的N值。
5、完整代码
public class BST<Key extends Comparable<Key>, Value> {
private Node root;
class Node {
Key key;
Value value;
Node left, right;//左右子结点
int N;//以该结点为父结点的所有子结点的数量
public Node(Key key, Value value, int N) {
this.key = key;
this.value = value;
this.N = N;
}
}
public void put(Key key, Value value) {
root=put(root,key,value);
}
/**
* 先递归向下寻找key所在的位置,如果有则更新对应的value,
* 如果没有,当在树的底部添加了一个新结点之后,还需要
* 从下往上更新所有父结点的N的值。
*/
private Node put(Node node, Key key, Value value) {
if (node==null){
return new Node(key,value,1);
}
int cmp=key.compareTo(node.key);
if (cmp<0) {
node.left=put(node.left,key,value);
}else if (cmp>0){
node.right= put(node.right,key,value);
}else {
node.value=value;
}
node.N=size(node.left)+size(node.right)+1;
return node;
}
public Value get(Key key) {
return get(root, key);
}
/**
* 使用递归的方式,从根节出发,不断向下查找
*/
private Value get(Node node, Key key) {
if (node == null) {
return null;
}
int cmp = key.compareTo(node.key);
if (cmp < 0) {
return get(node.left, key);
} else if (cmp > 0) {
return get(node.right, key);
} else {
return node.value;
}
}
public int size() {
return size(root);
}
public int size(Node node) {
if (node == null) return 0;
return node.N;
}
}
6、分析性能
使用二叉查找树的算法的运行时间取决于树的形状,而 树的形状又取决于键被插入的先后顺序。在最好的情况下, 一棵含有 N 个结点的树是完全平衡的,每条空链接和根结点 的距离都为~ lgN。在最坏的情况下,搜索路径上可能有 N 个结点。但在一般情况下树的形状和最好情 况更接近。
- 命题A:在由 N 个随机键构造的二叉查找树中,查找命中平均所需的比较次数为∼ 2lnN(约 1.39lgN)
- 命题B:在由 N 个随机键构造的二叉查找树中插入操作和查找未命中平均所需的比较次数为 ∼ 2lnN(约 1.39lgN)。
- 命题 A说明在二叉查找树中查找随机键的成本比二分查找高约 39%。命题 B 说明这些额外的 成本是值得的,因为插入一个新键的成本是对数级别的——这是基于二分查找的有序数组所不具备 的灵活性,因为它的插入操作所需访问数组的次数是线性级别的。
7、max()-获取二叉树最大的key
获取二叉查找树最大的key需要从根结点出发,不断地找它的右子结点,直到右边没有任何结点为止。min()同理。
/**
* 获取二叉查找树最大的key值
*/
public Key max() {
if (root == null) return null;
return max(root).key;
}
private Node max(Node x) {
if (x.right == null) return x;
return max(x.right);
}
/**
* 获取二叉查找树最小的key值
*/
public Key min() {
if (root == null) return null;
return min(root).key;
}
private Node min(Node x) {
if (x.left == null) return x;
return min(x.left);
}
8、floor()-获取小于等于key 的最大的key
获取小于等于key的最大key:
首先先要从根节点出发,根节点比key大,那么就需要从左子树中找,如果一直往左走都没比key小的结点,那么就会返回null。
如果当某一个左子结点a比key小,我们不能直接返回该key,因为a还有右子树,还需要在右子树继续找比a更合适的结点。
如果a的右子结点b大于key,那么我们继续从b的左子结点找,如此重复,要么找到和key相等的结点,要么就得找到树的底部为空结点为止。这样才能证明这个结点刚好小于等于key的最大结点。
/**
* 返回小于等于key 的最大的key
*/
public Key floor(Key key) {
Node x = floor(root, key);
if (x == null) return null;
return x.key;
}
private Node floor(Node x, Key key) {
if (x == null) return null;
int cmp = key.compareTo(x.key);
if (cmp == 0) return x;
if (cmp < 0) floor(x.left, key);
Node t = floor(x.right, key);
if (t != null) return t;
else return x;
}
/**
* 返回大于等于key 的最小的key
*/
public Key ceiling(Key key) {
Node x = ceiling(root, key);
if (x == null) return null;
return x.key;
}
private Node ceiling(Node x, Key key) {
if (x == null) return null;
int cmp = key.compareTo(x.key);
if (cmp == 0) return x;
if (cmp > 0) ceiling(x.right, key);
Node t = ceiling(x.left, key);
if (t != null) return t;
else return x;
}
9、select(index)-获取排名为index 的key
获取排名为index的key:
首先呢,我们需要查看根结点的左子结点的N是否大于index,大于说明index在左子树中,小于则说明index在右子树中;
如果在左子树中,那么我们一直向左找,结点的N==index的结点就可以了;
如果在右子树中,那么我们就需要找 (index-左子树中的N-父结点的1=k)除了左子树中的数量N,再找结点N为k的结点。
/**
* 返回排名为index的key
*/
public Key select(int index) {
Node x = select(root, index);
if (x == null) return null;
return x.key;
}
private Node select(Node x, int index) {
if (x == null) return null;
int k = size(x.left);
if (k > index) {
return select(x.left, index);
} else if (k < index) {
return select(x.right, index - k - 1);
} else return x;
}
10、rank(key)-获取比key小的结点数量
/**
* 返回比key小的结点的数量
*/
public int rank(Key key) {
return rank(root, key);
}
private int rank(Node x, Key key) {
if (x == null) return 0;
int cmp=key.compareTo(x.key);
if (cmp<0) return rank(x.left,key);
else if (cmp>0) return 1+size(x.left)+rank(x.right,key);
else return size(x.left);
}
8、delete()删除某个元素
删除某个元素,如果他的有左右子树,那么我们的步骤如下。
- 将指向即将被删除的结点的链接保存为 t;
- 将 x 指向它的后继结点 min(t.right);
- 将 x 的右链接(原本指向一棵所有结点都大于 x.key 的二叉查找树)指向 deleteMin(t. right),也就是在删除后所有结点仍然都大于 x.key 的子二叉查找树;
- 将 x 的左链接(本为空)设为 t.left(其下所有的键都小于被删除的结点和它的后继 结点)。
/**
* 删除最小的元素
*/
private Node deleteMin(Node x) {
if(x.left==null) return x.right;
x.left=deleteMin(x.left);
x.N=size(x.left)+size(x.right)+1;
return x;
}
public void delete(Key key){
root=delete(root,key);
}
public Node delete(Node x,Key key){
if (x==null) return null;
int cmp=key.compareTo(x.key);
if (cmp<0) x.left=delete(x.left,key);
else if (cmp>0) x.right=delete(x.right,key);
else {
if (x.right==null) return x.left;
if (x.left==null) return x.right;
Node t=x;
x=min(t.right);
x.right=deleteMin(t.right);
x.left=t.left;
}
x.N=size(x.left)+size(x.right)+1;
return x;
}