二叉排序树(Binary Sort Tree)又称二叉查找树(Binary Search Tree),亦称二叉搜索树。本文主要介绍二叉查找树的基本操作。
1. 二叉查找树的性质
1.1 定义
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
1.2 二叉查找树的性质
- 在由N个随机键构造的二叉查找树中,查找命中平均所需的比较次数为~2lnN(约为1.39lgN);
- 在由N个随机键构造的二叉查找树中插入操作和查找未命中平均所需的比较次数为~2lnN(约为1.39lgN);
- 在一棵二叉查找树中,所有操作在最坏情况下所需的时间都和树的高度成正比。
1.3 二叉查找树的优点
二叉排序树是一种比较有用的折衷方案。
数组的搜索比较方便,可以直接用下标,但删除或者插入某些元素就比较麻烦。
链表与之相反,删除和插入元素很快,但查找很慢。
二叉排序树就既有链表的好处,也有数组的好处。
在处理大批量的动态的数据是比较有用。
2. 二叉查找树的实现
二叉查找树的节点类:
private class Node{
private Key key;//键
private Value value;// 值
private Node left,right;//指向子树的链接
private Node parent; //指向父节点
private int N;//以该节点为根的子树的节点总数
public Node(Key key, Value value, int n) {
this.key = key;
this.value = value;
this.N = n;
}
}
获取指定节点为根节点的二叉查找树的节点总数:
/**
* 获取该查找二叉树的节点总数
* @return 返回节点总数
*/
public int size(){
return size(root);
}
/**
* 获取以node为根节点的子树的节点总数
* @param node 子树的根节点
* @return 返回节点总数
*/
private int size(Node node) {
if(node == null){
return 0;
}
else return node.N;
}
2.1 查找操作
2.1.1 查找的递归操作
/**
* 递归查找
* 在二叉树中查找键key所对应的值
* @param key 键
* @return 返回键所对应的值
*/
public Value get(Key key){
return get(root,key);
}
/**
* 在以node为根节点的子树中获取键为key的值
* @param node 要查找的子树的根节点
* @param key 要查找的键
* @return 返回键所对应的值
*/
private Value get(Node node, Key key) {
//如果node为空即所查找的子树不存在,则返回null
if(node == null){
return null;
}
//将要查找的键和子树的根节点比较
int cmp = key.compareTo(node.key);
//如果要查找的键小于子树的根节点的键,则递归查找左子树
if(cmp < 0){
return get(node.left,key);
}
//如果要查找的键大于子树的根节点的键,则递归查找右子树
else if(cmp > 0){
return get(node.right,key);
}else
return node.value;//键和根节点的键相同,返回根节点的值
}
2.1.2 查找的非递归操作
/**
* 非递归法查找
* @param key
* @return
*/
public Node search(Key key){
if(root == null){
return null;
}
//从根节点开始搜索
Node current = root;
while(current != null){
int cmp = key.compareTo(current.key);
if(cmp < 0){
//向左子树搜索
current = current.left;
}
else if(cmp > 0){
//向右子树搜索
current = current.right;
}else{
return current;
}
}
return null;
}
2.2 插入操作
2.2.1 插入的递归操作
/**
* 递归法添加新节点
* 查找key,找到则更新它,否则为他创建一个新节点
* @param key 要添加的节点的键
* @param val 要添加节点的值
*/
public void put(Key key,Value val){
root = put(root,key,val);
}
/**
* 在以node为跟的子树中添加节点,如果key存在于以node为根的子树中,则更新该节点,
* 否则在该子树 中添加节点
* @param node
* @param key
* @param val
* @return 返回添加的节点的父节点
*/
private Node put(Node node, Key key, Value val) {
if(node == null){
return new Node(key,val,1);
}
int cmp = key.compareTo(node.key);
if(cmp < 0){
node.left = put(node.left, key, val);
}
else if(cmp > 0){
node.right = put(node.right, key, val);
}
else node.value = val;
node.N = size(node.left) + size(node.right) + 1;
return node;
}
2.2.2 插入的非递归操作
/**
* 非递归法在二叉排序树中添加新节点
* @param key
* @param val
*/
public void add(Key key, Value val){
if(root == null){
root = new Node(key, val, 1);
}else{
Node current = root;
Node parent = null;
int cmp;
//搜索合适的叶子节点添加新节点
do{
parent = current;
cmp = key.compareTo(current.key);
if(cmp > 0){
//以右子节点为当前节点
current = current.right;
}
else if(cmp < 0){
current = current.left;
}
else
break;
}while(current != null);
//创建新节点
Node newNode = new Node(key, val, 1);
if(cmp > 0){
//新节点为父节点的右子节点
parent.right = newNode;
}
else if(cmp < 0){
//新节点为父节点的左孩子
parent.left = newNode;
}else//跟新父节点的值
current.value = val;
current.N = size(current.left) + size(current.right) + 1;
}
}
2.3 获取最大最小值操作
/**
* 获取二叉树中的最小键
* @return 返回最小键
*/
public Key min(){
return min(root).key;
}
/**
* 获取以node为根节点的自述中的最小键
* @param node 要查找的子树
* @return 返回最小键所对应的节点
*/
private Node min(Node node) {
if(node.left == null){
return node;
}else
return min(node.left);
}
/**
* 获取二叉树中的最大键
* @return 返回最大键
*/
public Key max(){
return max(root).key;
}
/**
* 获取以node为根节点的自述中的最大键
* @param node 要查找的子树
* @return 返回最大键所对应的节点
*/
private Node max(Node node) {
if(node.right == null){
return node;
}else
return min(node.right);
}
2.4 删除操作
2.4.1 递归法删除
/**
* 删除最小节点
*/
public void deletMin(){
root = deleteMin(root);
}
/**
* 删除node为根的子树中的最小节点
* @param node
* @return 返回删除节点的父节点的左孩子节点
*/
private Node deleteMin(Node node) {
if(node.left == null){
return node.right;
}
node.left = deleteMin(node.left);
node.N = size(node.left) + size(node.right) + 1;
return node;
}
/**
* 递归法删除键key所对应的节点
* @param key
*/
public void delete(Key key){
root = delete(root,key);
}
/**
* 递归法删除节点
* @param node
* @param key
* @return
*/
private Node delete(Node node, Key key) {
if(node == null){
return null;
}
int cmp = key.compareTo(node.key);
if(cmp < 0){
node.left = delete(node.left, key);
}
else if(cmp > 0){
node.right = delete(node.right, key);
}
else{
if(node.right == null){
return node.left;
}
if(node.left == null){
return node.right;
}
Node t = node;
//获取要删除节点所在子树的最小值
node = min(t.right);
node.right =t.right;
node.left = t.left;
}
node.N = size(node.left) + size(node.right) + 1;
return node;
}
2.4.2 非递归法删除
对于二叉查找树的删除操作分三种情况:
1、如果结点为叶子结点(没有左、右子树),此时删除该结点不会改变树的结构,直接删除即可,并修改其父结点指向它的引用为null.
2、如果被删除的节点只有左孩子或者只有右孩子,那么删除该节点后,树的整体结构并不会变化,只需要将被删除节点的左孩子或者右孩子连接到被删除节点的父节点即可。
3、被删除的节点既有左孩子又有右孩子,那么通常有两种方法来保持树的有序性:
- 以被删除节点为根节点,在它的右子树中查找最小节点,然后将此最小节点从原来的子树中删除,将被删除节点用此最小节点置换;
- 以被删除节点为根节点,在它的左子树中查找最大节点,然后将此最大节点从原来的子树中删除,将被删除节点用此最大节点置换。
/**
* 非递归法删除指定键所对应的节点
* @param key
*/
public void remove(Key key){
//获取要删除的节点
Node node = (Node) search(key);
//如果要删除的节点不存在,直接返回
if(node == null){
return;
}
//如果要删除节点的左右子树都为空
if(node.left == null && node.right == null){
//如果要删除的节点是根节点
if(node == root){
root = null;
}else{
//删除的节点是父节点的左孩子
if(node.parent.left == node){
//将父节点的左孩子置为空
node.parent.left = null;
}else{
//删除的节点是父节点的右孩子
node.parent.right = null;
}
node.parent = null;
}
}
//如果要删除的节点的左子树为空,右子树不为空
if(node.left == null && node.right != null){
//被删除节点是根节点
if(node == root){
root = node.right;
}else{
//删除的节点是父节点的左孩子
if(node.parent.left == node){
//将父节点的左孩子置为被删除节点的右孩子
node.parent.left = node.right;
}else{
//删除的节点是父节点的右孩子,则父节点的右孩子指向被删除节点的右孩子
node.parent.right = node.right;
}
//让被删除节点的右孩子的父节点指向被删除节点的父节点
node.right.parent = node.parent;
}
}
//如果要删除的节点的右子树为空,左子树不为空
if(node.left != null && node.right == null){
//要删除的是根节点
if(node == root){
root = node.left;
}else{
//要删除的节点是父节点的左孩子
if(node.parent.left == node){
//将父节点的左孩子置为被删除节点的zuo孩子
node.parent.left = node.left;
}else{
//要删除的节点是父节点的右孩子
node.parent.right = node.left;
}
//让被删除节点的左孩子的父节点指向被删除节点的父节点
node.left.parent = node.parent;
}
}
//被删粗节点的左右子树都不为空
else{
//leftMax保存左子树中最大键的节点
Node leftMax = node.left;
//搜索左子树的最大键
while(leftMax.right != null){
leftMax = leftMax.right;
}
//从原来的子树中删除leftMax
leftMax.parent.right = null;
//让leftMax的父节点指向node的父节点
leftMax.parent = node.parent;
//被删除的节点是父节点的左子节点
if(node.parent.left == null){
//让父节点的左孩子指向leftMax
node.parent.left = leftMax;
}else{
//被删除的节点是父节点的右子节点,让父节点的右孩子指向leftMax
node.parent.right = leftMax;
}
leftMax.left = node.left;
leftMax.right = node.right;
node.left = node.right = node.parent = null;
}
}
2.5 广度优先遍历
二叉树的遍历参考线索二叉树(上)。
广度优先遍历的代码如下:
public List<Node> breadthFirst(){
Queue<Node> queue = new ArrayDeque();
List<Node> list = new ArrayList();
if(root != null){
//将根节点加入队列
queue.offer(root);
}
while(!queue.isEmpty()){
//访问根节点
list.add(queue.peek());
//队列的头元素出队
Node p = queue.poll();
//如果左子树不为空,访问之
if(p.left != null){
queue.offer(p.left);
}
//如果右子树不为空,访问之
if(p.right != null){
queue.offer(p.right);
}
}
return list;
}
参考文献:
- 《java 算法》