第十三章 二叉排序树和平衡二叉树

文章目录

  • 第十三章 二叉排序树和平衡二叉树
  • 一、二叉排序树
    • 1.介绍
    • 2.创建
    • 3.删除结点
    • 4.代码实现
  • 二、平衡二叉树
    • 1.引入
    • 2.介绍
    • 3.插入结点
    • 4.删除结点
    • 5.代码实现

一、二叉排序树

1.介绍

二叉排序树(BST,Binary Sort Tree),对于二叉排序树的任何一个非叶子结点,要求左子结点的值比当前结点的值小,右子结点的值比当前结点的值大。如果有相同的值,可以将该结点放在左子结点或者右子结点

2.创建

把数组第一个值作为根结点

  1. 下一个值比根结点小就向左子树递归,直到当前结点没有左子结点,就令其为当前结点的左子结点
  2. 下一个值比根结点大就向右子树递归,直到当前结点没有右子结点,就令其为当前结点的右子结点
  3. 重复上述两步,直至数组遍历完成

3.删除结点

删除叶子结点

  1. 找到要删除的结点 targetNode
  2. 找到 targetNode 的父结点 parent
  3. 确定 targetNode 是 parent 的左子结点还是右子结点
  4. 根据条件进行删除
    左子结点:parent.left = null
    右子结点:parent.right = null

删除只有一个子结点的结点

  1. 找到要删除的结点 targetNode
  2. 找到 targetNode 的父结点 parent
  3. 确定 targetNode 的子结点是左子结点还是右子结点(左记作 A,右记作 B)
  4. 确定 targetNode 是 parent 的左子结点还是右子结点(左记作 1,右记作 2)
  5. 根据条件进行删除
    A1:parent.left = targetNode.left
    A2:parent.right = targetNode.left
    B1:parent.left = targetNode.right
    B2:parent.right = targetNode.right

删除有两个子结点的结点

  1. 找到要删除的结点 targetNode
  2. 找到 targetNode 的父结点 parent
  3. 在 targetNode 的右子树中递归找到叶子结点中最小的最近的结点 minVal
  4. 创建临时变量 temp,temp = minVal
  5. 删除 minVal
  6. targetNode.value = temp

4.代码实现

