使用Java实现平衡二叉树
首先我们先了解一下什么是平衡二叉树
平衡二叉树又叫AVL树,属于二叉搜索树(二叉排序树)的一种。
这里又提到了二叉搜索树,二叉搜索树是一种二叉树,他的特点是他的左子树节点的值<节点的值<右子树节点的值,这种特点有利于数据的查找(名字当中专门有搜索二字,肯定是为了方便搜索建立的数据结构),但是二叉搜索树有一个很大的缺点,在数据量极端情况(数据有序变大,或者有序变小)下会变成一个链表
比如:
/**
*
* 存储数据:5、6、7、8、9
* 就会变成
* 5
* \
* 6
* \
* 7
* \
* 8
* \
* 9
*/
就从树变成了一个链表,而平衡二叉树就是为了解决这个问题出来的
平衡二叉树的特点
1.左子树节点的值<节点的值<右子树节点的值
2.左子树高度-右子树高度不得>1(可以是-1,0,1)
下面我们进行具体实现,话不多说看代码
树的基本类
定义树的基本属性,然后我们在此基础上进行后续代码编写
public class AVLTree<E> {//采用泛型,
public Node root;//树的根节点
public Integer size = 0;// 树的大小
//相关元素的比较器(别急,关于这个元素的相关操作我们在插入操作的最开始讲解了一下)
public Comparator comparator;
}
节点类
定义一个表示树节点的类
class Node<E>{
Node parent;//父节点
Node leftChild;//左节点
Node rightChild;//右节点
int height = 1;//高度,咱们这里默认给1
E value;//节点的值
public Node() {
}
public Node(Node parent, Node leftChild, Node rightChild, E value)
{
this.parent = parent;
this.leftChild = leftChild;
this.rightChild = rightChild;
this.value = value;
}
public boolean isLeaf() {
//判断当前节点是否是叶子节点(其实就是度的计算)
return leftChild == null && rightChild == null;
}
public boolean hasTwoChildren() {
//判断当前节点的度数(其实就是查看左右子树是否都存在)
return leftChild != null && rightChild != null;
}
public boolean isLeftChild() {
//判断当前节点是否是父节点的左节点(如果本节点有父节点的话)
return parent != null && this == parent.leftChild;
}
public boolean isRightChild() {
//判断当前节点是否是父节点的右节点(如果本节点有父节点的话)
return parent != null && this == parent.rightChild;
}
public void updateHeight(){
//更新节点高度,节点高度等于左右子树中最高树的高度,再加上根节点高度(1)
int leftHeight = leftChild == null ? 0: leftChild.height;
int rightHeight = rightChild == null ? 0: rightChild.height;
height = 1 + Math.max(leftHeight,rightHeight);
}
public int balanceFactor(){
//计算平衡因子(平衡因子其实就是左子树的高度减去右子树的绝对值)
int leftHeight = leftChild == null ? 0: leftChild.height;
int rightHeight = rightChild == null ? 0: rightChild.height;
return Math.abs(leftHeight-rightHeight);
}
private Node tallerChild(){
//返回当下节点两个子树当中最高的树
//如果左右两个子树,高度一致,就按照当下是父节点的左子树还是右子树,进行返回(如果是根节点直接返回右子树)
int leftHeight = leftChild == null ? 0: leftChild.height;
int rightHeight = rightChild == null ? 0: rightChild.height;
if (leftHeight > rightHeight) return leftChild;
if (leftHeight < rightHeight) return rightChild;
return isLeftChild() ? leftChild : rightChild;
}
}
添加
平衡二叉树的添加其实说白了就是分两步进行,
- 将节点插入树(与二叉搜索树的插入相同)
- 第二步就是调整平衡,让整个树(包含树当中的每一个子树)
都符合平衡二叉树的特点(左子树高度-右子树高度不得>1)
我们首先实现插入操作
比较方法的实现
在进行插入之前我们先讲解一下compare(比较)方法的实现。
平衡二叉树的插入操作,我们需要通过比较要插入的值与已经存在值的大小关系,确定节点的位置(是哪个节点的子树?,是左子树还是右子树?)。
树的值元素是不确定,所以没办法固定写死使用一种比较规则,
所以我们要编写两种方式让使用者将元素的比较规则传入
- 我们设置一个构造方法,使用者可以通过此构造函数传入自己编写的比较器(还记得我们刚开始设置树的基本属性设置的public Comparator comparator; 属性吗,这里就用上了,接着看)
public AVLTree(Comparator comparator){
this.comparator=comparator;
}
- 另一种方式是传入的元素默认实现了Comparable接口,定义了排序规则
/**
* @return 返回值等于0,代表e1和e2相等;返回值大于0,代表e1大于e2;返回值小于于0,代表e1小于e2
*/
public int compare(E e1,E e2){
if (comparator!=null){
//如果通过构造函数传入比较器将优先使用比较器
return comparator.compare(e1,e2);
}
return ((Comparable)e1).compareTo(e2);
}
为什么要设置这两种方式,我们举个例子来说:
我们设定树的元素是Integer(Integer是实现了 Comparable接口的)我们不再额外规定一个比较器,这个时候,我们最后排列出来的树 右节点的值>根节点的值>左节点的值
但是我们偶然间想让 左子树的值是最大的,想给他反过来,这个时候怎么办,修改Integer源码?,不用!我们只需要根据构造函数传入一个比较器就可以了
真正的添加!
话不多说直接看代码
public boolean add(E value){
if (root == null){
//这步没什么好说的,就是当树为空(根节点都是空,怎么可能有其余结构),将节点当做根节点
Node<E> node = new Node(null,null,null,value);
root = node;
size++;
return true;
}
Node node = root;
Node parentNode = new Node();
int result = 0;
while(node!=null){
//首先确定插入节点的位置(确定他是哪个节点下的子节点)
//从根节点开始比较,依次跟节点比较,直到树的末端(没有后续节点)
result = compare((E) node.value,value);
parentNode = node;
if (result>0){//说明新增值小于比较的节点值(应该往是左子树的一部分),接着与左子树进行比较
node = node.leftChild;
}else if (result<0){//说明新增值大于比较的节点值(应该往是右子树的一部分),接着与右子树进行比较
node = node.rightChild;
}else {
node.value = value;//如果相同就进行覆盖处理
}
}
//确定位置之后,知道是哪个节点下的子节点,再根据result的大小确定是该节点的左子树还是右子树
if (result>0){
Node<E> nodeAdd = new Node(parentNode,null,null,value);
parentNode.leftChild = nodeAdd;
}else{
Node<E> nodeAdd = new Node(parentNode,null,null,value);
parentNode.rightChild = nodeAdd;
}
size++;
return true;
}
调整平衡
插入完成之后,我们调整平衡(我的理解是通过旋转进行操作)
插入完成之后又四种情况(针对每种情况进行不同处理)(原谅我画图丑)
第一种情况 LL:右单旋转
- 出现平衡问题节点的左子树高度高于右子树,
- 插入点位于 出现平衡问题节点 的 左子节点 的左节点(可能比较绕,咱们具体看图)(左侧为出现问题情景,右侧为旋转过后)
第二种情况 LL:右单旋转
- 出现平衡问题节点的右子树高度高于左子树,
- 插入点位于 出现平衡问题节点 的 右子节点 的 右节点(可能比较绕,咱们具体看图)
(左侧为出现问题情景,右侧为旋转过后)
第三种情况 RL平衡旋转:先右后左
- 比较复杂需要进行两次旋转
- 出现平衡问题节点的右子树高度高于左子树,
- 插入点位于 出现平衡问题节点 的 右子节点 的 左节点(可能比较绕,咱们具体看图)
(左侧为出现问题情景,中间是进行第一次旋转。右侧为第二次旋转过后)
第四种情况 LR平衡旋转:先左后右
- 比较复杂需要进行两次旋转
- 出现平衡问题节点的左子树高度高于右子树,
- 插入点位于 出现平衡问题节点 的 左子节点 的 右节点(可能比较绕,咱们具体看图)
(左侧为出现问题情景,中间是进行第一次旋转。右侧为第二次旋转过后)
总结一下
第三种第四种情况,其实就是相当于先将出现平衡问题节点的子节点先进行旋转,转换成第一二种情况,然后再进行旋转
而且旋转其实就两种:左旋跟右旋
话不多说,咱们看看具体代码
左旋
private void leftRotate(Node grandNode){
//要进行左旋转,所以出问题的是grandNode的右子树,我们将它命名为parentNode
Node parentNode = grandNode.rightChild;
// parentNode的左子树,给予grandNode的右子树
grandNode.rightChild = parentNode.leftChild;
// parentNode的左子树就变为grantNode
parentNode.leftChild = grandNode;
//parentNode的父节点变为,grantNode的父节点
parentNode.parent = grandNode.parent;
// 判读grandNode原本是父节点的左子树还是右子树,相应进行改变
if (grandNode.isLeftChild()){
grandNode.parent.leftChild = parentNode;
}else if (grandNode.isRightChild()){
grandNode.parent.rightChild = parentNode;
} else {
//当grandNode既不是父节点的左子树,也不是父节点的右子树,那么相当于grandNode是根节点,相应进行改变
root = parentNode;
}
//grantNode的父节点变为parentNode
grandNode.parent = parentNode;
//parentNode原本的左子树(现在grandNode的右子树)的父节点变为grandNode
if (grandNode.rightChild != null ){
grandNode.rightChild.parent = grandNode;
}
//parentNode的高度是比较低的,咱们先更新parentNode的高度(更新高度可以看Node类当中封装的方法)
parentNode.updateHeight();
//更新grandNode的高度(更新高度可以看Node类当中封装的方法)
grandNode.updateHeight();
}
右旋:
private void rightRotate(Node grandNode){
//右旋转,所以出问题的是grandNode的左子树,我们将它命名为parentNode
Node parentNode = grandNode.leftChild;
// parentNode的右子树,给予grandNode的左子树
grandNode.leftChild = parentNode.rightChild;
// parentNode的右子树就变为grantNode
parentNode.rightChild = grandNode;
//parentNode的父节点变为,grantNode的父节点
parentNode.parent = grandNode.parent;
// 判读grandNode原本是父节点的左子树还是右子树,相应进行改变
if (grandNode.isLeftChild()){
grandNode.parent.leftChild = parentNode;
}else if (grandNode.isRightChild()){
grandNode.parent.rightChild = parentNode;
} else {
//当grandNode既不是父节点的左子树,也不是父节点的右子树,那么相当于grandNode是根节点,相应进行改变
root = parentNode;
}
//grantNode的父节点变为parentNode
grandNode.parent = parentNode;
if (grandNode.rightChild != null ){
//parentNode原本的左子树(现在grandNode的右子树)的父节点变为grandNode
grandNode.rightChild.parent = grandNode;
}
//同左旋
parentNode.updateHeight();
grandNode.updateHeight();
}
如何判断是否需要平衡
我们通过判断节点的平衡因子来确定是否是需要平衡
来看具体实现代码
private void balanceTree(Node node){
//通过新增节点,一层层的对父节点进行判断,如果平衡就更新高度,如果不平衡则进行平衡(平衡因子的方法在Node类当中)(后边有对二者进行粘贴,方便查看)
while ((node = node.parent)!=null){
if (node.balanceFactor()<=1){
node.updateHeight();
}else {
toBalance(node);
break;
}
}
}
public void updateHeight(){ //更新节点高度,节点高度等于左右子树中最高树的高度,再加上根节点高度(1)
int leftHeight = leftChild == null ? 0: leftChild.height;
int rightHeight = rightChild == null ? 0: rightChild.height;
height = 1 + Math.max(leftHeight,rightHeight);
}
public int balanceFactor(){
//计算平衡因子(平衡因子其实就是左子树的高度减去右子树的绝对值)
int leftHeight = leftChild == null ? 0: leftChild.height;
int rightHeight = rightChild == null ? 0: rightChild.height;
return Math.abs(leftHeight-rightHeight);
}
具体的平衡方法(对刚开始讲的四种情况进行分析,分别给予解决方案)
private Node tallerChild(){
//返回当下节点两个子树当中最高的树
//如果左右两个子树,高度一致,就按照当下是父节点的左子树还是右子树,进行返回(如果是根节点直接返回右子树)
int leftHeight = leftChild == null ? 0: leftChild.height;
int rightHeight = rightChild == null ? 0: rightChild.height;
if (leftHeight > rightHeight) return leftChild;
if (leftHeight < rightHeight) return rightChild;
return isLeftChild() ? leftChild : rightChild;
}
private void toBalance(Node grandNode){
Node<E> parent = (grandNode).tallerChild();//找到左右子树当中最高的树
Node<E> node = (parent).tallerChild();
//导致不平衡的因素,用通俗的话来讲就是左右两边,某一边变重了,需要进行操作对其平衡,在实现当中其实这个"重"其实就是高度是两边当中比较高的
//不是说两边必须一样"重",而是因为能进到这个方法其实就说明这个节点是不平衡的,那导致这个节点不平衡,肯定就是有一方"重"
if (parent.isRightChild()){
if (node.isRightChild()){//上面讲的第二种情况 在这个情况里失衡点的右节点与失衡点的右节点的右节点两个点都是比较"重"的需要进行操作,这个时候进行一次左旋即可
leftRotate(grandNode);
}else {//上面讲的第三种情况 在这个情况里失衡点的右节点与失衡点的右节点的左节点两个点都是比较"重"的需要进行操作,这个时候需要先进行一次右旋,然后再进行一次左旋
rightRotate(parent);
leftRotate(grandNode);
}
}else {
if (node.isLeftChild()){//上面讲的第一种情况 在这个情况里失衡点的左节点与失衡点的左节点的左节点两个点都是比较"重"的需要进行操作,这个时候进行一次右旋即可
rightRotate(grandNode);
}else {上面讲的第四种情况 在这个情况里失衡点的左节点与失衡点的左节点的右节点两个点都是比较"重"的需要进行操作,这个时候需要先进行一次左旋,然后再进行一次右旋
leftRotate(parent);
rightRotate(grandNode);
}
}
}
做完平衡操作与插入操作,我们将其合并形成最终的add添加方法
public boolean add(E value){
if (root == null){
Node<E> node = new Node(null,null,null,value);
root = node;
size++;
balanceTree(node);
return true;
}
Node node = root;
Node parentNode = new Node();
int result = 0;
while(node!=null){
result = compare((E) node.value,value);
parentNode = node;
if (result>0){//说明新增值小于比较的节点值,接着与左子树进行比较
node = node.leftChild;
}else if (result<0){
node = node.rightChild;
}else { //相同就不进行操作了,说明树当中有了
node.value = value;//如果相同就进行覆盖处理
}
}
if (result>0){
Node<E> nodeAdd = new Node(parentNode,null,null,value);
parentNode.leftChild = nodeAdd;
balanceTree(nodeAdd);
}else{
Node<E> nodeAdd = new Node(parentNode,null,null,value);
parentNode.rightChild = nodeAdd;
balanceTree(nodeAdd);
}
size++;
return true;
}
删除操作
删除操作跟增加操作一样分为两步
- 首先查找到相应值对应得节点
- 删除操作
- 对树进行平衡操作(平衡操作跟上述插入的操作是一致的,这里不再赘述)
第一步就不进行说明了,直接看代码即可
删除节点
删除操作分为几种情况(根据节点度来进行分类删除)
- 度为0
这种情况直接删除就可 - 度为1
度为1,证明该节点有一个子节点(左子节点或者右子节点)如图
我们开始尝试删除红色节点(先不考虑平衡操作)
- 将绿色节点父节点改为黑色节点(不是根节点也一样操作)
- 将黑色节点的左节点(或者右节点)改为绿色节点
- 度为2
还是刚刚的图,我们来研究一下删除黑色节点
- 我们想删除黑色节点,就需要找寻一个节点可以代替这个节点,就需要找到一个
节点,这个节点的值必须大于现在黑色节点左子树里的所有值,小于黑色节点右子树里的所有值 - - - - - - - -其实就是找寻左子树当中最大的或者右子树当中最小的 - 我们就来找一个左子树当中最大的就是图中蓝色节点,我们只需要将蓝色节点的值覆盖掉黑色,然后删除蓝色节点即可
- 因为我们找寻的是左子树当中最大的或者右子树当中最小的,所以找寻的这个节点的度只有可能是 0或者1,删除的时候再次按照度为0或者度为1 的删除即可
删除节点的平衡操作
还是这个图,删除红色或者绿色节点之后,删除节点以下的节点(所有子节点或者子树)是不会受到影响的,只可能是父节点收到影响,所以还是跟插入操作的平衡是一致的
真正的删除
代码如下:
- 查找相应值对应的节点
public Node queryNode (E element){
//从根节点往下进行寻找比较
//值相同返回,值大则往右子树寻找
//值小则往左子树寻找
Node<E> node = root;
while (node!=null){
int result = compare(node.value,element);
if (result>0){
node = node.leftChild;
}else if (result<0){
node = node.rightChild;
}else {
return node;
}
}
return null;
}
2.删除操作
找寻左子树当中最大(其实就是删除节点的左子树的右子树的右子树的右子树…)
private Node predecessor(Node node){
if (node == null){
return null;
}
Node nodeLeft = node.leftChild;
if (nodeLeft != null){
while (nodeLeft.rightChild!=null){
nodeLeft = nodeLeft.rightChild;
}
return nodeLeft;
}
return null;
}
找寻右子树当中最小(其实就是删除节点的右子树的左子树的左子树的左子树…)
private Node successor(Node node){
if (node == null){
return null;
}
Node noderight = node.rightChild;
if (noderight != null){
while (noderight.leftChild!=null){
noderight = noderight.leftChild;
}
return noderight;
}
return null;
}
public void remove(E element){
Node node = queryNode(element);
if (node == null) return;
size--;
//度为2的节点只需要将找到的
if (node.hasTwoChildren()) { // 度为2的节点
// 找到左子树当中最大值的节点
Node<E> s = predecessor(node);
// 用找到的左子树当中最大值的节点的值覆盖度为2的节点的值
node.value = s.value;
// 删除左子树当中最大值的节点
node = s;
}
// 删除node节点(node的度必然是1或者0)
Node<E> replacement = node.leftChild != null ? node.leftChild : node.rightChild;
if (replacement != null) { // node是度为1的节点
// 更改parent
replacement.parent = node.parent;
// 更改parent的left、right的指向
if (node.parent == null) { // node是度为1的节点并且是根节点
root = replacement;
} else if (node == node.parent.leftChild) {
node.parent.leftChild = replacement;
} else { // node == node.parent.right
node.parent.rightChild = replacement;
}
// 删除节点之后的处理
balanceTree(node);
} else if (node.parent == null) { // node是叶子节点并且是根节点
root = null;
// 删除节点之后的处理
//afterRemove(node);
} else { // node是叶子节点,但不是根节点
if (node == node.parent.leftChild) {
node.parent.leftChild = null;
} else { // node == node.parent.right
node.parent.rightChild = null;
}
//删除节点之后平衡操作
//删除节点以下的节点(所有子节点或者子树)是不会受到影响的,只可能是父节点收到影响,所以还是跟插入操作的平衡是一致的
//其实就是去判断父节点的平衡因子(相应的进行操作)
balanceTree(node);
}
}