二叉查找树是二叉树的一种特殊形式,对于每一个节点来说,其属性Key值可以比较,其Key值比左节点大,比右节点小。即二叉查找树是有序的,中序遍历输出即为有序序列。二叉查找树的查找性能平均情况下为O(logN)对数级别,极端情况下为O(N)。
如下图所示,为一颗二叉查找树,对于每个节点来说,左孩子小于其值,右孩子大于其值,整个二叉树的中序遍历输出为所有数据的有序序列。
二叉查找树BST的API:
API | 作用描述 |
size() | 返回输的节点个数 |
get(Key key) | 查找某一个key值 |
put(Key key, Value val) | 放入某一个节点 |
max() | 返回最大元素节点 |
min() | 返回最小元素节点 |
floor(Key key) | 返回小于等于key的最大节点 |
ceiling(Key key) | 返回大于等于key的最小节点 |
select(int k) | 查找排名为k的节点 |
rank(Key key) | 返回该key对应的排名 |
delete(Key key) | 删除某个key值 |
deleteMin() | 删除最小值 |
deleteMax() | 删除最大值 |
keys(Key lo, Key hi) | 返回给定范围内key值集合 |
printBinaryTree() | 图形化打印二叉树 |
height() | 返回树的高度 |
二叉查找树的组成
首先构造一个内部类Node来表示二叉树的节点,每个节点对应key-value,以及左右子节点链接,N表示已该节点为根的子树中含有的节点总数。
内部持有一个Node root用来表示二叉树的根节点。
//二叉查找树
public class BST<Key extends Comparable<Key>, Value> {
private class Node{
private Key key;
private Value value;
private Node left, right;
private int N;
public Node(Key key, Value value, int N){
this.key = key;
this.value = value;
this.N = N;
}
// @Override
// public String toString() {
// return "Node [key=" + key + ", value=" + value + ", left=" + left + ", right=" + right + ", N=" + N + "]";
// }
@Override
public String toString() {
return "Node [key=" + key + "]";
}
}
private Node root;
}
下面进行二叉查找树的具体实现,使用递归方式,也可以使用非递归方式来实现,一般情况下,非递归方式效率要高。
Size()方法
size(x)方法返回以x为根的子树含有的节点总数,使用递归,依次将左子树,右子树以及自身相加返回即可。
public int size(){
return size(root);
}
private int size(Node x){
if(x==null) return 0;
return size(x.left) + size(x.right) + 1;
}
get(Key key)方法
对于二叉查找树的查找方法,可以从根节点进行查找
如果相等则直接返回;
如果比根节点小,则待查找节点必定在左子树,则递归到左子树进行查找;
如果比根节点大,则进入到右子树进行查找。
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.right, key);
else if(cmp<0)
return get(x.left, key);
else
return x.value;
}
put(Key key, Value val)方法
对于插入操作,首先找到待插入节点在二叉树中的位置,从根节点x开始出发比较
如果相等,则说明该节点已经存在,则执行更新操作;
如果小于x,则说明待插入节点合理位置在x的左子树,将x.left指向新建的节点
如果大于x,则说明待插入节点合理位置在x的右子树,将x.right指向新建的节点
public void put(Key key, Value val){
root = put(root, 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.right = put(x.right, key, val);
else if(cmp<0)
x.left = put(x.left, key, val);
else
x.value = val;
x.N = size(x.left) + size(x.right) + 1;
return x;
}
max()与min()
最大值从根节点开始,
如果右子树为空,则说明根节点为最大值;
否则一直往右子树寻找,直到找到某个节点右子树为空,返回该节点即可。
最小值同理,从左子树中不断寻找。
public Node max(){
return max(root);
}
private Node max(Node x){
if(x == null)
return null;
if(x.right==null)
return x;
return max(x.right);
}
public Node min(){
return min(root);
}
private Node min(Node x){
if(x == null)
return null;
if(x.left == null)
return x;
return min(x.left);
}
floor(Key key)与ceiling(Key key)
floor返回小于等于key的最大值节点,从根节点开始寻找,
如果key值等于根节点key,则直接返回根节点即可;
如果key值小于根节点key,则说明该值必在其左子树;
如果key值大于根节点key,则说明该值可能在右子树,如果右子树中最小值比key值大,则说明右子树中不存在该节点,即根节点返回,否则说明右子树中有节点比key值要小,返回最大的值即可。
ceiling方法同理
//小于等于key的最大值
public Node floor(Key key){
if(root == null)
return null;
return floor(root, 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 r = floor(x.right, key);
if(r!=null) return r;
else return x;
}
//大于等于key的最小值
public Node ceiling(Key key){
if(root==null) return null;
return ceiling(root, 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) return ceiling(x.right, key);
Node l = ceiling(x.left, key);
if(l!=null) return l;
else return x;
}
select(int k)与rank(Key key)
查找排名为k的节点,从根节点开始寻找,利用根节点的左子树的节点总数,
如果根节点的左子树节点总数==k-1,则说明根节点为排名为k的节点,返回根节点即可;
如果左子树总数大于k-1,则说明排名k节点在根节点的左子树中,从左子树中递归查找排名为k的节点
如果大于k-1,则说明排名为k的节点在跟节点的右子树中,在右子树中的排名为k-1-左子树总数;
//查找排名为k的节点
public Node select(int k){
if(root==null) return null;
return select(root, k);
}
private Node select(Node x, int k){
if(x==null) return null;
int leftnum = size(x.left);
if(leftnum==k-1) return x;
if(leftnum>k-1) return select(x.left, k);
return select(x.right, k-1-leftnum);
}
//返回该key对应的排名
public int rank(Key key){
if(root==null) return -1;
return rank(root, key);
}
private int rank(Node x, Key key){
if(x==null) return -1;
int leftnum = size(x.left);
int cmp = key.compareTo(x.key);
if(cmp==0) return leftnum+1;
if(cmp<0) return rank(x.left, key);
return leftnum + 1 + rank(x.right, key);
}
deleteMin()与deleteMax()
删除最小值,一直往左子树寻找,直到某个节点的左子树为空,将指向该节点的左连接指向该节点的右子树。
删除最大值,一直往右子树寻找,直到某个节点右子树为空,将指向该节点的有链接指向该节点的左子树。
public void deleteMin(){
root = deleteMin(root);
}
//一直往左子树寻找,直到某个节点的左子树为空,将该节点的右子树指向指向该节点的左连接
private Node deleteMin(Node x){
if(x==null) return null;
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 deleteMax(){
root = deleteMax(root);
}
private Node deleteMax(Node x){
if(x==null) return null;
if(x.right==null) return x.left;
x.right = deleteMax(x.right);
x.N = size(x.left) + size(x.right) + 1;
return x;
}
delete()方法
在deleteMin以及deleteMax中是删除的特殊情况,即某个待删除节点的左节点或者右节点为空,如果待删除节点的左右均非空,
可以采用以下步骤来删除该节点,使用该节点的右子树的最小节点来代替该节点。
新建节点t指向待删除节点x
将x指向t右子树最小节点
将左节点指向t左节点
将x右节点指向t右子树删除最小节点后返回的根节点
public void delete(Key key){
if(root==null) return;
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);
if(cmp>0) x.right = delete(x.right, key);
if(x.left==null) return x.right;
if(x.right==null) return x.left;
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;
}
keys(Key lo, Key hi)方法
返回给定范围内的key值集合,利用中序遍历以及队列结构,将符合条件的key保存在队列中返回。
中序遍历,从根节点开始比较其与范围左右端点值大小,
如果根节点小于左端点,则说明符合条件值可能在右子树
如果根节点在左右节点范围内部,则将根节点加入到队列中
如果根节点大于右节点,则应该递归遍历其左子树
public Iterable<Key> keys(){
return keys(min().key, max().key);
}
//返回给定范围内的key值集合
public Iterable<Key> keys(Key lo, Key hi){
Queue<Key> q = new LinkedList<>();
keys(root, q, lo, hi);
return q;
}
private void keys(Node x, Queue<Key> q, 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.right, q, lo, hi);
if(cmplo<=0&&cmphi>=0) q.add(x.key);
if(cmphi>0) keys(x.left, q, lo, hi);
}
printBinaryTree()图形化打印二叉树
为了便于形象化展示二叉树,采用控制台来图形化输出二叉树,日后研究一下使用SWT来图形化输出二叉树,使用控制台过于繁琐。
给定一颗二叉树,首选使用层序遍历,将二叉树补全为完全二叉树,树中的空节点使用特殊符号specialChar来代替,层序遍历的结果是将二叉树输出到一个List<List<Node>>中,然后利用坐标变换来计算树中每个节点的位置以及输出绘制。
二叉树转换为完全二叉树的层序遍历
利用队列,求出二叉树的深度,层序遍历二叉树,首先将根节点入队,然后开始循环遍历,每一次循环,代表处理二叉树的每一层,首先计算队列中的节点个数,即该层depth节点个数,将队列中元素依次出队,首先将元素加入到List中,然后将元素的左右节点依次入队,如果子节点为空,则新建一个特殊字符节点来代替,依次遍历每一层,得到最终的层序遍历结果。
public List<List<Node>> levelOrder2(){
if(root==null) return null;
List<List<Node>> ll = new ArrayList<>();
Queue<Node> q = new LinkedList<>();
q.add(root);
int h = height();
int depth = 1;
while(q.size()>0){
int s = q.size();
List<Node> lk = new ArrayList<>();
while(s>0){
Node tmp = q.poll();
lk.add(tmp);
if(tmp.left!=null){
q.add(tmp.left);
}else{
q.add(new Node((Key)specialChar, (Value)specialChar, 1));
}
if(tmp.right!=null){
q.add(tmp.right);
}else{
q.add(new Node((Key)specialChar, (Value)specialChar, 1));
}
s--;
}
ll.add(lk);
if(depth>h-1)
break;
depth++;
}
for(List<Node> t : ll){
for(Node n : t){
System.out.print(n.key);
}
System.out.println();
}
return ll;
}
给定的二叉树以及层序遍历的结果为,即将二叉树的每一行存储到List<List<>>中,List每一个元素代表一行,特殊字符使用#来代替
由于是完全二叉树,深度为h行,节点个数为pow(2,h-1)。
最终绘制的效果如下所示:用横线代替箭头
下面分析计算二叉树的每个节点的坐标关系
将二叉树节点以及左右子节点箭头看成一个基本单位,将二叉树往最底层投影,可以得到二叉树节点以及箭头的编号,对于深度为4的完全二叉树来说,最后一层总长度为29个单位,即最后一行总长度为pow(2,h+1)-3
考虑第i行,有:
第i行距离最左边的起始偏移量为pow(2,h-i+1)-2
第i行节点之间的间隔为pow(2,h-i+2)-1
第i行元素距离其左节点或右节点的距离为pow(2,h-i),该距离用于行与行之间的横线绘制
绘制程序如下所示:
遍历层序结果的每一行
对于每一行,首先计算行起始偏移量,以及节点间隔,打印该行的每一个节点
对于该行,绘出下一行横线部分
横线部分起始偏移地址为pow(2,h-i)-2
该节点对应的左右节点总距离为2*pow(2,h-i)+1
横线间隔即为下一行的节点之间的间隔pow(2,h-i+2-1)-1
最终程序如下:
public void printBinaryTree2(){
List<List<Node>> ll = levelOrder2();
List<Node> l = null;
int h = height();
for(int i=1; i<=h; i++){
//节点行 开始地址
int lineStart = pow2(h-i+1)-2;
//每个节点的偏移地址
int offset = pow2(h-i+2)-1;
//该节点到左子节点或右子节点的长度
int brackets = pow2(h-i);
//System.out.println("linenum = " + i + " start = " + lineStart + " offset = " + offset + " brackets = " + brackets);
printFormat(lineStart, spaceFormat);
l = ll.get(i-1);
for(int j=0; j<l.size(); j++){
if(!l.get(j).key.equals(specialChar)){
System.out.print(l.get(j).key);
}else{
System.out.print(" ");
}
printFormat(offset, spaceFormat);
}
//最后一行不需要绘出
if(i>=h)
break;
System.out.println();
//横线部分行开始地址
int lineCrossStart = pow2(h-i)-2;
printFormat(lineCrossStart, spaceFormat);
for(int j=0; j<l.size(); j++){
//System.out.println(l.get(j).key);
if(!l.get(j).key.equals(specialChar) && (l.get(j).left!=null || l.get(j).right!=null)){
//每个元素下一行横线部分长度
printFormat(brackets*2+1, crossFormat);
//横线部分 间隔 即下一行的节点的间隔
printFormat(pow2(h-i+2-1)-1, spaceFormat);
}else{
printFormat(brackets*2+1, spaceFormat);
printFormat(pow2(h-i+2-1)-1, spaceFormat);
}
}
System.out.println();
}
System.out.println();
}
public int pow2(int n){
return (int) Math.pow(2, n);
}
public void printFormat(int num, String format){
StringBuilder sb = new StringBuilder();
for(int i=0; i<num; i++){
sb.append(format);
}
System.out.print(sb.toString());
}
public int height(){
return height(root);
}
public int height(Node x){
if(x==null) return 0;
if(x.left==null&&x.right==null) return 1;
return 1+Math.max(height(x.left), height(x.right));
}
演示部分
二叉查找树与快速排序思想类似,二叉查找树的根节点对应快速排序基准值,二叉查找树的最终结果与元素插入顺序有关,如果插入元素顺序为有序的,则最终二叉树会蜕变成线性表,例如依次插入1-5有
此时执行查找操作,则最终时间复杂度变为O(N)
顺序插入如下数据,删除8节点,效果如下
BST<Integer, String> bst = new BST<>();
int[] arr = new int[]{8,6,10,5,7,9,12,2,4};
//int[] arr = new int[]{1,2,3,4,5};
//int[] arr = new int[]{8,6,10,5,7,9,12};
for(int i=0; i<arr.length; i++){
bst.put(arr[i], String.valueOf(i));
}
bst.printBinaryTree2();
bst.delete(8);
bst.printBinaryTree2();