package com.sisyphus.binarysorttree;/*** @Description: 二叉排序树$* @Param: $* @return: $* @Author: Sisyphus* @Date: 7/25$*/
public class BinarySortTreeDemo {public static void main(String[] args) {int[] arr = {7,3,10,12,5,1,9,0};BinarySortTree binarySortTree = new BinarySortTree();//循环地添加结点到二叉排序树for (int i : arr) {binarySortTree.add(new Node(i));}//中序遍历二叉排序树System.out.println("中序遍历:");binarySortTree.infixOrder();//测试删除binarySortTree.delNode(10);System.out.println("删除节点后");binarySortTree.infixOrder();}
}//创建二叉排序树
class BinarySortTree{private Node root;//查找要删除的结点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);}}/**** @param node  传入的结点(当作二叉排序树的根结点)* @return      以 node 为根节结点的二叉排序树的最小结点的值*/public int delRightTreeMin(Node node){Node target = node;//循环地查找左子结点,就会找到最小值while(target.left != null){target = target.left;}//这时 target 就指向了最小节点//删除最小结点delNode(target.value);return target.value;}//删除结点public void delNode(int value){if (root == null){return;}else {//1.需要先去找到要删除的结点 targetNodeNode targetNode = search(value);//如果没有找到要删除的结点if (targetNode == null){return;}//如果我们发现这棵二叉排序树只有一个结点if (root.left == null && root.right == null){root = null;return;}//去找到 targetNode 的父结点Node parent = searchParent(value);//如果要删除的结点是叶子结点if (targetNode.left == null && targetNode.right == null){//判断 targetNode 是父结点的左子结点还是右子结点if (parent.left != null && parent.left.value == value){    //删除结点是左子结点parent.left = null;}else if (parent.right != null && parent.right.value == value){ //删除结点是右子结点parent.right = null;}}else if(targetNode.left != null && targetNode.right != null){  //删除有两棵子树的结点int minVal = delRightTreeMin(targetNode.right);targetNode.value = minVal;}else{  //删除只有一棵子树的结点//如果要删除的结点有左子结点if (targetNode.left != null){if (parent != null){//如果 targetNode 是 parent 的左子结点if (parent.left.value == value){parent.left = targetNode.left;}else{ //targetNode 是 parent 的右子结点parent.right = targetNode.left;}}else {root = targetNode.left;}}else{  //如果要删除的结点是右子结点if (parent != null){//如果 targetNode 是 parent 的左子结点if (parent.left.value == value){parent.left = targetNode.right;}else{  //如果 target 是 parent 的右子结点parent.right = targetNode.right;}}else {root = targetNode.right;}}}}}//添加结点的方法public void add(Node node){if (root == null){root = node;//如果 root 为空则直接让 root 指向 node}else{root.add(node);}}//中序遍历public void infixOrder(){if (root == null){System.out.println("树为空,无法遍历");}else{root.infixOrder();}}
}//创建 Node
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);}}//查找要删除结点的父结点/**** @param value 要找到的结点的值* @return      返回的是要删除的结点的父结点,如果没有返回 null*/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 < this.value && this.left != null){return this.left.searchParent(value);   //向左子树递归查找}else if (value >= this.value && this.right != null){return this.right.searchParent(value);  //向右子树递归查找}else{return null;//没有找到父结点}}}@Overridepublic String toString() {return "Node{" +"value=" + value +'}';}//添加结点的方法//递归的形式添加结点,注意需要满足二叉排序树的要求public void add(Node node){if(node == null){return;}//判断传入的结点的值,和当前子树的根结点的值的关系if (node.value < this.value){//如果当前结点左子结点为 nullif (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 void infixOrder(){if (this.left != null){this.left.infixOrder();}System.out.println(this);if (this.right != null){this.right.infixOrder();}}
}

二、平衡二叉树

1.引入

二叉排序树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1, 3, 5, 7, 9,}。依据此序列构造的二叉排序树为右斜树,同时,二叉树退化成单链表,搜索效率降低为 O(n)

二叉排序树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。当结点数目一定时,保持树的左右两端的平衡,树的查找效率最高,由此就有了平衡二叉树

2.介绍

平衡二叉树(Balanced Binary Tree)又被称为 AVL 树,由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树。它是一 棵空树或者它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树

平衡因子

某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高

最小失衡子树

在新插入的节点向上查找,以第一个平衡因子的绝对值超过 1 的节点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树

平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋右旋

左旋

思路:

  1. 用当前结点的右子结点替代当前结点
  2. 右子结点的左子树变为当前结点的右子树
  3. 当前节点变为右子结点的左子树
private void leftRotate(){Node newNode = new Node(value);newNode.left = left;newNode.right = right.left;value = right.value;right = right.right;left = newNode;
}

下面三张图为实际左旋过程,有助于理解代码

右旋

思路:

  1. 用当前结点的左子结点替代当前结点
  2. 左子结点的右子树变为当前结点的左子树
  3. 当前结点变为左子结点的右子树
private void rightRotate(){Node newNode = new Node(value);newNode.right = right;newNode.left = left.right;value = left.value;left = left.left;right = newNode;
}

下面三张图为实际右旋过程,有助于理解代码

3.插入结点

平衡二叉树插入结点的情况分为以下四种

插入方式 描述 旋转方式
LL 在 A 的左子树根结点的左子树上插入结点而破坏平衡 右旋转
RR 在 A 的右子树根结点的右子树上插入结点而破坏平衡 左旋转
LR 在 A 的左子树根结点的右子树上插入结点而破坏平衡 先左旋后右旋
RL 在 A 的右子树根结点的左子树上插入结点而破坏平衡 先右旋后左旋

