常用查找算法之二叉查找树
定义
一棵二叉查找树(又称二叉排序树、二叉搜索树)是一棵二叉树,其中每个节点都含有一个Comparable的键以及相关联的值且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键。
二叉查找树有一个重要性质,就是它的中序遍历结果递增排序。
基本实现
树由Node
对象组成,每个对象都含有一对键值、两条链接和一个节点计数器。节点计数器表示以该节点为根的子树中的节点总数,总是满足size(x) = size(x.left) + size(x.right) + 1
。
public class BST<Key extends Comparable<Key>, Value> {
private Node root; // 二叉查找树的根节点
private class Node {
private Key key; // 键
private Value val; // 值
private Node left, right; // 指向子树的链接
private int size; // 以该节点为根的子树中的节点总数
public Node(Key key, Value val, int size) {
this.key = key;
this.val = val;
this.size = size;
}
}
public int size() {
return size(root);
}
private int size(Node x) {
if (x == null) return 0;
else return x.size;
}
//实现见下文
public Value get(Key key)
//实现见下文
public void put(Key key, Value val)
//其它有序性相关的方法及删除操作见下文
}
查找
如果树是空的,则查找未命中;如果被查找的键和根节点的键相等,查找命中;否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。
当找到一个含有被查找的键的节点(命中)或者当前子树变为空(未命中)时这个过程才会结束。
public Value get(Key key) {
return get(root, key);
}
private Value get(Node x, Key key) {
if (x == null)
return null;
int cmp = key.compareTo(x.key);
if (cmp < 0) return get(x.left, key);
else if (cmp > 0) return get(x.right, key);
else return x.val;
}
插入
如果树是空的,就返回一个含有该键值对的新节点(使上层节点的链接指向该节点);如果被查找的键小于根节点的键,继续在左子树中插入该键,否则在右子树中插入该键。
@Override
public void put(Key key, Value value) {
root = put(root, key, value);
}
private Node put(Node x, Key key, Value val) {
if (x == null) return new Node(key, val, 1);
int cmp = key.compareTo(x.key);
if (cmp < 0) x.left = put(x.left, key, val);
else if (cmp > 0) x.right = put(x.right, key, val);
else x.val = val;
x.size = 1 + size(x.left) + size(x.right);
return x;
}
有序性相关的方法及删除操作
范围查找
利用二叉查找树中序遍历的结果为递增的特点对其进行指定范围的查找。
Override
public List<Key> keys(Key l, Key h) {
List<Key> list = new ArrayList<>();
keys(root, list, l, h);
return list;
}
private void keys(Node x, List<key> list, Key l, Key h) {
if (x == null)
return;
int cmpL = l.compareTo(x.key);
int cmpH = h.compareTo(x.key);
if (cmpL < 0) keys(x.left, list, l, h);
if (cmpL <= 0 && cmpH >= 0) list.add(x.key);
if (cmpH > 0) keys(x.right, list, l, h);
}
删除最小节点
只需令指向最小节点的链接指向最小节点的右子树。
public void deleteMin() {
root = deleteMin(root);
}
private Node deleteMin(Node x) {
if (x.left == null)
return x.right;
x.left = deleteMin(x.left);
x.size = size(x.left) + size(x.right) + 1;
return x;
}
删除指定节点
如果待删除的节点只有一个子树, 那么只需要让指向待删除节点的链接指向唯一的子树即可;否则,让右子树的最小节点替换该节点。
public void delete(Key key) {
root = delete(root, key);
}
private 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.size = size(x.left) + size(x.right) + 1;
return x;
}
查找最小键
@Override
public Key min() {
return min(root).key;
}
private Node min(Node x) {
if (x == null)
return null;
if (x.left == null)
return x;
return min(x.left);
}
排名
rank(key)
返回key的排名,排名从0开始。如果键和根节点的键相等,返回左子树的节点数;如果小于,递归计算在左子树中的排名;如果大于,递归计算在右子树中的排名,加上左子树的节点数,再加上1(根节点)。
@Override
public int rank(Key key) {
return rank(key, root);
}
private int rank(Key key, Node x) {
if (x == null) return 0;
int cmp = key.compareTo(x.key);
if (cmp < 0) return rank(key, x.left);
else if (cmp > 0) return 1 + size(x.left) + rank(key, x.right);
else return size(x.left);
}
复杂度分析
在最好的情况下,一棵含有N个节点的树是完全平衡的,插入和查找的时间复杂度均为O(logn);在最坏的情况下,搜索路径上可能有N个节点,此时的时间复杂度为O(n)。