树的应用
二叉排序树
给你一个数列 7, 3, 10, 12, 5, 1, 9
,要求能够高效的完成对数据的查询和添加。
在 为什么需要该数据结构 中讲解了数组、链表数据结构的优缺点,简单说:
-
数组访问快,增删慢
新增或移除时,需要整体移动数据
-
链表增删快,访问慢
只能从头开始遍历查找
那么利用 二叉排序树(Binary Sort/Search Tree),既可以保证数据的检索速度,同时也可以保证数据的插入、删除、修改 的速度
二叉排序树介绍
二叉排序树(Binary Sort/Search Tree),简称 BST。
对于二叉排序树的任何一个 非叶子节点,要求如下:
- 左节点,比父节点小
- 右节点,比父节点大
特殊说明:如果有有相同的值,可以将该节点放在左节点或右节点。当然,最理想的是没有重复的值,比如 Mysql 中的 B 树索引,就是以主键 ID 来排序的。
比如对下面这个二叉树增加一个节点:
- 从根节点开始,发现比 7 小,直接往左子树查找,相当于直接折半了
- 比 3 小,再次折半
- 比 1 大:直接挂在 1 的右节点
创建、遍历、删除、查找
|
//定义节点 |
|
class Node{ |
|
int value; |
|
Node left; |
|
Node right; |
|
|
|
public Node(int value) { |
|
this.value = value; |
|
} |
|
//添加节点 |
|
public void add(Node node){ |
|
if(node == null){ |
|
return; |
|
} |
|
if(node.value < this.value){ |
|
if(this.left == null){ |
|
this.left = node; |
|
}else{ |
|
this.left.add(node);//递归查找添加 |
|
} |
|
}else{ |
|
if(this.right == null){ |
|
this.right = node; |
|
}else{ |
|
this.right.add(node); |
|
} |
|
} |
|
} |
|
//查找要删除的父节点 |
|
public Node searchParent(int value){ |
|
if(this.left != null && this.left.value == value || this.right != null && this.right.value == value){ |
|
return this; |
|
}else if(this.left != null && this.value > value){//左递归查找 |
|
return this.left.searchParent(value); |
|
}else if(this.right != null && this.value < value){//右递归查找 |
|
return this.right.searchParent(value); |
|
}else{ |
|
return null; |
|
} |
|
} |
|
|
|
//查找要删除的节点 |
|
public Node search(int value){ |
|
if(this.value == value){ |
|
return this; |
|
}else if(value < this.value){ |
|
if(this.left != null){ |
|
return this.left.search(value);//向左递归查找 |
|
}else{ |
|
return null; |
|
} |
|
}else { |
|
if(this.right != null){ |
|
return this.right.search(value); |
|
}else{ |
|
return null; |
|
} |
|
} |
|
} |
|
|
|
//中序遍历 |
|
public void infixOrder(){ |
|
if(this.left != null){ |
|
this.left.infixOrder(); |
|
} |
|
System.out.println(this); |
|
if(this.right != null){ |
|
this.right.infixOrder(); |
|
} |
|
} |
|
|
|
|
|
public String toString() { |
|
return "Node{" + |
|
"value=" + value + |
|
'}'; |
|
} |
|
} |
|
class BinarySortTree{ |
|
private Node root; |
|
|
|
public Node getRoot() { |
|
return root; |
|
} |
|
|
|
public void add(Node node){ |
|
if(root == null){ |
|
root = node; |
|
}else{ |
|
root.add(node); |
|
} |
|
} |
|
|
|
public void infixOrder(){ |
|
if(root != null){ |
|
root.infixOrder(); |
|
}else{ |
|
System.out.println("二叉排序树为空"); |
|
} |
|
} |
|
//查找要删除的节点 |
|
public Node search(int value){ |
|
if(root != null){ |
|
return root.search(value); |
|
}else{ |
|
return null; |
|
} |
|
} |
|
//查找要删除的父节点 |
|
public Node searchParent(int value){ |
|
if(root != null){ |
|
return root.searchParent(value); |
|
}{ |
|
return null; |
|
} |
|
} |
|
//找到目标节点为根节点最小的那个节点,并删除其节点,然后返回其值 |
|
public int delRightMin(Node root){ |
|
Node target = root; |
|
while(target.left != null){ |
|
target = target.left; |
|
} |
|
delNode(target.value); |
|
return target.value; |
|
} |
|
|
|
//删除节点 |
|
public void delNode(int value){ |
|
Node targetNode = search(value); |
|
//要删除的节点是根节点 |
|
if(targetNode == root){ |
|
root = null; |
|
return; |
|
} |
|
Node parentNode = searchParent(value); |
|
|
|
if(targetNode.left == null && targetNode.right == null){//删除的叶子节点 |
|
if(parentNode.left == targetNode){//当目标节点为父节点的左子节点 |
|
parentNode.left = null; |
|
}else{ |
|
parentNode.right = null; |
|
} |
|
}else if(targetNode.left != null && targetNode.right != null){//删除的节点为有两个子节点的非叶子节点 |
|
targetNode.value = delRightMin(targetNode.right); |
|
|
|
}else{//删除的节点为只有一个节点的非叶子节点 |
|
if(targetNode.left != null){ |
|
if(parentNode != null){ |
|
if(parentNode.left == targetNode){ |
|
parentNode.left = targetNode.left; |
|
}else{ |
|
parentNode.right = targetNode.left; |
|
} |
|
}else{ |
|
root = targetNode.left; |
|
} |
|
}else { |
|
if(parentNode != null){ |
|
if(parentNode.left == targetNode){ |
|
parentNode.left = targetNode.right; |
|
}else{ |
|
parentNode.right = targetNode.right; |
|
} |
|
}else{ |
|
root = targetNode.right; |
|
} |
|
} |
|
} |
|
|
|
} |
|
} |
完整平衡二叉树(AVL树)
二叉排序树可能的问题
一个数列 {1,2,3,4,5,6}
,创建一颗二叉排序树(BST)
创建完成的树如上图所示,那么它存在的问题有以下几点:
-
左子树全部为空,从形式上看,更像一个单链表
-
插入速度没有影响
-
查询速度明显降低
因为需要依次比较,不能利用二叉排序树的折半优势。而且每次都还要比较左子树,可能比单链表查询速度还慢。
那么解决这个劣势的方案就是:平衡二叉树(AVL)。
基本介绍
平衡二叉树也叫 平衡二叉搜索树(Self-balancing binary search tree),又被称为 AVL 树,可以保证 查询效率较高。它是解决 二叉排序 可能出现的查询问题。
它的特点:是一颗空树或它的 左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一颗平衡二叉树。
平衡二叉树的常用实现方法有:
- 红黑树
- AVL(算法)
- 替罪羊树
- Treap
- 伸展树
如下所述,哪些是平衡二叉树?
-
是平衡二叉树:
- 左子树高度为 2
- 右子树高度为 1
他们差值为 1
-
也是平衡二叉树
-
不是平衡二叉树
- 左子树高度为 3
- 右子树高度为 1
他们差值为 2,所以不是
单旋转(左旋转)
一个数列 4,3,6,5,7,8
,创建出它对应的平衡二叉树。
思路分析:下图红线部分是调整流程。
按照规则调整完成之后,形成了下面这样一棵树
完整流程如下图所示:
插入 8 时,发现左右子树高度相差大于 1,则进行左旋转:
-
创建一个新的节点
newNode
,值等于当前 根节点 的值(以 4 创建) - 把新节点的 左子树 设置为当前节点的 左子树
- 把新节点的 右子树 设置为当前节点的 右子树的左子树
- 把 当前节点 的值换为 右子节点 的值
- 把 当前节点 的右子树设置为 右子树的右子树
- 把 当前节点 的左子树设置为新节点
注:左图是调整期,右图是调整后。注意调整期的 6 那个节点,调整之后,没有节点指向他了。也就是说,遍历的时候它是不可达的。那么将会自动的被垃圾回收掉。
树高度计算
前面说过,平衡二叉树是为了解决二叉排序树中可能出现的查找效率问题,那么基本上的代码都可以在之前的二叉排序树上进行优化。那么下面只给出与当前主题相关的代码,最后放出一份完整的代码。
树的高度计算,我们需要得到 3 个高度:
- 这颗树的整体高度
- 左子树的高度
- 右子树的高度
|
//Node |
|
//获得左子树的高度 |
|
public int leftHeight(){ |
|
if(left == null){ |
|
return 0; |
|
} |
|
return left.height(); |
|
} |
|
//获得右子树的高度 |
|
public int rightHeight(){ |
|
if(right == null){ |
|
return 0; |
|
} |
|
return right.height(); |
|
} |
|
//获得当前节点高度 |
|
public int height(){ |
|
return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height())+1; |
|
} |
旋转
说下旋转的时机:也就是什么时机采取做旋转的操作?
当然是:当 右子树高度 - 左子树高度 > 1
时,才执行左旋转。
这里就得到一些信息:
-
每次添加完一个节点后,就需要检查树的高度
-
满足
右子树高度 - 左子树高度 > 1
,那么一定满足下面的条件:- 左子树高度为 1
- 右子树高度为 3
也就是符合这张图
|
//左旋转 |
|
public void leftRotate(){ |
|
//使用当前跟节点创建一个新节点 |
|
Node newNode = new Node(value); |
|
//当前根节点的左子树设置为新节点的左子树 |
|
newNode.left = left; |
|
//当前根节点的右子树的左子树设置为当前新节点的右子树 |
|
newNode.right = right.left; |
|
//将右子树的值拷贝到当前根节点 |
|
value = right.value; |
|
//将跟节点的右子树设置为右子树的右子树 |
|
right = right.right; |
|
//将根节点左子树设置为新节点 |
|
left = newNode; |
|
} |
右旋转
其实这个就很好理解了:
-
左旋转:
右 - 左 > 1
,把右边的往左边旋转一层 -
右旋转:
左 - 右 > 1
,把左边的往右边旋转一层
他们其实是反着来的,那么右旋转的思路如下:
-
创建一个新的节点
newNode
,值等于当前 根节点 的值(以 4 创建) - 把新节点的 右子树 设置为当前节点的 右子树
- 把新节点的 左子树 设置为当前节点的 左子树的右子树
- 把 当前节点 的值换为 左子节点 的值
- 把 当前节点 的左子树设置为 左子树的左子树
- 把 当前节点 的右子树设置为新节点
|
//右旋转 |
|
public void rightRotate(){ |
|
Node newNode = new Node(value); |
|
newNode.right = right; |
|
newNode.left = left.right; |
|
value = left.value; |
|
left = left.left; |
|
right = newNode; |
|
} |
双旋转
在前面的例子中,使用单旋转(即一次旋转)就可以将非平衡二叉树转换为平衡二叉树。
但是在某些情况下,就无法做到。比如下面这两组数列
左侧这个树满足 leftHeight - rightHeight > 1
,也就是满足右旋转,旋转之后,树结构变化了。但是还是一个非平衡二叉树。
它的主要原因是:root 左子树的 左子树高度 小于 右子树的高度。即:节点 7 的左子树高度小于右子树的高度。
解决办法:
- 先将 7 这个节点为 root 节点,进行左旋转
- 再将原始的 root 节点进行右旋转
过程示意图如下:
其实可以参考下前面两个单旋转的图例,它有这样一个特点:
-
右旋转:
- root 的 left 左子树高度 大于 右子树高度
- 右旋转的时候,会将 left.right 旋转到 right.left 节点上
-
左旋转:
- root 的 right 右子树高度 大于 左子树高度
- 左旋转的时候,会将 right.left 旋转到 left.right 上。
如果不满足这个要求,在第二个操作的时候,就会导致 2 层的高度被旋转到 1 层的节点下面,导致不平衡了。
|
//左旋转 |
|
if(rightHeight() - leftHeight() > 1){ |
|
//如果右子树的左子树高度大于右子树的右子树,先进行右旋转,再进行左旋转 |
|
if(right != null && right.leftHeight() > right.rightHeight()){ |
|
right.rightRotate(); |
|
leftRotate();//左旋转 |
|
}else{ |
|
leftRotate(); |
|
} |
|
return;//进行了旋转之后就不用进行下面的再次旋转 |
|
} |
|
//右旋转 |
|
if(leftHeight() - rightHeight() > 1){ |
|
//如果左子树的右子树高度,大于左子树的左子树先进行左旋转,再进行右旋转 |
|
if(left != null && left.rightHeight() > left.leftHeight()){ |
|
left.leftRotate();//左子树左旋转 |
|
rightRotate();//右旋转 |
|
}else{ |
|
rightRotate(); |
|
} |
|
} |
完整代码
|
//AVL树 |
|
class AVLTree{ |
|
private Node root; |
|
|
|
public Node getRoot() { |
|
return root; |
|
} |
|
public void add(Node node){ |
|
if(root == null){ |
|
root = node; |
|
}else{ |
|
root.add(node); |
|
} |
|
} |
|
public void infixOrder(){ |
|
if(root != null){ |
|
root.infixOrder(); |
|
}else{ |
|
System.out.println("二叉排序树为空"); |
|
} |
|
} |
|
//查找要删除的节点 |
|
public Node search(int value){ |
|
if(root != null){ |
|
return root.search(value); |
|
}else{ |
|
return null; |
|
} |
|
} |
|
//查找要删除的父节点 |
|
public Node searchParent(int value){ |
|
if(root != null){ |
|
return root.searchParent(value); |
|
}{ |
|
return null; |
|
} |
|
} |
|
//找到目标节点为根节点最小的那个节点,并删除其节点,然后返回其值 |
|
public int delRightMin(Node root){ |
|
Node target = root; |
|
while(target.left != null){ |
|
target = target.left; |
|
} |
|
delNode(target.value); |
|
return target.value; |
|
} |
|
|
|
//删除节点 |
|
public void delNode(int value){ |
|
Node targetNode = search(value); |
|
//要删除的节点是根节点 |
|
if(targetNode == root){ |
|
root = null; |
|
return; |
|
} |
|
Node parentNode = searchParent(value); |
|
|
|
if(targetNode.left == null && targetNode.right == null){//删除的叶子节点 |
|
if(parentNode.left == targetNode){//当目标节点为父节点的左子节点 |
|
parentNode.left = null; |
|
}else{ |
|
parentNode.right = null; |
|
} |
|
}else if(targetNode.left != null && targetNode.right != null){//删除的节点为有两个子节点的非叶子节点 |
|
targetNode.value = delRightMin(targetNode.right); |
|
|
|
}else{//删除的节点为只有一个节点的非叶子节点 |
|
if(targetNode.left != null){ |
|
if(parentNode != null){ |
|
if(parentNode.left == targetNode){ |
|
parentNode.left = targetNode.left; |
|
}else{ |
|
parentNode.right = targetNode.left; |
|
} |
|
}else{ |
|
root = targetNode.left; |
|
} |
|
}else { |
|
if(parentNode != null){ |
|
if(parentNode.left == targetNode){ |
|
parentNode.left = targetNode.right; |
|
}else{ |
|
parentNode.right = targetNode.right; |
|
} |
|
}else{ |
|
root = targetNode.right; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
//节点 |
|
class Node{ |
|
int value; |
|
Node left; |
|
Node right; |
|
|
|
public Node(int value) { |
|
this.value = value; |
|
} |
|
//获得左子树的高度 |
|
public int leftHeight(){ |
|
if(left == null){ |
|
return 0; |
|
} |
|
return left.height(); |
|
} |
|
//获得右子树的高度 |
|
public int rightHeight(){ |
|
if(right == null){ |
|
return 0; |
|
} |
|
return right.height(); |
|
} |
|
//获得当前节点高度 |
|
public int height(){ |
|
return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height())+1; |
|
} |
|
|
|
//左旋转 |
|
public void leftRotate(){ |
|
//使用当前跟节点创建一个新节点 |
|
Node newNode = new Node(value); |
|
//当前根节点的左子树设置为新节点的左子树 |
|
newNode.left = left; |
|
//当前根节点的右子树的左子树设置为当前新节点的右子树 |
|
newNode.right = right.left; |
|
//将右子树的值拷贝到当前根节点 |
|
value = right.value; |
|
//将跟节点的右子树设置为右子树的右子树 |
|
right = right.right; |
|
//将根节点左子树设置为新节点 |
|
left = newNode; |
|
} |
|
//右旋转 |
|
public void rightRotate(){ |
|
Node newNode = new Node(value); |
|
newNode.right = right; |
|
newNode.left = left.right; |
|
value = left.value; |
|
left = left.left; |
|
right = newNode; |
|
} |
|
|
|
public void add(Node node){ |
|
if(node == null){ |
|
return; |
|
} |
|
if(node.value < this.value){ |
|
if(this.left == null){ |
|
this.left = node; |
|
}else{ |
|
this.left.add(node);//递归查找添加 |
|
} |
|
}else{ |
|
if(this.right == null){ |
|
this.right = node; |
|
}else{ |
|
this.right.add(node); |
|
} |
|
} |
|
//左旋转 |
|
if(rightHeight() - leftHeight() > 1){ |
|
//如果右子树的左子树高度大于右子树的右子树,先进行右旋转,再进行左旋转 |
|
if(right != null && right.leftHeight() > right.rightHeight()){ |
|
right.rightRotate(); |
|
leftRotate();//左旋转 |
|
}else{ |
|
leftRotate(); |
|
} |
|
return;//进行了旋转之后就不用进行下面的再次旋转 |
|
} |
|
//右旋转 |
|
if(leftHeight() - rightHeight() > 1){ |
|
//如果左子树的右子树高度,大于左子树的左子树先进行左旋转,再进行右旋转 |
|
if(left != null && left.rightHeight() > left.leftHeight()){ |
|
left.leftRotate();//左子树左旋转 |
|
rightRotate();//右旋转 |
|
}else{ |
|
rightRotate(); |
|
} |
|
} |
|
} |
|
} |