LL 和 RR 在上面的介绍中已经讲过了,我们着重讲 LR 和 RL

LR 和 RL 都是无法通过一次选择重新平衡的情况,如下图所示

A 的左子树高于右子树,如果只进行一次右旋转,则变化为:

我们发现 B 的右子树又高于左子树了

为了使插入方式为 LR 或者 RL 的平衡二叉树重新平衡,我们需要采取下面两种做法

对于 LR,先左旋后右旋

  1. 对根结点的左子结点进行左旋转
  2. 对根结点做右旋转

对于 RL。先右旋后左旋

  1. 对根结点的右子结点做右旋转
  2. 对根结点做左旋转

4.删除结点

平衡二叉树和二叉排序树的删除操作情况一致,都分为四种情况。但是平衡二叉树在删除结点后需要重新检查平衡性并修正

删除操作与插入操作后的平衡修正区别在于,插入操作后只需要对插入栈中的弹出的第一个非平衡点进行修正,而删除操作需要修正栈中的所有非平衡点

对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了

5.代码实现

package com.sisyphus.avltree;/*** @Description: 平衡二叉树$* @Param: $* @return: $* @Author: Sisyphus* @Date: 7/25$*/
public class AVLTreeDemo {public static void main(String[] args) {//int[] arr = {4,3,6,5,7,8};//int[] arr = {10,12,8,9,7,6};int[] arr = {10,11,7,6,8,9};//创建一个 AVLTree 对象AVLTree avlTree = new AVLTree();//添加结点for (int i = 0; i < arr.length; i++) {avlTree.add(new Node(arr[i]));}//遍历System.out.println("中序遍历");avlTree.infixOrder();System.out.println("平衡后:");System.out.println("树的高度:" + avlTree.getRoot().height());   //4System.out.println("树的左子树高度:" + avlTree.getRoot().leftHeight());System.out.println("树的右子树高度:" + avlTree.getRoot().rightHeight());System.out.println("当前的根结点为:" + avlTree.getRoot());//        //删除测试一,一个子结点
//        avlTree.delNode(7);
//        System.out.println("删除后的中序遍历:");
//        avlTree.infixOrder();//        //删除测试二,两个子结点
//        avlTree.delNode(10);
//        System.out.println("删除后的中序遍历:");
//        avlTree.infixOrder();//删除测试三,删除失衡avlTree.delNode(6);avlTree.delNode(7);System.out.println("删除后的中序遍历:");avlTree.infixOrder();System.out.println("树的高度:" + avlTree.getRoot().height());   //4System.out.println("树的左子树高度:" + avlTree.getRoot().leftHeight());System.out.println("树的右子树高度:" + avlTree.getRoot().rightHeight());System.out.println("当前的根结点为:" + avlTree.getRoot());System.out.println("当前根节点的左子结点为:" + avlTree.getRoot().left);System.out.println("当前根节点的右子结点为:" + avlTree.getRoot().right);System.out.println("当前根节点的左子结点的右子结点为:" + avlTree.getRoot().left.right);}}//创建 AVLTree
class AVLTree{private Node root;public Node getRoot() {return root;}public void setRoot(Node root) {this.root = root;}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);}}/**** @param node  传入的结点(当作二叉排序树的根结点)* @return      以 node 为根节结点的二叉排序树的最小结点的值*/public int delRightTreeMin(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){return;}else {Node targetNode = search(value);if (targetNode == null){return;}if (root.left == null && root.right == null){root = null;return;}Node parent = searchParent(value);if (targetNode.left == null && targetNode.right == null){if (parent.left != null && parent.left.value == value){parent.left = null;}else if (parent.right != null && parent.right.value == value){parent.right = null;}}else if(targetNode.left != null && targetNode.right != null){int minVal = delRightTreeMin(targetNode.right);targetNode.value = minVal;}else{if (targetNode.left != null){if (parent != null){if (parent.left.value == value){parent.left = targetNode.left;}else{parent.right = targetNode.left;}}else {root = targetNode.left;}}else{if (parent != null){if (parent.left.value == value){parent.left = targetNode.right;}else{parent.right = targetNode.right;}}else {root = targetNode.right;}}}}//当删除完一个结点后,如果(右子树的高度 - 左子树的高度) > 1,左旋转if (root.rightHeight() - root.leftHeight() > 1){//如果它的右子树的左子树高度大于它的右子树的高度if (root.right != null && root.right.leftHeight() > root.right.rightHeight()){//先对当前结点的右结点(右子树) 进行左旋转root.right.rightRotate();//再对当前结点进行左旋转root.leftRotate();}else{//直接进行左旋转即可root.leftRotate();return;}}//当删除完一个节点后,如果(左子树的高度 - 右子树的高度) > 1。右旋转if ((root.leftHeight() - root.rightHeight()) > 1){//如果它的左子树的右子树高度大于它的左子树的高度if (root.left != null && root.left.rightHeight() > root.left.leftHeight()){//先对当前结点的左结点(左子树) 进行左旋转root.left.leftRotate();//再对当前结点进行右旋转root.rightRotate();}else{//直接进行右旋转即可root.rightRotate();}}}//添加结点的方法public void add(Node node){if (root == null){root = node;}else{root.add(node);}}public void infixOrder(){if (root == null){System.out.println("树为空,无法遍历");}else{root.infixOrder();}}
}//创建 Node
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;}//查找要删除的结点/**** @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);}}//查找要删除结点的父结点/**** @param value 要找到的结点的值* @return      返回的是要删除的结点的父结点,如果没有返回 null*/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 < this.value && this.left != null){return this.left.searchParent(value);}else if (value >= this.value && this.right != null){return this.right.searchParent(value);}else{return null;}}}@Overridepublic String toString() {return "Node{" +"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);}}//当添加完一个结点后,如果(右子树的高度 - 左子树的高度) > 1,左旋转if (rightHeight() - leftHeight() > 1){//如果它的右子树的左子树高度大于它的右子树的高度if (right != null && right.leftHeight() > right.rightHeight()){//先对当前结点的右结点(右子树) 进行左旋转right.rightRotate();//再对当前结点进行左旋转leftRotate();}else{//直接进行左旋转即可leftRotate();return;}}//当添加完一个节点后,如果(左子树的高度 - 右子树的高度) > 1。右旋转if ((leftHeight() - rightHeight()) > 1){//如果它的左子树的右子树高度大于它的左子树的高度if (left != null && left.rightHeight() > left.leftHeight()){//先对当前结点的左结点(左子树) 进行左旋转left.leftRotate();//再对当前结点进行右旋转rightRotate();}else{//直接进行右旋转即可rightRotate();}}}//中序遍历public void infixOrder(){if (this.left != null){this.left.infixOrder();}System.out.println(this);if (this.right != null){this.right.infixOrder();}}
}

