高度平衡二叉树的构建_我所知道数据结构之平衡二叉树

前言需求介绍

我们从一个数列(1,2,3,4,5,6),来说明构成二叉排序树的一些问题

1.左子树全部为空, 从形式图所看,更像一个单链表

2.查询速度明显降低(因为需要依次比较),不能发挥BST

的优势,因为每次还需要比较左子树,其查询速度比单链表还慢

那么怎么办?

那么像这样的数列我们可以是用解决方案--->平衡二叉树(AVL)

一、什么是平衡二叉树

基本介绍

1.平衡二叉树也叫平衡二叉搜索树(Self balancing binary searchtree)又被称为AVL树, 可以保证查询效率较高。

有以下特点:

1.它是一颗空树或它的左右两个子树的高度差绝对超过1

2.左右两个子树都是一棵平衡二叉树。

平衡二叉树的常用实现:红黑树、AVL(算法)、替罪羊树、Treap、伸展树等。

哪些树是AVL树?为什么?

结合前面我们介绍的AVL树特点分析看看,现在你知道了吗?

简单说来

平衡二叉树之所以将二叉排序树,调整为平衡状态,是为了在二叉排序树近似为链的情况下,增强其查找性能,降低时间复杂度。

常见调整方式

常见的二叉平衡树调整平衡方法有:LL、LR、RR、RL

RR型介绍

当前这种情况图三,链式就需要平衡调整,否则则影响到查询的效率

平衡调整:一个根节点与两左右子节点的二叉排序树(如下图)

本来Mar节点再插入May时,还保持平衡:满足右子节点大于根节点

当插入麻烦节点Nov时平衡点被破坏,且Nov节点是在根节点的右子树的右子树上

根据二叉排序的特性:右子树节点比当前节点大

我们找到Mar、May、Nov的中间数,它们的大小关系是Mar

此时将May作为根节点,Mar作为左子树、Nov作为右子树进行调整

这时,这种插入即称呼为RR插入,平衡调整也成为RR旋转(右单转)

LL型介绍

当插入麻烦节点Apr时平衡点被破坏,且Apr节点是在Mar节点的左子树的左子树上

我们找到Mar、Aug、Apr的中间数,它们的大小关系是Apr

此时将Aug作为根节点,Apr作为左子树、Mar作为右子树进行调整

这时,这种插入即称呼为LL插入,平衡调整也成为LL旋转(左单转)

当然插入的节点也可能是左子节点或者右子节点

我们发现其实进行旋转调整的时候呢

不一定是根节点才进行旋转。在中间的节点Mar也是可以的

LR介绍

当插入麻烦节点Jan时平衡点被破坏,且Jan节点是在May节点的左子树的右子树上

我们找到May、Aug、Mar的中间数,它们的大小关系是Aug

此时将Mar作为根节点,Aug作为左子树、May作为右子树

且Mar>Aug、Jan

这时,这种插入即称呼为LR插入,平衡调整也成为LR旋转

RL介绍

当插入麻烦节点Feb时平衡点被破坏,且Feb节点是在Aug节点的右子树的左子树上

我们找到Jan、Aug、Dec的中间数,它们的大小关系是Aug

此时将Dec作为根节点,Aug作为左子树、Jan作为右子树

且Jan>Dec、Feb>Dec 根据特性Feb作为Jan左子树进行调整

这时,这种插入即称呼为RL插入,平衡调整也成为RL旋转

LL、RR、LR、RL四种模式方法怎么判别?

即看插入节点把谁破坏了,跟被破坏节点是什么关系?

是左边的左边?右边的右边?还是左边的右边?右边的左边?

但是需要注意的是:平衡调整后仍为二叉排序树

二、通过示例认识平衡二叉树的右旋转

给你一个数列{4,3,6,5,7,8},让你能够高效的完成对数据的查询和添加

那么按照我们之前的思路,先构建:一颗二叉排序树

二叉排序树的特点?

非叶子节点特点:左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。

特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点

回顾构建二叉排序树思路分析判断左子节点的值是否比当前节点的值小

否则判断右子节点的值比当前节点的值大

递归判断是否符合左子节点、右子节点的条件

那么我们前面分析了在二叉排序树近似为链的情况下

1.从形式图所看,更像一个单链表

