1.基本概念
一颗二叉查找树是一颗二叉树,其中每个节点都含有一个Comparable的键以及和键相关联的值,且每个节点的键都大于其左子树中任意节点的键而小于右子树的任意节点的键。使用链表构成的符号表在插入操作上具有灵活性,而数组构成的符号表在搜索查找上具有更高的效率,二叉查找树可以将二者的优势结合。一颗二叉查找树代表一组键值的集合,同一个集合可以用多棵不同的二叉查找树表示。二叉查找树能够保持键的有序性,所以可以用它来实现有序符号表。
如图为一颗二叉查找树,各个节点的数字为这个节点的键(Key),由图可知10为根节点(root),6是其左子树的根节点,14为其右子树的根节点。
2.实现
数据结构
定义一个私有类表示二叉树中的节点,每个节点含有一个键(Key),一个值(Value),一条左链接,一条右链接和一个节点计数器。左链接指向一棵由小于该节点组成的二叉查找树,右链接指向一颗由大于该节点组成的二叉查找树,数据结构如下。
private class Node { Key key; /* 键 */ Value val; /* 值 */ Node left; /* 该节点的左子树链接 */ Node right; /* 该节点的右子树链接 */ int N; /* 以该节点为根的子树中的节点总数 */ public Node(Key key, Value val, int N) { this.key = key; this.val = val; this.N = N; } }
查找
在符号表中查找一个键会得到两种结果,如果键值位于符号表中则返回相应的值,如果键值不在符号表中则返回null。对于二叉查找树可以很容易的使用递归进行查找,如果为空树则直接返回null,如果被查找的Key和根节点相同,则命中返回根节点对应的Value,如果被查找的Key大于根节点的Key则递归在其右子树上查找,如果被查找的Key小于根节点的Key则递归在其左子树上查找。可以使用get方法实现递归查找,get方法的参数分别为树的根节点以及被查找的Key。如图,需要在以下二叉查找树中搜索键为N的节点。
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; } }
插入
插入操作使用put方法实现,它包含三个参数,分别为根节点,要插入的key以及要插入的val。当根节点为null时直接返回以key和val新建的节点,否则如果当被插入的key小于根节点的key时,递归在左子树中执行插入操作,如果被插入的key大于根节点的key时,递归在右子树中执行插入操作,如果被插入的key等于根节点的key时更新此根节点的val。
private Node put(Node x, Key key, Value val) { /* 如果key存在于以x为根节点的树中,则找到此节点更新它的值 * 否则将key和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++; return x; }
最大键和最小键
如果一个二叉查找树根节点的左链接为空,则根节点即为最小键的节点,如果左链接非空,则树的最小键就是左子树的最小键;同理如果根节点的右链接为空,则根节点即为最大键节点,如果右链接不为空,则树的最大键就是右子树的最大键。
private Node min(Node x) { /* 如果x为null则根节点即为最小键,否则左子树的最小键即为 * 整棵树的最小键*/ if (x.left == null) return x; return min(x.left); } private Node max(Node x) { if (x.right == null) return x; return max(x.right); }
删除最大键和最小键
二叉查找树中的删除操作是最难实现的,因为当我们删除一个节点时,对应的可能就会调整整棵树的结构。先考虑删除最小键和最大键两种特殊的情况,如果根节点的左子树为空,则直接删除根节点即为删除最小键,如果根节点的左子树不为空,则要深入根节点的左子树,直到遇到一个空链接,然后将直线该节点的链接指向该节点的右子树。
private Node deleteMin(Node x) { if (x.left == null) { return x.right; } /* 当x.left不为null时,递归删除x左子树 */ x.left = deleteMin(x.left); x.N--; return x; } private Node deleteMax(Node x) { if (x.right == null) { return x.left; } x.right = deleteMin(x.right); x.N--; return x; }
删除
我们可以使用删除最大键和最小键的方法删除二叉查找树的叶子节点,但是当删除一个含有左右两个子树的节点时,会产生两棵子树,此时如何将两棵子树合并呢?一种简单的办法是当删除节点x之后,使用x的后继节点即为右子树的最小节点替换,具体步骤如下:
- 将要被删除的节点的链接保存为t;
- 将x指向它的后继节点min(t.right);
- 将x的右链接指向deleteMin(t.right);
- 将x的左链接设为t.left。
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.left == null) { return x.right; } if (x.right == null) { return x.left; } Node t = x; x = min(t.right); x.left = t.left; x.right = deleteMin(x.right); } x.N--; return x; }
3.性能分析
使用二叉查找树的算法运行时间取决于树的形状,而树的形状取决于键被插入的顺序。在最好情况下一棵含有N个节点的树是完全平衡的,每条空链接和根节点的距离都为lg(N),在最坏情况下搜索路径上可能有N个节点。二叉查找树满足如下命题:
- 在由N个随机键构造的二叉查找树中,查找命中平均所需的比较次数为2ln(N)约等于1.39lg(N)。
- 在由N个随机数构造的二叉查找树中,插入操作和查找未命中平均所需的比较次数为2ln(N)约等于1.39lg(N)。
- 在一棵二叉查找树中,所有操作在最坏情况下所需时间和树的高度成正比。
4.使用实例
import java.util.Random; /** * Created by Administrator on 2014/11/28. */ public class BST<Key extends Comparable<Key>, Value> { private Node root; private class Node { Key key; /* 键 */ Value val; /* 值 */ Node left; /* 该节点的左子树链接 */ Node right; /* 该节点的右子树链接 */ int N; /* 以该节点为根的子树中的节点总数 */ public Node(Key key, Value val, int N) { this.key = key; this.val = val; this.N = N; } } /* 在二叉查找树中查找键为key的节点 */ public Value get(Key key) { return get(root, key); } private Value get(Node x, Key key) { /* 如果x为空则直接返回null,否则如果key大于根节点的key * 则递归在右子树中查找,如果key小于根节点的key则递归在 * 左子树中查找,如果key等于根节点的key则返回此根节点对 * 应的val */ 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; } } /* 返回二叉查找树的节点个数 */ public int size() { return size(root); } private int size(Node x) { if (x == null) return 0; else return root.N; } /* 向二叉查找树中插入元素 */ public void put(Key key, Value val) { /* 查找key,找到则更新它的值,否则为它创建一个新的节点 */ root = put(root, key, val); //System.out.println(root.key + " " + root.val + " " + root.left + " " + root.right); } private Node put(Node x, Key key, Value val) { /* 如果key存在于以x为根节点的树中,则找到此节点更新它的值 * 否则将key和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++; return x; } /* 返回二叉查找树中的最小键 */ public Key min() { return min(root).key; } private Node min(Node x) { /* 如果x为null则根节点即为最小键,否则左子树的最小键即为 * 整棵树的最小键*/ if (x.left == null) return x; return min(x.left); } /* 返回二叉查找树中的最大键 */ public Key max() { return max(root).key; } private Node max(Node x) { if (x.right == null) return x; return max(x.right); } /* 删除二叉查找树中的最小键 */ public void deleteMin() { root = deleteMin(root); } private Node deleteMin(Node x) { if (x.left == null) { return x.right; } /* 当x.left不为null时,递归删除x左子树 */ x.left = deleteMin(x.left); x.N--; return x; } /* 删除二叉查找树中的最大键 */ public void deleteMax() { root = deleteMax(root); } private Node deleteMax(Node x) { if (x.right == null) { return x.left; } x.right = deleteMin(x.right); x.N--; return x; } /* 删除二叉树查找树中键为key的节点 */ 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.left == null) { return x.right; } if (x.right == null) { return x.left; } Node t = x; x = min(t.right); x.left = t.left; x.right = deleteMin(x.right); } x.N--; return x; } public static void main(String[] args) { BST<String, Integer> bst = new BST<String, Integer>(); bst.put("BB", 2); bst.put("A", 1); bst.put("CCC", 3); System.out.println(bst.get("A")); System.out.println(bst.get("BB")); System.out.println(bst.get("CCC")); bst.deleteMin(); bst.deleteMax(); bst.delete("BB"); System.out.println(bst.get("A")); System.out.println(bst.get("BB")); System.out.println(bst.get("CCC")); } }