【Java数据结构与算法】第十三章 二叉排序树和平衡二叉树相关推荐

  1. JAVA数据结构和算法:第一章(时间复杂度和空间复杂度)

    数据结构 数据结构基础概念 不论是哪所大学,数据结构和算法这门课都被贯上无趣.犯困.困难的标签,我们从最基础最通俗的语言去说起,保证通俗易懂. 数据结构到底是什么呢?我们先来谈谈什么叫数据. 数据:数 ...

  2. 【Java数据结构与算法】第九章 顺序查找、二分查找、插值查找和斐波那契查找

    第九章 顺序查找.二分查找.插值查找和斐波那契查找 文章目录 第九章 顺序查找.二分查找.插值查找和斐波那契查找 一.顺序查找 1.基本介绍 2.代码实现 二.二分查找 1.基本介绍 2.代码实现 三 ...

  3. 【Java数据结构与算法】第一章 稀疏数组和队列

    第一章 稀疏数组和队列 文章目录 第一章 稀疏数组和队列 一.线性结构和非线性结构 1.线性结构 2.非线性结构 二.稀疏数组 三.队列 1.队列 2.环形队列 一.线性结构和非线性结构 1.线性结构 ...

  4. Java数据结构与算法(第二章数组)

    2019独角兽企业重金招聘Python工程师标准>>> 数组是应用最广泛的数据存储结构.它被植入到大部分编程语言中. Java中数组的基础知识     创建数组 在Java中把它们当 ...

  5. 【Java数据结构与算法】第二章 单链表及简单面试题

    第二章 单链表 文章目录 第二章 单链表 一.单链表 1.基本介绍 2.思路 3.代码实现 二.简单面试题 1.求单链表中有效节点的个数 2.查找单链表中的倒数第k个节点(新浪面试题) 3.单链表的反 ...

  6. Java数据结构与算法(十三):程序员常用的10种算法

    1. 二分查找算法(非递归) 1.1 基本介绍 二分查找法只适用于从有序数列中进行查找(比如数字和字母等),将数列排序后再进行查找: 二分查找法的运行时间为对数时间O(log2 n),即查找到需要的目 ...

  7. Java数据结构与算法(二)

    Java数据结构与算法(二) 第六章 递归 1 递归应用场景 2 递归的概念 3 递归调用机制 4 递归能解决什么样的问题 5 递归需要遵守的重要规则 6 递归-迷宫问题 6.1 迷宫问题 6.2 代 ...

  8. java数据结构与算法之双链表设计与实现

    转载请注明出处(万分感谢!): http://blog.csdn.net/javazejian/article/details/53047590 出自[zejian的博客] 关联文章: java数据结 ...

  9. Java 数据结构和算法的总结

    第1章综述 数据结构和算法能起到什么作用? 数据结构的概述 算法的概述 一些定义 面向对象编程 软件工程 对于C++程序员的Java Java数据结构的类库 第2章数组 Array专题Applet J ...

