二叉排序树
先来说说这个二叉排序树的定义和性质:
定义:二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的键值均小于或等于它的根结点的键值;
(2)若右子树不空,则右子树上所有结点的键值均大于或等于它的根结点的键值;
(3)左、右子树也分别为二叉排序树;
下面的代码将用java实现,并且全部基于递归实现(非递归算法复杂一些且效率高)。主要讨论BST的如下操作:
查找、插入、最大键、最小键、向上取整、向下取整、排名k的键、获取键key的排名、删除最大最小键、删除操作、范围查找。
1.结点的数据结构的定义
下面是BST(后文都用BST表示二叉排序树)中结点的数据结构的定义。
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
此外,对于整个二叉查找树来说,有一个根节点,所以在BST类中定义了一个根结点:
private Node root;//二叉查找树的根节点
- 1
2. 计算二叉排序树的size
思想: 根据我们数据结构Node中的定义,里面有一个属性 N 表示的就是以当前节点为根的子树的结点总数。所以源码如下:
/**
* 获取整个二叉查找树的大小
* @return
*/
public int size(){
return size(root);
}
/**
* 获取某一个结点为根结点的二叉查找树的大小
* @param x
* @return
*/
private int size(Node x){
if(x == null){
return 0;
} else {
return x.N;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
3. 查找和插入
在实现BST类的时候,BST继承自Comparable接口的,实现compareTo()函数。因为我们知道二叉查找树的键值是有有序的,左子树小于根节点,右子树大于根节点。所以实现Comparable接口,那么我们就很容易根据key找到插入的位置,而且对于BTS来说,插入的位置都是在叶子节点处。对于插入和查找都是基于键值的比较。下面是源码:
/**
* 查找:通过key获取value
* @param key
* @return
*/
public Value get(Key key){
return get(root, key);
}
/**
* 在以 x 为根节点的子树中查找并返回Key所对应的值,如果找不到就返回null
* 递归实现
* @param x
* @param key
* @return
*/
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.value;
}
}
/**
* 插入:设置键值对
* @param key
* @param value
*/
public void put(Key key, Value value){
root = put(root, key, value);
}
/**
* key如果存在以 x 为根节点的子树中,则更新它的值;
* 否则将key与value键值对插入并创建一个新的结点.
* @param x
* @param key
* @param value
* @return
*/
private Node put(Node x, Key key, Value value){
if( x==null ){
x = new Node(key, value, 1);
return x;
}
int cmp = key.compareTo(x.key);
if(cmp<0){
x.left = put(x.left, key, value);
}else if(cmp>0){
x.right = put(x.right, key, value);
} else{
x.value=value;//更新value的值
}
//设置根节点的N属性
x.N = size(x.left) + size(x.right) + 1;
return x;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
4. 最大键和最小键的实现
求BST中的最大键值和最小键值。根据BSt的特性,其实原理很简单:最小值就是最左下边的一个节点。最大键值就是最右下边的结点。源码如下:
/**
* 最小键
*/
public Key min(){
return min(root).key;
}
/**
* 返回结点x为root的二叉排序树中最小key值的Node
* @param x
* @return 返回树的最小key的结点
*/
private Node min(Node x){
if(x.left == null){
return x;
}else{
return min(x.left);
}
}
/**
* 最大键
*/
public Key max(){
return max(root).key;
}
/**
* 返回结点x为root的二叉排序树中最大key值的Node
* @param x
* @return
*/
private Node max(Node x){
if(x.right == null){//右子树为空,则根节点是最大的
return x;
}else{
return max(x.right);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
5. Key值向下取整和向上取整
所谓的向下取整和向上取整,就是给定键值key,向下取整的意思就是找出小于等于当前key的的Key值。向上取整的意思就是找出大于等于当前key的的Key值。实现也是基于Comparable接口的,具体的源码如下:
/**
* key向下取整
*/
public Key floor(Key key){
Node x = floor(root, key);
if(x == null){
return null;
}
return x.key;
}
/**
* 以x 为根节点的二叉排序树,查找以参数key的向下取整的Node
* @param x
* @param key
* @return
*/
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){//说明key参数小于x结点的key,所以向下取整结点在左子树
return floor(x.left, key);
}
//向下取整在右子树,
Node t = floor(x.right, key);
if( t!= null){
return t;
}else {
return x;
}
}
/**
* key向上取整
*/
public Key ceiling(Key key){
Node x = ceiling(root, key);
if(x == null){
return null;
}
return x.key;
}
/**
* 以x 为根节点的二叉排序树,查找以参数key的向上取整的Node
* @param x
* @param key
* @return
*/
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){//说明key参数大于x结点的key,所以向上取整结点在右子树
return ceiling(x.right, key);
}
//向上取整在左子树,
Node t = ceiling(x.left, key);
if( t!= null){
return t;
}else {
return x;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
6. 获取排名
我们经常可能会遇到需要获取到排名为k的结点或则获取某一个结点的排名,具体的实现也是基于Comparable接口的比较。
/**
* 排名为k的结点的key
*/
public Key select(int k){
Node x = select(root, k);
if(x == null){
return null;
}
return x.key;
}
/**
* 返回排名为k的结点
* @param x 根节点
* @param k 排名
* @return
*/
private Node select(Node x, int k){
if(x == null){
return null;
}
int t = size(x.left);//获取左子树的节点数
if(t == k) {//左子树节点数和k相同
return x.left ;
} else if( t+1 == k ){//左子树结点数比k小一.
return x;
} else if(t>k){//排名k的结点在左子树
return select(x.left, k);
}else{
//排名k的在右子树
return select(x.right, k-t-1);
}
}
/**
* 返回给定键key的排名
*/
public int rank(Key key){
return rank(root, key);
}
/**
* 在二叉排序树x上返回key的排名
* @param x
* @param key
* @return
*/
private int rank(Node x, Key key){
if(x == null){
return 0;
}
int cmp = key.compareTo(x.key);
if(cmp < 0){
//key键小于root的key,所以key在左子树中
return rank(x.left, key);
} else if(cmp>0){
//key大于root的key,所以key在右子树中
return 1+size(x.left)+rank(x.right, key);
} else{
return size(x.left)+1;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
7. 删除最小键值和最大键值的结点。
删除二叉排序树中的最大键值和最小键值的结点。这里的思想和找到最大键值和最小键值结点几乎一样。只是这里需要删除该结点。由于最大键值和最小键值的位置的特殊性,都在叶子结点,所以这里的删除都是比较简单的,不涉及到子树的移动。源码如下:
/**
* 删除键值最小结点
*/
public void deleteMin(){
//删除root二叉查找树中的最小key的结点,其实也就是最左边的结点
root = deleteMin(root);
}
/**
* 删除键值最小结点
* @param x
* @return 返回新的二叉查找树的根节点
*/
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 deleteMax(){
//删除root二叉查找树中的最小key的结点,其实也就是最左边的结点
root = deleteMax(root);
}
/**
* 删除
* @param x
* @return 返回新的二叉查找树的根节点
*/
private Node deleteMax(Node x){
if(x.right == null){//右子树为空
return x.left;//删除根节点,这时返回的是新的二叉查找树的根节点
}
x.right = deleteMax(x.right);
x.N = size(x.left) + size(x.right) + 1;
return x;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
8. 删除任意结点
这里的删除指删除二叉排序树中任意位置的结点x,这个时候的结点x就有如下4种情况:
首先做个约定:待删除的结点是结点x,其父结点是结点p,左子树结点是l,右子树结点是r,兄弟结点为b。其中NIL均为虚拟的null结点。(以上结点均在存在的情况下)
1、当待删除结点x位于叶子结点位置:
这时直接删除该叶子结点x,并将其父结点p指向该结点的域设置为null。
这时只要删除叶子结点x,然后将p的左结点指向NIL 结点:
2、当待删除结点x只有一个子结点时(该结点只有左子树或则右子树:即左子树和右子树中有一个为空),这时将x结点的父结点p指向待删除的结点x的指针直接指向x结点的唯一儿子结点(l或则是r),然后删除x结点就OK了。下图只演示只有左子树的时候,只有右子树时候相同:
这时只用删除x结点,并将p结点左子树指向x的子结点L. 如下图:
3、当待删除结点x有两个子结点的时候,这时候删除待删除结点x是最麻烦的。一个最经典的方案是:用删除结点x的后继结点填补它的位置。因为x有一个右子结点,因此它的后继结点就是右子树中的最小结点,这样替换就能保证有序性,因为x.key和它的后继之间不存在其他的键。我们用下面的4个步骤解决x替换为它后继结点的任务:
1)将指向即将被删除的结点x的链接保存为t;
2)将x指向它的后继结点min(x.right);
3)将x的右链接指向deleteMin(t.right) (这里也就是右子树删除最小结点之后的),也就是删除最小结点后,其右子树所有节点都仍大于x.key的右子二叉查找树。
4)将x的左链接(本为空)设为t.left(其下所有的键都小于被删除的结点和它的后继结点)。
源码如下:
/**
* 删除键key结点.
* @param key
*/
public void delete(Key key){
root = delete(root, key);
}
/**
* 删除以x为根结点的二叉查找树的key键的结点
* @param x
* @param key
* @return 新的二叉查找树的根节点
*/
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 {//这时删除根节点x
if(x.left == null){
return x.right;
}
if(x.right == null){
return x.left;
}
//根节点有左右子树
Node t = x;
//1. 先求出x的右子树中最小键值的结点并让x指向它.
x = min(t.right);
//2. 将t的右子树删除最小的结点之后的根节点返回
x.right = deleteMin(t.right);
//3. 将t的左子树给x的左子树
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}