2.查询速度明显降低(因为需要依次比较),不能发挥BST的优势

所以我们需要进行调整:平衡状态增强其查找性能,降低时间复杂度

我们发现这颗二叉排序树左子树高度为1,右边子树高度为3

此时不符合平衡二叉树的特点:它是一颗空树或它的左右两个子树的高度差绝对超过1

根据上面介绍的四种平衡调整模式介绍,目前比较符合的是RR旋转

使用RR旋转图解案例思路分析

我们结合上面的图与RR旋转的思路一起来分析当前的案例

未插入节点8时,还保持平衡:满足右子节点大于根节点

当插入麻烦节点8时平衡点被破坏,且节点8是在根节点的右子树的右子树上

根据二叉排序的特性:右子树节点比当前节点大

我们找到4、6、7的中间数,它们的大小关系是4 < 6 < 7

此时将 6 作为根节点,4 作为左子树、 7 作为右子树进行调整

RR旋转代码实现思路分析创建一个新节点newNode等于当前根节点root,值相等

新节点newNode的左子树设置为当前根节点root的左子树

新节点newNode的右子树设置为当前根节点root的右子树的左子树

当前根节点root的值换为右子节点的值

当前根节点root的右子树设置成根节点root的右子树的右子树

当前根节点root的左子树设置为新节点

示例代码实现

大家有没有发现,节点与子树之间的高度是关键的

并不是说加入一个节点得时候就进行旋转,而是左右两个子树的高度差绝对超过1才去进行平衡调整

所以需要先完成事情是:统计当前树的高度、统计与左子树、或右子树的高度

那么我们用代码实践来统计:树的高度、左子树高度、右子树高度

(节点代码、AVL代码可参考二叉排序树相关代码)class Node{

int value;

Node left;

Node right;

public Node(int value) {

this.value = value;

}

/**

* @param value 希望删除的结点的值

* @return如果找到返回该结点,否则返回null

*/

public Node search(int value) {

if(value == this.value) { //找到就是该结点

return this;

} else if(value < this.value) {//如果查找的值小于当前结点,向左子树递归查找

//如果左子结点为空

if(this.left == null) {

return null;

}

return this.left.search(value);

} else { //如果查找的值不小于当前结点,向右子树递归查找

if(this.right == null) {

return null;

}

return this.right. search(value);

}

}

public Node searchParent(int value){

//如果当前节点是需要删除节点的父节点则返回

if((this.left!=null && this.left.value == value) ||

(this.right!=null && this.right.value == value)){

return this;

}else{

//如果查找的值小于当前节点的值,并且当前节点的左子节点不为空

if(value

return this.left.searchParent(value);

}else if(value >= this.value && this.right!=null){

//如果查找的值大于等于于当前节点的值,并且当前节点的右子节点不为空

return this.right.searchParent(value);

}else {

return null;//没有找到父节点,比如说节点7

}

}

}

//添加节点方法

//递归方式添加节点,要满足二叉排序树的要求

//要求是:`左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。`

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);

}

}

}

@Override

public String toString() {

return "Node{" +"value=" + value +'}';

}

//中序遍历