最新文章

  1. AAAI2020 | SNERL:抛开mention级别的监督,实体链接、关系抽取我都行
  2. java中object转int
  3. vscode / 杂项
  4. ACM MM 2018论文概述:基于多粒度监督的图像语义物体协同标注
  5. 吴恩达神经网络和深度学习-学习笔记-33-为什么使用卷积
  6. LINQ表间关联执行分析
  7. 48 个 Linux 面试问题和答案
  8. Android PackageInstaller 静默安装的实现(附源码)
  9. 数据中台实战入门篇:双中台战略
  10. 为什么软件工程师的脾气都这么暴躁
  11. 【Android】关于WIFI局域网的手机摄像头当视频监控用实现方案详解
  12. MySQL批量创建测试数据脚本
  13. 时值年末,年度CSDN小峯的回顾过去的2020年(推荐)
  14. 人月神话中的神话(一)
  15. Ubuntu16.04 安装配置深度学习配置:GPU加速 Cuda+cudnn+opencv+caffe
  16. 机器学习模型1——线性回归和逻辑回归
  17. Windows11快速入门
  18. 硬件知识:如何快速挑选一款好的固态硬盘?
  19. oracle EM的安装配置
  20. 《财务自由之路》读后感

热门文章

  1. mysql 5.5 编译安装教程,Centos6下mysql 5.5.* 编译安装步骤详解
  2. java怎么获取中文首字母_Java如何获取中文拼音首字母的方法介绍
  3. ant编译无法依赖rt.jar
  4. php返回json数据函数实例
  5. C#从数据库导出数据到CSV
  6. 计算文档中不同单词出现的次数
  7. 全球地区资料json 含中英文 经纬度_2020年Brain Bee北京、天津、河北赛区地区赛参赛说明...
  8. 分布式选举协议:Raft
  9. MapReduce之InputFormat理解
  10. (73)多路选择器(二选一)