1、定义
将链表插入的灵活性和有序数组查找的高效性结合起来的符号表实现——二叉查找树
定义:一棵 二叉查找树(BST)
是一棵二叉树,每个节点都含有一个Comparable的键(以及对应的值)。且每个节点的键都大于其左子树中任意节点的键而小于右子树中任意节点的键。
2、数据表示
嵌套定义一个私有类 Node 来表示二叉查找树上的一个结点,每个结点都有一个键,一个值,一条左链接,一条右链接。左链接指向一棵由小于该结点的所有键组成的二叉查找树,右链接指向一棵由大于该结点的所有键组成的二叉查找树。
变量 N 给出以该结点为根的子树的结点总数。
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 N; //以该结点为根的子树中的结点总数
public Node(Key key,Value val,int N){
this.key=key;this.val=val;this.N=N;
}
}
public int size(){
return size(root);
}
private int size(Node x) {
if(x==null) return 0;
else return x.N;
}
public Value get(Key key)
public void put(Key key,Value val)
//max()、min()、floor()等
}
3、基本实现
3.1 查找
在二叉查找树中 递归/非递归 查找 get() 一个键:
– 如果二叉查找树为空,查找失败(search miss),返回null;
– 如果根节点的键等于要查找的键,返回根节点的值(search hit)。
– 否则,继续在相应的子树中查找。如果要查找的键小于根节点的键,在左子树中查找;否则在右子树中查找。
– 重复上述步骤,直至search miss或者search hit。
//根据键找对应的值,如果找不到返回Null
public Value get(Key key) {
return get(root,key);
}
/* 在以x为根节点的子树中查找,并返回key所对应的值; 如果找不到则返回null */
/* 基于非递归 */
private Value get(Node x, Key key) {
while (x != null) {
int cmp = key.compareTo(x.key);
if(cmp > 0) x = x.right;
else if(cmp < 0) x = x.left;
else return x.val;
}
return null;
}
/* 基于递归 */
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;
}
3.2 插入
插入的逻辑:
– 如果二叉查找树是空的,生成一个新节点
– 如果要插入的键 = 根节点键,更新根节点的值。
– 如果要插入的键 < 根节点键,在左子树插入,并将根节点的左链接指向插入后的左子树。(重置父、子节点间的链接)
– 如果要插入的键 > 根节点键,在右子树插入,并将根节点的右链接指向插入后的右子树。
– 更新根节点的size,并返回根节点作为插入新节点后的新二叉树根节点。
– 重复上述步骤,直至插入或者更新成功。
//向树中插入结点
public void put(Key key, Value val) {
root = put(root, key, val);
}
/* 如果key存在于以 x为根节点 的子树中则更新它的值;否则将以key和val为键值对的新节点插入到该子树中 */
/* 基于递归 */
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.N = size(x.left)+size(x.right)+1; //递归实现的,在插入之后更新节点的size
return x;
}
/* 基于非递归 */
public void put(Key key, Value val) {
if(root == null) { //插入根节点的情况
root = new Node(key, val);
return;
}
boolean alreadyin = contains(key); //判断要插入的键值对是否已经在二叉树中
Node cur = root;
Node parent = null; //记录父节点
while(cur != null) {
cur.N = alreadyin ? cur.N : cur.N+1; //边搜索边更新size
parent = cur;
int cmp = key.compareTo(cur.key);
if(cmp < 0) cur = cur.left;
else if(cmp > 0) cur = cur.right;
else {
cur.val = val;
return;
}
}
if(key.compareTo(parent.key) < 0)
parent.left = new Node(key, val);
else
parent.right = new Node(key, val);
}
非递归实现的比较麻烦,需要处理一些特殊情况:
A、需要记录父节点,用于更新父节点的链接
B、还要处理一个特殊情况,就是根节点就是要插入的位置
C、还要多调用一次get,用于判断要插入的键值对是否已经在二叉树中。如果在就不用更新size,否则就需要更新size
D、非递归实现更新size 跟 递归实现的顺序相反*,要边搜索边更新size
注:后边只要改变二叉搜索树结构的,非递归实现都需要考虑这些特殊情况,如insert或delete等。
3.3 删除
3.3.1 删除最大键和删除最小键
deleteMin():不断深入根节点的左子树中直至遇见一个空链接,然后将指向该节点的链接指向该节点的右子树。此时已经没有任何链接指向要被删除的结点,因此它会被垃圾收集器清理掉。
/*删除最小键所对应的键值对*/
public void deleteMin() {
root = deleteMin(root);
}
/* 基于递归 */
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;
}
/* 基于非递归 */
private Node deleteMin(Node x) {
if(x.left == null) return x.right; //根节点是要删除的结点
Node cur = x;
Node parent = null; //记录父节点
while(cur.left != null) {
cur.N--; //边搜索边更新size
parent = cur;
cur = cur.left;
}
parent.left = cur.right;
return x;
}
3.3.2 删除操作
删除没有子节点的结点
删除只有一个子节点的结点
删除拥有两个子节点的结点:删除结点x后,用它的后继结点
填补它的位置。它的后继结点
就是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;
else 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;
}
/* 基于非递归 */
public void delete(Key key) {
if(key.compareTo(root.key) == 0) {
deleteRoot();
return;
}
Node cur = root;
Node parent = null;
while(cur != null) {
int cmp = key.compareTo(cur.key);
if(cmp < 0) {
cur.size--;
parent = cur;
cur = cur.left;
}
else if(cmp > 0){
cur.size--;
parent = cur;
cur = cur.right;
}
else {
int parentcmp = key.compareTo(parent.key);
if(cur.left == null) {
if(parentcmp < 0) parent.left = cur.right;
else parent.right = cur.right;
return;
}
if(cur.right == null) {
if(parentcmp < 0) parent.left = cur.left;
else parent.right = cur.left;
return;
}
Node temp = cur;
cur = min(temp.right);
cur.right = deleteMin(temp.right);
cur.left = temp.left;
cur.size = size(cur.left) + size(cur.right) + 1;
if(parentcmp < 0) parent.left = cur;
else parent.right = cur;
return;
}
}
}
3.4 遍历
前序遍历:访问根结点; 先序遍历左子树; 先序遍历右子树
中序遍历:中序遍历左子树; 访问根结点; 中序遍历右子树
后序遍历:后序遍历左子树; 后序遍历右子树; 访问根节点
/*前序遍历*/
public void preOrder() {
preOrder(root);
}
private void preOrder(Node x) {
if(x == null) return;
System.out.print(x.key+" ");
preOrder(x.left);
preOrder(x.right);
}
/*后序遍历*/
public void postOrder() {
postOrder(root);
}
private void postOrder(Node x) {
if(x == null) return;
postOrder(x.left);
postOrder(x.right);
System.out.print(x.key+" ");
}
3.5 其他操作
3.5.1 最大键和最小键
/*找到最小的键*/
/*如果根结点左子树为空,那么根结点为最小键*/
/*如果左链接非空,那么最小键为左子树的最小键*/
public Key min() {
return min(root);
}
private Key min(Node x) {
if(x == null) return null;
while (x.left != null) {
x = x.left;
}
return x.key;
}
3.5.2 前驱和后继
前驱:数据值小于该结点的最大结点
后继:数据值大于该结点的最小结点
(附:前驱后继算法参考此处)
/*查找前驱结点*/
public Node predecessor(Node x) {
// 如果x存在左孩子,则x的前驱为 "以其左孩子为根的子树的最大结点"。
if (x.left != null)
return max(x.left);
// 如果x没有左孩子。则x有以下两种可能:
// (1) x是"一个右孩子",则x的前驱为 "它的父结点"。
// (2) x是"一个左孩子",则查找x的最低父结点,并且该父结点具有右孩子(为父节点)。也即是方向发生改变的那个最低父节点
Node y = x.parent;
while ((y!=null) && (x==y.left)) { //直到链接方向发生改变,不再是左孩子的方向时,该祖先结点即为其前驱
x = y;
y = y.parent;
}
return y;
}
/*查找后继结点*/
public Node successor(Node x) {
// 如果x存在右孩子,则x的后继为 "以其右孩子为根的子树的最小结点"。
if (x.right != null)
return min(x.right);
// 如果x没有右孩子。则x有以下两种可能:
// (1) x是"一个左孩子",则x的后继为 "它的父结点"。
// (2) x是"一个右孩子",则查找x的最低父结点,并且该父结点具有左孩子(为父节点)。也即是方向发生改变的那个最低父节点
Node y = x.parent;
while ((y!=null) && (x==y.right)) { //直到链接方向发生改变,不再是右孩子的方向时,该祖先结点即为其后继
x = y;
y = y.parent;
}
return y;
}
3.5.3 向上取整和向下取整
/*向上取整 - 小于等于key的最大键*/
/*如果key小于二叉查找树的根节点的键,那么floor一定在根节点的左子树中*/
/*如果key大于二叉查找树的根节点的键,那么只有当根节点右子树中存在小于等于key的结点时,floor才会出现在右子树中,
否则根节点就是floor*/
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) return floor(x.left, key);
Node t = floor(x.right, key);
if (t!=null) return t;
else return x;
}
3.5.4 选择 排名为k的结点
size() 是专门为 select() 和 rank() 等函数准备的。
/*返回排名为k的键*/
/*如果左子树中结点数 t>k ,就(递归地)在左子树中查找排名为 k 的键*/
/*如果左子树中结点数 t=k , 就返回根节点的键*/
/*如果左子树中结点数 t<k , 就(递归地)在右子树中查找排名为(k-t-1)的键*/
public Key select(int k) {
return select(root, k).key;
}
private Node select(Node x,int k){
if(x==null) return null;
int t = size(x.left);
if (t > k) return select(x.left, k);
else if (t < k) return select(x.right, k-t-1);
else return x;
}
3.5.5 排名 返回给定键的排名
rank() 是 select() 的逆方法,也用到了size()
/*返回以x为根节点的子树中小于x.key的键的数量*/
/*如果给定的键 = 根节点的键,就返回左子树中的结点总数t */
/*如果给定的键 < 根节点的键,就返回该键在左子树中的排名 */
/*如果给定的键 > 根节点的键,就返回 t+1 (根节点)加上它在右子树中的排名 */
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);
}
3.5.6 范围查找
返回给定范围内的键,将所有符合的键加入一个队列Queue
/*返回在(lo-hi)范围内的键*/
public Iterable<Key> keys() {
return keys(min(), max());
}
public Iterable<Key> keys(Key lo, Key hi) {
LinkedList<Key> queue = new LinkedList<>();
keys(root, queue, lo, hi);
return queue;
}
private void keys(Node x, LinkedList<Key> queue, Key lo, Key hi) {
if(x == null) return;
int cmplo = lo.compareTo(x.key);
int cmphi = hi.compareTo(x.key);
if(cmplo < 0) keys(x.left, queue, lo, hi);
if(cmplo <= 0 && cmphi >= 0) queue.add(x.key);
if(cmphi > 0) keys(x.right, queue, lo, hi);
}
4、性能分析
5、比较顺序查询、二分查询、二叉树查找
一般来说,递归的实现更容易验证其正确性,而非递归的实现效率更高。如果树不是平衡的,函数调用的栈的深度可能回成为递归实现的一个问题。
附:更多递归和非递归实现可参考二叉查找树