public void infixOrder(){

if(this.left != null){

this.left.infixOrder();

}

System.out.println(this);

if(this.right != null){

this.right.infixOrder();

}

}

}class AVLTree {

private Node root;

public Node getRoot() {

return root;

}

public void setRoot(Node root) {

this.root = root;

}

//添加节点的方法

public void add(Node node){

if(root == null){

root = node;

}else{

root.add(node);

}

}

/**

* @param node 传入的节点(当做新二叉排序树的根节点)

* return 返回新跟节点的最小节点的值

*/

public int delRigthTreeMin(Node node){

Node target = node;

//循环的查找左子节点,找到最小值

while(target.left!=null) {

target = target.left;

}

//删除最小值

delNode(target.value);

//返回最小值

return target.value;

}

public void delNode(int value){

if(root == null){

System.out.println("当前根节点为空!无法删除节点!");

return;

}else{

//1.需要先找到删除的值的对应节点

Node targetNode = search(value);

//如果没有找到需要删除的节点

if(targetNode == null){

System.out.println("对不起!没有找到删除节点信息!");

return;

}

//如果我们发现根节点没有左子节点与右子节点

if(root.left == null && root.right == null){

root = null;

return;

}

//找到targetNode 的父节点

Node parent = searchParent(value);

//如果删除节点是叶子节点

if(targetNode.left == null && targetNode.right == null){

//判断删除节点是父节点的左子节点还是右子节点

if(parent.left!= null && parent.left.value == targetNode.value){

parent.left = null;

}else if (parent.right!= null && parent.right.value == targetNode.value){

parent.right = null;

}

}else if (targetNode.left != null && targetNode.right != null){

int minValue = delRigthTreeMin(targetNode.right);

targetNode.value = minValue;//重置值

}else{//删除只有一颗子树的节点

//如果删除节点的子节点是左子节点

if(targetNode.left !=null){

if(parent!=null){

//判断删除节点是父节点的左子节点还是右子节点

if(parent.left.value == targetNode.value){

//将原删除节点的位置给到子节点

parent.left = targetNode.left;

}else if (parent.right.value == targetNode.value){

//将原删除节点的位置给到子节点

parent.right = targetNode.left;

}

}else{

root = targetNode.left;

}

}else if (targetNode.right != null){ //如果删除节点的子节点是右子节点

if(parent!=null){

//判断删除节点是父节点的左子节点还是右子节点

if( parent.left.value == targetNode.value){

//将原删除节点的位置给到子节点

parent.left = targetNode.right;

}else if (parent.right.value == targetNode.value){

//将原删除节点的位置给到子节点

parent.right = targetNode.right;

}

}else{

root = targetNode.right;

}

}

}

}

}

//查找需要删除节点的方法

public Node search(int value){

if(root == null){

return null;

}else{

return root.search(value);

}

}

//查找需要删除节点的父节点信息

public Node searchParent(int value){

if(root == null){

return null;

}else{

return root.searchParent(value);

}

}

//调用中序遍历的方法

public void infixOrder(){

if(root == null){

System.out.println("当前二叉排序根节点为空,无法遍历");

return;

}else{

root.infixOrder();

}

}

}

如上图所示,我们取的该这颗树的高度是为:3 ,算上根节点则是 4

所以我们求该树的高度,需要知道左节点与右节点的高度分别是多少class Node{

//......省略其他关键代码

//返回当前节点的左子树的高度

public int leftHigth(){

if(left == null){

return 0;

}

return left.hight();

}

//返回当前节点的右子树的高度

public int rightHight(){

if(right == null){

return 0;

}

return right.hight();

}

//返回当前节点的高度,若算上根节点则需要 + 1

public int hight(){

return Math.max(left == null? 0 : left.hight(),right == null?0:right.hight()) + 1;

}

}

有没有发现,我们需要知道左子树的高度是多少

同时也需要知道左子树的左子树与右边子树是多少.....

这是一个一直递归的过程。public static void main(String[] args) {

int[] arr ={4,3,6,5,7,8};

AVLTree avlTree = new AVLTree();

for(int i = 0; i

avlTree.add(new Node(arr[i]));

}

//遍历

System.out.println("中序遍历");

avlTree.infixOrder();

//节点高度

System.out.println("算上跟节点高度为:"+avlTree.getRoot().hight());

}

运行结果如下:

中序遍历

Node{value=3}

Node{value=4}

Node{value=5}

Node{value=6}

Node{value=7}

Node{value=8}

算上跟节点高度为:4

我们刚刚也说了,算上跟根节点高度就是4,那么我们看看左子树和右子树//节点高度

System.out.println("节点高度为:"+avlTree.getRoot().hight());

//节点高度

System.out.println("左节点高度为:"+avlTree.getRoot().leftHigth());

//节点高度

System.out.println("右节点高度为:"+avlTree.getRoot().rightHight());

运行结果如下:

节点高度为:4

左节点高度为:1

右节点高度为:3

我们刚刚说到,左右两个子树的高度差绝对超过1才去进行平衡调整,那么当前的情况则需要进行调整:右旋转class Node{

//......省略其他关键代码

//RR旋转方法

private void RightRotate(){

//创建一个新节点newNode等于当前根节点root,值相等

Node newNode = new Node(value);

//新节点newNode的左子树设置为当前根节点root的左子树

newNode.left = left;

//新节点newNode的右子树设置为当前根节点root的右子树的左子树

newNode.right = right.left;

//当前根节点root的值换为右子节点的值

value = right.value;

//当前根节点root的右子树设置成根节点root的右子树的右子树

right = right.right;

//当前根节点root的左子树设置为新节点

left = 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);

}

}

//当添加完一个节点后:如果(右子树的高度 - 左子树的高度)> 1 则执行RR旋转

if(rightHight() - leftHigth() > 1 ){

RightRotate();//执行RR旋转

}

}

}

接下来我们实践看看,当添加节点满足条件是否会进行平衡调整public static void main(String[] args) {

int[] arr ={4,3,6,5,7,8};

AVLTree avlTree = new AVLTree();

for(int i = 0; i

avlTree.add(new Node(arr[i]));

}

//遍历

System.out.println("中序遍历");

avlTree.infixOrder();

//节点高度

System.out.println("节点高度为:"+avlTree.getRoot().hight());

//节点高度

System.out.println("左节点高度为:"+avlTree.getRoot().leftHigth());

//节点高度

System.out.println("右节点高度为:"+avlTree.getRoot().rightHight());

}

运行结果如下:

中序遍历

Node{value=3}

Node{value=4}

Node{value=5}

Node{value=6}

Node{value=7}

Node{value=8}

节点高度为:3

左节点高度为:2

右节点高度为:2

这时我们进行平衡调整,从左右两个子树的高度差没有超过1了。

三、通过示例认识平衡二叉树的左旋转

给你一个数列{10,12,8,9,7,6},让你能够高效的完成对数据的查询和添加

那么按照我们之前的思路,先构建:一颗二叉排序树

二叉排序树的特点?

非叶子节点特点:左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。

特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点

回顾构建二叉排序树思路分析判断左子节点的值是否比当前节点的值小

否则判断右子节点的值比当前节点的值大

递归判断是否符合左子节点、右子节点的条件

我们发现这颗二叉排序树左子树高度为3,右边子树高度为1

此时不符合平衡二叉树的特点:它是一颗空树或它的左右两个子树的高度差绝对超过1

根据上面介绍的四种平衡调整模式介绍,目前比较符合的是LL旋转

根据我们前面的示例经验,我们可以直接进行实现思路分析创建一个新节点newNode等于当前根节点root,值相等

新节点newNode的右子树设置为当前根节点root的右子树

新节点newNode的左子树设置为当前根节点root的左子树的右子树

当前根节点root的值换为左子节点的值

当前根节点root的左子树设置成根节点root的左子树的左子树

当前根节点root的右子树设置为新节点

示例代码实现class Node{

//......省略其他关键代码

//LL旋转方法

private void leftRotate(){

//创建一个新节点newNode等于当前根节点root,值相等

Node newNode = new Node(value);

//新节点newNode的右子树设置为当前根节点root的右子树

newNode.right = right;

//新节点newNode的`左子树`设置为当前根节点root的`左子树的右子树`

newNode.left = left.right;

//当前根节点root的值换为左子节点的值

value = left.value;

//当前根节点root的左子树设置成根节点root的`左子树的左子树`

left = left.left;

//当前根节点root的右子树设置为新节点

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);

}

}

//当添加完一个节点后:如果(右子树的高度 - 左子树的高度)> 1 则执行RR旋转

if(rightHight() - leftHigth() > 1 ){

RightRotate();//执行RR旋转

}

//当添加完一个节点后:如果(左子树的高度 - 右子树的高度)> 1 则执行LL旋转

if(leftHigth() - rightHight() > 1 ){

leftRotate();//执行LL旋转

}

}

}

接下来我们Demo测试一下为调整之前的高度分别是多少public static void main(String[] args) {

//int[] arr ={4,3,6,5,7,8};

int[] arr ={10,12,8,9,7,6};

AVLTree avlTree = new AVLTree();

for(int i = 0; i

avlTree.add(new Node(arr[i]));

}

//遍历

System.out.println("中序遍历");

avlTree.infixOrder();

//节点高度

System.out.println("节点高度为:"+avlTree.getRoot().hight());

//节点高度

System.out.println("左节点高度为:"+avlTree.getRoot().leftHigth());

//节点高度

System.out.println("右节点高度为:"+avlTree.getRoot().rightHight());

}

运行结果如下:

中序遍历

Node{value=6}

Node{value=7}

Node{value=10}

Node{value=9}

Node{value=8}

Node{value=12}

节点高度为:3

左节点高度为:2

右节点高度为:2

这时我们进行平衡调整,从左右两个子树的高度差没有超过1了。

四、通过示例认识平衡二叉树的双旋转

给你一个数列{10,11,7,6,8,9},让你能够高效的完成对数据的查询和添加

那么按照我们之前的思路,先构建:一颗二叉排序树

我们发现这颗二叉排序树左子树高度为3,右边子树高度为1

此时不符合平衡二叉树的特点:它是一颗空树或它的左右两个子树的高度差绝对超过1

按照我们之前思路适合的是LL旋转,那么我们执行左旋后的样子发现是

那么出现这种问题的原因是什么呢?

分析1:加入节点九时,左子树(节点7)的树高度 > 右子树(节点11) 的高度

分析2:左子树高度 - 右子树高度 >1 触发LL旋转,就变成上面那样了

那么我们可以根据上面的LR介绍可以猜到一些思路,解决这个问题.

我们在符合:执行LL旋转时条件时

思路1.获取左子树的右子树(节点8)高度,取名为:k

思路2.获取左子树(节点7)的高度,取名为:J

思路3.如果 k > J,对左子树进行RR旋转

思路4.如果 k < J,直接进行LL旋转

简单的一句话:如果K>J,则先旋转子树再旋转自己

同时我们在符合:执行RR旋转时条件时

思路1.获取右子树的左子树高度 ,取名为:U

思路2.获取右子树(节点11)的高度,取名为:L

思路3.如果U > L,对右子树进行LL旋转

思路4.如果U < L,直接进行RR旋转class Node{

//......省略其他关键代码

//优化添加节点操作

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);

}

}

//当添加完一个节点后:如果(右子树的高度 - 左子树的高度)> 1 则执行RR旋转

if(rightHight() - leftHigth() > 1 ){

//获取它的右子树的左子树的高度 取名U,获取它的右子树高度L

//如果u > l 执行LL旋转

if(right != null && right.leftHigth()> right.rightHight()){

right.leftRotate();//执行LL旋转

RightRotate();//执行RR旋转

}else{

RightRotate();

}

return;//防止接着往下走

}

//当添加完一个节点后:如果(左子树的高度 - 右子树的高度)> 1 则执行LL旋转

if(leftHigth() - rightHight() > 1 ){

//获取左子树的右子树高度,取名为:k 获取左子树的高度,取名为:J

//如果 k > J,对左子树进行RR旋转

if(left != null && left.rightHight() > left.leftHigth()){

left.RightRotate();//执行RR旋转

leftRotate();//执行LL旋转

}else{

leftRotate();//执行LL旋转

}

}

}

}

接下来让我们使用Demo验证一下我们的思路public static void main(String[] args) {

//int[] arr ={4,3,6,5,7,8};

int[] arr ={10,12,8,9,7,6};

AVLTree avlTree = new AVLTree();

for(int i = 0; i

avlTree.add(new Node(arr[i]));

}

//遍历

System.out.println("中序遍历");

avlTree.infixOrder();

//节点高度

System.out.println("节点高度为:"+avlTree.getRoot().hight());

//节点高度

System.out.println("左节点高度为:"+avlTree.getRoot().leftHigth());

//节点高度

System.out.println("右节点高度为:"+avlTree.getRoot().rightHight());

System.out.println("右节点高度为:"+avlTree.getRoot().rightHight());

System.out.println("当前根节点为:"+avlTree.getRoot());

System.out.println("当前根节点的左节点为:"+avlTree.getRoot().left);

System.out.println("当前根节点的右节点为:"+avlTree.getRoot().right);

}

运行结果如下:

中序遍历

Node{value=6}

Node{value=7}

Node{value=8}

Node{value=9}

Node{value=10}

Node{value=11}

节点高度为:3

左节点高度为:2

右节点高度为:2

当前根节点为:Node{value=8}

当前根节点的左节点为:Node{value=7}

当前根节点的右节点为:Node{value=10}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值