09 树结构的实际运用【数据结构与算法学习笔记(Java)】

数据结构与算法(Java实现)

我的学习资料:
视频:尚硅谷Java数据结构与java算法(Java数据结构与算法)
书籍:《大话数据结构》
笔记中包括学习的内容,代码,同时自己总结了知识点速记(部分会带页内跳转,可点击跳转)供快速回顾和记忆学到的知识点。

十(补)树快速复习

09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第1张图片
主要是二叉排序树:

  1. 二叉排序树BST的产生主要是为了高效的查找,时间复杂度O (logn),只要中序遍历就可以从小到大输出
  2. 散列表的查找是常量级O1的,但是加上散列冲突,不一定比BST优秀
  3. Redis为什么要用跳表来实现有序集合而不用红黑树
    先说什么是跳表
    我们在顺序表排序后,可以利用元素的有序性,通过二分查找来实现查找的时间复杂度是logn的操作
    对于链表来说,即使元素是有序的,要想查找一个数据,也需要从头到尾去遍历,效率低是On
    跳表就是在单链表的基础上,将一些结点往上提,构成索引,在索引值之上,再拿出一些索引,层层构建,这样链表+多个索引层,就是跳表,它的查找的时间复杂度是O(logn),空间复杂度是O(n)
    跳表的插入:
    根据索引来定位,然后插入,插入后如果不动索引,就会因为底层链表塞太多,跳表性能退化成链表,所以需要一个随机函数来确定一个K,插入这个值,算出K,在1-K层索引中加入这个值做索引。随机函数的选取要考虑索引大小和数据大小的平衡性,使得其性能不能过度退化。
    跳表的删除:
    定位后删除,如果索引中有它,也要删除索引中的。

跳表维护平衡性:随机函数
红黑树(以及AVL树)维护平衡性:左右旋
二叉排序树(二叉查找树)的定义
左子树仅包含小于当前结点的值
右子树仅包含大于当前结点的值
左右子树每个也必须是二叉查找树
优点:
中序遍历即可从小到大输出,理想状态下增删查时间复杂度O(logn),
缺点:
极端情况下倾斜,退化为链表,查的复杂度变为O(n)

因此出现了
二叉平衡树(AVL)
任何节点的左右子树的高度差不大于1的二叉查找树,是一种高度平衡的二叉查找树
因为增删查的操作都与树的高度挂钩,因此二叉平衡树就是通过左右旋来保证左右子树的高度差不多,使得树的高度接近O(logn),防止性能的退化。(一棵及其平衡的二叉树的高度大约是log2n)

红黑树
二叉平衡树的一种,严格的二叉平衡树维护平衡的代价很高,很复杂,因此弱平衡的红黑树,或者说近似平衡经常被使用到
定义:

  1. 每个节点都是红或者黑
  2. 根必须是黑色
  3. 没有相邻的两个红色节点
  4. 对于每个节点,这个节点到达它能到达的叶子结点的所有路径,都包含相同数目的黑色节点
    在红黑树的创建过程中,3.4点可能会被打破
    因此采用左右旋来维持平衡性
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第2张图片
    首先,插入的节点必须是红色

选择跳表而不是红黑树:
5. 跳表的性能与红黑树近似,但是跳表代码实现更简单
6. 有序集合除了插入、删除、查询,还有一个范围查询的功能,由于跳表的索引可以很快的定位范围,在这个范围遍历输出即可,而红黑树的范围输出性能会弱一些

Hash散列表快速复习

通过哈希函数,将键映射到数组对应的位置中
hash冲突解决方式:开放寻址或者拉链法
开放寻址法中最简单是线性探测:一旦有冲突就往下找,直到找到空位为止(ThreadLocalMap就用的这个)
然后是二次探测,步长变长,+1 +2 变成+1的平方 +2的平方
双重散列:多订几个哈希函数,第一个有冲突就用第二个
拉链法:如果遇到冲突,就以链表的形式坠到hash桶后面
装载因子:填入表中的元素个数/散列表的长度
装载因子越大,空闲位置越少,冲突越多
打造工业级的散列表
装载因子过大了怎么办 -> 扩容
散列表扩容:容量变为2倍,数据进行搬移,一次性搬移延迟较高,可以分摊,扩容时不搬移旧数据,插入新数据时先插入新表,同时搬移一条旧数据,期间的查询,先在新表中查,查不到再去旧表。
选择冲突解决方法
开放寻址法:
优点:
4. 散列表中的数据都存在数组中,可以有效利用CPU加快查询速度
5. 序列化简单(链表中有指针,序列化不容易)
缺点:
6. 删除数据麻烦
7. 冲突的代价更高,装载因子上限不能太大,因此浪费内存空间
数据量小,装载因子小,用开放寻址法——Java的ThreadLocalMap使用开放寻址来解决散列冲突的原因

拉链法(链表法)
优点:
1.内存利用率高,不需要提前申请空间
2.对大装载因子的容忍度高
缺点:
1.额外耗费内存,要存储指针,对小对象来说比较消耗内存
2.链表节点零散分布在内存中,不连续,对CPU缓存不友好
大对象、大数据量的散列表,用拉链法,并且它支持更多的优化策略,比如用红核数代替链表
HashMap分析
默认大小:16
装载因子和动态扩容:0.75,当HashMap中元素个数超过0.75*capacity(capacity是散列表的容量)就会扩容,扩容为两倍的大小。
散列冲突解决方法:
JDK1.8之前:数组+链表
JDK1.8之后:数组+链表+红黑树
当链表长度大于8之后,就会变为红黑树,因为数据量小的时候,红黑树维护平衡性的策略是左右旋,比起链表,性能优势不明显
散列函数:

int hash(Object key) {
	int h = key.hashCode()return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}

其中,hashCode() 返回的是 Java 对象的 hash code。比如 String 类型的对象的 hashCode()就是下面这样:

public int hashCode() {
	int var1 = this.hash;
	if(var1 == 0 && this.value.length > 0) {
		char[] var2 = this.value;
		for(int var3 = 0; var3 < this.value.length; ++var3) {
			var1 = 31 * var1 + var2[var3];
		}
		this.hash = var1;
	}
	return var1;
}

因此设计一个工业级别的散列表需要考虑:

  • 支持查询、插入、删除工作
  • 合适的散列函数(使得值在散列中随机且均匀分布,还要兼顾性能,复杂了会耗时)
  • 合适的装载因子和动态扩容策略
  • 合适的散列冲突方法(小装载因子小数据量开放寻址、大数据量拉链法)

十、树结构的实际运用

10.1 堆排序

10.1.1 堆排序基本介绍

  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。
  2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
  3. 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
  4. 大顶堆举例说明
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第3张图片
  5. 小顶堆举例说明
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第4张图片
  6. 一般升序采用大顶堆降序采用小顶堆

10.1.2 堆排序基本思想

堆排序的基本思想是:

  1. 将待排序序列构造成一个大顶堆
  2. 此时,整个序列的最大值就是堆顶的根节点。
  3. 将其与末尾元素进行交换,此时末尾就为最大值。
  4. 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
    可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

10.1.3 堆排序步骤图解说明

要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。原始的数组 [4, 6, 8, 5, 9]

  1. .假设给定无序序列结构如下
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第5张图片
  2. .我们将序列变成一个大顶堆,其实就是从下往上、从右到左,将每个非终端结点(非叶结点)当做根结点,将其和子树调整成大顶堆。由完全二叉树的性质:
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第6张图片
  • 性质:二叉树的根结点从0开始编号,则i号结点的双亲编号为【(i-1)/2】,左孩子的编号为【2i+1】,右孩子的编号为【2i+2】。

从下到上,从右到左处理第一个非叶结点:

  • 首先,从下到上,从左到右的第一个非叶结点是最后一个结点的双亲。
  • 下标从0开始,一共有arr.length个元素,最后一个结点的编号是 i = arr.length-1,由性质,它的双亲编号是(i-1)/2=( arr.length-1-1)/2=arr.length/2-1。
  • 因此我们从最后一个非叶结点(标号arr.length/2-1=5/2-1=2-1=1号)开始从下往上、从右到左当做根结点进行调整,但是调整时是从所定的根结点开始从上到下来调整的,调整的范围是被改动过的范围
  • 1号结点的值是6,根据大顶堆的定义,大顶堆根结点值要大于等于它的孩子结点,因此将6与它的左右孩子5和9中较大者进行比较即可,9>5,因此根结点6与9比较,6<9,因此这个根树中6和9互换位置完成这个根结点的大顶堆。
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第7张图片
  1. .找到第二个非叶节点,它的标号是【上一个非叶结点标号 - 1】 即【1-1=0】号,值为4,同理,由于[4,9,8]中左孩子 9 最大,根4 和 9 交换。
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第8张图片
  2. 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第9张图片
    此时,我们就将一个无序序列构造成了一个大顶堆。
  • 步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
  1. .将堆顶元素 9 和末尾元素 4 进行交换
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第10张图片
  2. .重新调整结构,使其继续满足堆定义
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第11张图片
  3. .再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8.
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第12张图片
  4. 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第13张图片
    再简单总结下堆排序的基本思路:
    1).将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
    2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
    3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

10.1.4 堆排序代码

二叉树用顺序存储的数组实现,标号从0开始

  • 从右到左,从下到上取非叶结点为根将其调整为一个大顶堆(adjustHeap),【右→左,下→上】->【arr.length/2-1 递减→标号0】
    重复以下过程:
  • 顶端(标号0)与末尾j的元素交换,这些元素中最大值被沉到末尾“取出”,所以末尾向前定义一位【j- -】
  • 将剩余的元素继续调整为大顶堆
  • 直到末尾 j=0

循环调整为大顶堆(adjustHeap)的过程:

  • 指针i指向【要调整的位置】,开始是【根的位置】,将根的值存入【temp】
  • 循环调整开始:
  • 指针k指向值较大的左或右孩子结点(如果有左右孩子的话),因此k以【2*k+1】方式递增
  • 判断是否有右孩子,如果有,则将k指向值较大的孩子
  • 判断【temp】与孩子谁大:
    1. 孩子大,不符合大顶堆定义,需要调整,指针i指向【要调整的位置】,因此将孩子的值给要调整的位置 arr[i] =arr[k],孩子的位置发生了改变,需要以孩子为根向下重新调整,因此将孩子的位置赋值给要调整的位置 i=k,进入下一轮循环。
    1. temp大,即temp已经大于左右孩子,复合大顶堆的定义,无需调整,退出循环。
  • 循环过后:
  • i已经指向了需要调整的最终位置,将temp值放入位置i

调整为大顶堆的过程类似将最开始的根值temp备份视为第一个要调整的位置,然后从其孩子开始看,如果temp值比大孩子的值小,那么孩子的值先拽过去覆盖原来要调整的位置,同时这个孩子的位置变为下一个待调整位置,从孩子的孩子继续看,一直这样有大的就拽上来,最终一直到temp大于它的左右孩子结点,符合大顶堆的定义,那么就可以退出循环,将temp值放入上一次的待调整位置。

public class HeapSort {
    public static void main(String[] args) {
        int[] arr ={4, 6, 8, 5, 9};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void heapSort(int[] arr){
        //从右到左,从下到上的第一个非叶结点开始先调整为一个大顶堆
        for (int i=arr.length/2-1;i>=0;i--){
            adjustHeap(arr,i,arr.length-1);
        }

        //调整后将大顶堆的顶端与当前末尾交换位置,继续调整为大顶堆
        for(int j=arr.length-1;j>0;j--){
            int temp = arr[0];
            arr[0] = arr[j];
            arr[j] = temp;
            adjustHeap(arr,0,j-1);
        }
    }

    //将数组arr[],非叶结点标号i,元素最大标号length
    public static void adjustHeap(int[] arr,int i,int length){
        //i指向【要调整的位置】,开始是根结点的位置
        int temp = arr[i];//将根结点的值进行存储
        //从2*i+1是标号i的左孩子,k指向值较大的左孩子或者右孩子
        for (int k=2*i+1;k<=length;k=k*2+1){
            if (k+1<=length && arr[k+1]>arr[k]){
                k++;//如果右孩子大于左孩子,那么k指向右孩子
            }
            //如果左或右孩子的值大于根的值,不符合大顶堆,需要调整
            //将孩子值赋值给待调整位置的值,相当于我们对【以孩子为根的树】进行了调整,所以将i指向孩子的位置k
            //进行下一轮调整
            if (arr[k]>temp){
                arr[i]=arr[k];
                i=k;
            }else {
               break;//如果左或右孩子的值小于根的值,符合大顶堆,无需调整,退出循环
            }
        }
        //循环过后,i已经指向了最后的待调整位置,将temp放入
        arr[i]=temp;
    }
}

10.2 赫夫曼树

10.2.1 基本介绍

  1. 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树, 若该树的带权路径长度(wpl) 达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
  2. 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近

10.2.2 赫夫曼树几个重要概念和举例说明

  1. 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度,若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
  2. 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。 结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
  3. 树的带权路径长度(WPL):树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  4. WPL 最小的就是赫夫曼树
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第14张图片

10.2.3 赫夫曼树创建思路图解

10.2.4 赫夫曼树代码

public class HuffmanTreeTest {
    public static void main(String[] args) {
        int[] arr={ 13, 7, 8, 3, 29, 6, 1};
        TreeNode l1 = huffManTree(arr);
        l1.preOrder();
    }

    public static TreeNode huffManTree(int[] arr){

        //遍历arr数组,将arr中的每一个元素变成一个树结点
        List<TreeNode> treenodes = new ArrayList<TreeNode>();
        for (int i:arr
             ) {
            treenodes.add(new TreeNode(i));
        }

        //循环,每次取前两个最小的元素变成一个树结点
        while(treenodes.size()>1){
            //排序,从小到大
            Collections.sort(treenodes);

            //取出权值最小和第二小的节点作为左右结点
            TreeNode leftNode=treenodes.get(0);
            TreeNode rightNode= treenodes.get(1);

            //将左右结点的值相加作为新的结点
            TreeNode newNode = new TreeNode(leftNode.val+rightNode.val);
            //将左右结点放到新结点后面
            newNode.left=leftNode;
            newNode.right=rightNode;
            //从集合中删除刚刚的左右结点,并加入新结点进行下一轮排序
            treenodes.remove(leftNode);
            treenodes.remove(rightNode);
            treenodes.add(newNode);

        }
        return treenodes.get(0);
    }
}

class TreeNode implements Comparable<TreeNode> {

    int val;
    TreeNode left;
    TreeNode right;

    public TreeNode(int val) {
        this.val = val;
    }
    public void preOrder(){
        System.out.println(this);
        if (this.left!=null) this.left.preOrder();
        if (this.right!=null) this.right.preOrder();
    }

    @Override
    public String toString() {
        return "Node["+val+"]";
    }

    @Override
    public int compareTo(TreeNode o) {
        return this.val-o.val;
    }

}
  • 运行结果
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第15张图片

10.4 二叉排序树(也叫二叉查找树)(BST)Binary Search Tree

10.4.1 先看一个需求

给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加

10.4.2 解决方案分析

  • 使用数组
    数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢.
    数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位
    置后,后面的数据需整体移动,速度慢。
  • 使用链式存储-链表
    不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。[示意图]
  • 使用二叉排序树

10.4.3 二叉排序树介绍

二叉排序树BST (Binary Sort(Search) Tree):对于二叉排序树的 任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第16张图片

10.4.4 二叉排序树创建和遍历

一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为 :
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第17张图片

public class binarySortTreeTest {
    public static void main(String[] args) {
        int[] arr ={7, 3, 10, 12, 5, 1, 9};
        BSTNode t1 =new BSTNode(arr[0]);
        for (int i=1;i<arr.length;i++){
            t1.add(new BSTNode(arr[i]));
        }
        //二叉排序树的中序遍历
        t1.infixOrder();
    }


}

//二叉排序树结点类
class BSTNode{
    int val;
    BSTNode left;
    BSTNode right;

    public BSTNode(int val) {
        this.val = val;
    }

    @Override
    public String toString() {
        return "BSTNode{" +
                "val=" + val +
                '}';
    }
    //添加二叉排序树的结点
    public void add(BSTNode node){
        if (node==null) return;
        if (node.val<this.val){
            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 void infixOrder(){
        if (this.left!=null) this.left.infixOrder();
        System.out.println(this);
        if (this.right!=null) this.right.infixOrder();
    }
}

10.4.5 二叉排序树的删除

二叉排序树的删除情况比较复杂,有下面三种情况需要考虑

  1. 点 删除叶子节点 (比如:2, 5, 9, 12)
  2. 删除点 只有一颗子树的节点 (比如:1)
  3. 删除 有两颗子树的节点. (比如:7, 3,10 )
  4. 操作的思路分析
    第一种情况:
    删除叶子节点 (比如:2, 5, 9, 12)
    思路
    (1) 先去找到要删除的结点 targetNode
    (2) 找到 targetNode 的 父结点 parent
    (3) 确定 targetNode 是 parent 的左子结点 还是右子结点
    (4) 根据前面的情况来对应删除
    左子结点 parent.left = null
    右子结点 parent.right = null;

第二种情况: 删除只有一颗子树的节点 比如 1
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 确定 targetNode 的子结点是左子结点还是右子结点
(4) targetNode 是 parent 的左子结点还是右子结点
(5) 如果 targetNode 有左子结点
5. 1 如果 targetNode 是 parent 的左子结点
parent.left = targetNode.left;
5.2 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.left;
(6) 如果 targetNode 有右子结点
6.1 如果 targetNode 是 parent 的左子结点
parent.left = targetNode.right;
6.2 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right

情况三 : 删除有两颗子树的节点. (比如:7, 3,10 )
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 从 targetNode 的右子树找到最小的结点
(4) 用一个临时变量,将 最小结点的值保存 temp = 11
(5) 删除该最小结点
(6) targetNode.value = temp

public class binarySortTreeTest {
    public static void main(String[] args) {
        int[] arr ={7, 3, 10, 12, 5, 1, 9,2};
        BSTNode t1 =new BSTNode(arr[0]);
        for (int i=1;i<arr.length;i++){
            t1.add(new BSTNode(arr[i]));
        }
        //二叉排序树的中序遍历
        t1.infixOrder();
        //测试删除叶子结点
//        t1.deleteBSTNode(2);
//        t1.infixOrder();
        //测试删除一个子树的结点
//        t1.deleteBSTNode(1);
//        t1.infixOrder();
        //测试删除两个子树的结点
        t1.deleteBSTNode(7);
        t1.infixOrder();

    }


}

//二叉排序树结点类
class BSTNode{
    int val;
    BSTNode left;
    BSTNode right;

    public BSTNode(int val) {
        this.val = val;
    }

    @Override
    public String toString() {
        return "BSTNode{" +
                "val=" + val +
                '}';
    }
    //添加二叉排序树的结点
    public void add(BSTNode node){
        if (node==null) return;
        if (node.val<this.val){
            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 void infixOrder(){
        if(this==null) System.out.println("二叉树为空~~");
        if (this.left!=null) this.left.infixOrder();
        System.out.println(this);
        if (this.right!=null) this.right.infixOrder();
    }

    //查找目标结点
    public BSTNode searchTarget(int value){
        if (this==null) return null;
        if (this.val==value){
            return this;
        }else if(this.val>value){
            if (this.left!=null){
                return this.left.searchTarget(value);
            }else{
                return null;
            }
        }else{
            if (this.val<value){
                return this.right.searchTarget(value);
            }else {
                return null;
            }
        }

    }

    public BSTNode searchPerant(int value){
        if (this==null) return null;
        if ((this.left!=null&&this.left.val==value)||
                (this.right!=null&&this.right.val==value)){
            return this;
        }else{
            if (this.val>value && this.left!=null){
                return this.left.searchPerant(value);
            }else if(this.val<value && this.right!=null){
                return this.right.searchPerant(value);
            }else{
                return null;
            }
        }
    }

    //返回以node为根结点的二叉排序树的最小结点的值
    //同时删除以node为根结点的二叉排序树的最小结点
    public int delRightTreeMin(BSTNode node ){
        BSTNode target =node;
        while (target.left!=null){
            target =target.left;//二叉排序树的左孩子一定比自己小,这样一直循环找到最小
        }
        //此时target指向最小值结点,删除最小结点
        deleteBSTNode(target.val);
        return target.val;
    }

    public void deleteBSTNode(int value){
        if (this==null) {
            return;
        }else{
            //先去寻找要删除的结点 targetNode
            BSTNode targetNode = searchTarget(value);
            if (targetNode==null){
                return;
            }
            BSTNode parentNode = searchPerant(value);
            if (parentNode==null && targetNode.left==null && targetNode.right==null){
                targetNode=null;//如果目标结点存在,目标结点的父结点和左右孩子都不存在,证明是只有一个结点的树,将它删除即可
            }
            //第一种情况:如果目标结点没有左右孩子,证明是叶子结点
            if (targetNode.right==null && targetNode.left==null){
                //判断target是parent的左孩子还是右孩子
                if (parentNode.left==targetNode){
                    parentNode.left =null;
                }else {
                    parentNode.right =null;
                }

            }else if(targetNode.right!=null && targetNode.left!=null){//第二种情况:目标结点有两棵子树
                //调用方法:delRightTreeMin,删除目标结点的最小值结点,同时将最小值结点的值赋值给目标结点
                int minVal = delRightTreeMin(targetNode.right);
                targetNode.val=minVal;
            }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;
                        }
                    }

            }
        }

    }
}

10.5 平衡二叉树(AVL)

10.5.1看一个案例(说明二叉排序树可能的问题)

给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.

  • 左边 BST 存在的问题分析:
  1. 左子树全部为空,从形式上看,更像一个单链表.
  2. 插入速度没有影响
  3. 查询速度明显降低(因为需要依次比较), 不能发挥 BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
  4. 解决方案-平衡二叉树(AVL)

10.5.2 基本介绍

  1. 平衡二叉树也叫平衡 二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树(前提是它必须是二叉排序树!,AVL是在二叉排序树上实现的), 可以保证查询效率较高,它是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。
  2. 具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
  3. 举例说明, 看看下面哪些 AVL 树, 为什么?
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第18张图片
    1的根左子树高度为2,右子树高度为1,高度差为1,是AVL
    2的根左子树高度为2,右子树高度为2,高度差为0,是AVL
    3的根左子树高度为3,右子树高度为1,高度差为2,不是AVL
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第19张图片
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第20张图片
    我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF (Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是一1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。**距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。**图8-7-3,当新插入结点37时,距离它最近的平衡因子绝对值超过1的结点是58 (即它的左子树高度2减去右子树高度0),所以从58开始以下的子树为最小不平衡子树。

09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第21张图片

10.5.3 平衡二叉树实现原理

平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
为了能在讲解算法时轻松-一些,我们先讲一个平衡二叉树构建过程的例子。假设我们现在有一一个数组 a[10]={3,2,1,4,5,6,7,10,9,8}需要构建 二叉排序树。在没有学习平衡二叉树之前,根据二叉排序树的特性,我们通常会将它构建成如图8-7-4的图1所示的样子。虽然它完全符合二叉排序树的定义,但是对这样高度达到8的二叉树来说,查找是非常不利的。我们更期望能构建成如图8-7-4的图2的样子,高度为4的二叉排序树才可以提供高效的查找效率。那么现在我们就来研究如何将一个数组构建出图2的树结构。

09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第22张图片
对于数组a[10]={3,2,1,4,5,6,7,10,9,8}的前两位3和2,我们很正常地构建,到了第3个数“1”时,发现此时根结点“3” 的平衡因子变成了2,此时整棵树都成了最小不平衡子树,因此需要调整,如图8-7-5 的图1 (结点左上角数字为平衡因子BF值)。因为BF值为正,因此我们将整个树进行右旋(顺时针旋转),此时结点2成了根结点,3成了2的右孩子,这样三个结点的BF值均为0,非常的平衡,如图8-7-5 的图2所示。
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第23张图片
然后我们再增加结点4,平衡因子没发生改变,如图3。
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第24张图片

增加结点5时,结点3的BF值为-2,说明要旋转了。由于BF是负值,所以我们对这棵最小平衡子树进行左旋(逆时针旋转),如图4,此时我们整个树又达到了平衡。
继续,增加结点6时,发现根结点2的BF值变成了-2,如图8-7-6的图6。所以我们对根结点进行了左旋,注意此时本来结点3是4的左孩子,由于旋转后需要满足二叉排序树特性,因此它成了结点2的右孩子,如图7。增加结点7,同样的左旋转,使得整棵树达到平衡,如图8和图9所示。
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第25张图片
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第26张图片
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第27张图片
平衡因子>1,右旋转
平衡因子<-1,左旋转

10.5.4 应用案例-单旋转(左旋转)

  1. 要求: 给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}
  2. 思路分析(示意图)
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第28张图片
    按照BST的创建方法,根的左边子树高度1,右边高度为3,平衡因子BF为-2,不是一个AVL,小于-1,需要左旋转
  • 代码实现
    首先需要一个方法:计算以当前结点为根结点的树的高度
    //获取当前结点的高度,以该结点为根结点的树的高度
    public int height(){
        return Math.max(left==null?0:left.height(),right==null?0:right.height())+1;
    }

然后计算左右子树的高度:

    //获取左子树的高度
    public int getLeftHeight(){
        if (left==null) return 0;
        return left.height();
    }

    //获取右子树的高度
    public int getRightHeight(){
        if (right==null) return 0;
        return right.height();
    }

编写左旋的方法:

   // P 当前结点 PL 当前结点的左子树
   //左旋转方法
    //    P               newNode=P     P            newNode=P    R(P变为R)                        R(P变为R)
    //   / \                / \          \             / \         \                                 /    \
    //  PL  R     ->       PL  RL         R    ->    PL  RL         \    ×R(被孤立的样子) ---->  newNode=P   RR
    //     / \                            \                          \                             / \      \
    //    RL  RR                           RR                         RR                          PL  RL    N
    //          \                           \N                         \N                            (恢复平衡)
    //          N(插入破坏了平衡)
    //新建一个与当前结点值相同的结点 newNode

    public void leftRotate(){
        //以当前结点的值创建一个新结点
        AVLTreeNode newNode =new AVLTreeNode(this.val);
        //新结点的左子树是当前结点的左子树
        newNode.left=this.left;
        //新结点的右子树,是当前结点右子树的左子树
        newNode.right = this.right.left;
        //当前结点的值变为它右孩子的值
        this.val=this.right.val;
        //当前结点的右子树变为它右孩子的右子树
        this.right=this.right.right;
        //当前结点的左子树变为新结点
        this.left = newNode;
    }

未进行左旋的树:
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第29张图片

在node类的add方法中加入判断需要左旋的代码:

        //如果平衡因子<-1,需要左旋
        if(this.getLeftHeight()-this.getRightHeight()<-1){
            leftRotate();
        }

这样的左旋判断其实有问题,先这样写,然后测试:

    public static void main(String[] args) {
        int[] arr = {4, 3, 6, 5, 7, 8};
        AVLTreeNode t1 = new AVLTreeNode(arr[0]);
        for (int i = 1; i < arr.length; i++) {
            t1.add(new AVLTreeNode(arr[i]));
        }
        //中序遍历
        t1.infixOrder();
        System.out.println("树的高度为"+t1.height());
        System.out.println("左子树的高度为"+t1.getLeftHeight());
        System.out.println("右子树的高度为"+t1.getRightHeight());

    }
  • 左旋测试结果
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第30张图片
    变为了一颗AVL树。

10.5.5 应用案例-单旋转(右旋转)

  1. 要求: 给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}
  2. 思路分析(示意图)
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第31张图片
  • 代码实现
    右旋转与左旋转的思想几乎一样,有点对称的感觉
    //如果bf值大于1,进行右旋转
    //相当于要往左拎一个,增加右子树的高度
    public void rightRotate(){
        //以当前结点的值创建一个新结点
        AVLTreeNode newNode =new AVLTreeNode(this.val);
        //新结点的右子树是当前结点的右子树
        newNode.right = this.right;
        //新结点的左子树是当前结点左子树的右子树
        newNode.left =this.left.right;
        //当前结点的值变为左孩子的值
        this.val=this.left.val;
        //当前结点的左子树变为它左孩子的左子树
        this.left =this.left.left;
        //当前结点的右子树变为新结点
        this.right =newNode;
    }

未进行右旋的树:
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第32张图片

在node类的add方法中加入判断需要右旋的代码,这样的右旋判断其实有问题,先这样写,

        //如果平衡因子>1,需要右旋
        if(this.getLeftHeight()-this.getRightHeight()>1){
            rightRotate();
        }

加入右旋后测试右旋转是否正确:

    public static void main(String[] args) {
//        int[] arr = {4,3,6,5,7,8};
        int[] arr = {10,12, 8, 9, 7, 6};
        AVLTreeNode t1 = new AVLTreeNode(arr[0]);
        for (int i = 1; i < arr.length; i++) {
            t1.add(new AVLTreeNode(arr[i]));
        }
        //中序遍历
        t1.infixOrder();
        System.out.println("树的高度为"+t1.height());
        System.out.println("左子树的高度为"+t1.getLeftHeight());
        System.out.println("右子树的高度为"+t1.getRightHeight());

    }
  • 测试结果
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第33张图片
    变为了一棵AVL树。

10.5.6 应用案例-双旋转

前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转
不能完成平衡二叉树的转换。比如数列
int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL 树.
int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL 树
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第34张图片

  1. 问题分析
    09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第35张图片
  2. 解决思路分析
    当符号右旋转的条件时
  1. 如果它的【左子树的右子树】>【左子树的左子树】高度
  2. 先对当前这个结点【左结点左旋】
  3. 再对当前结点进行【右旋】的操作即可

需要左旋时同理对称:

  1. 符合左旋的条件,如果【右子树的左子树】高度>【右子树的右子树】高度
  2. 先对这个结点的【右结点右旋】
  3. 再对当前结点进行【左旋】操作
  • 在判断左右旋的条件中加入条件:
       //如果平衡因子<-1,需要左旋
        if(this.getLeftHeight()-this.getRightHeight()<-1){
            if (this.right!=null &&this.right.getLeftHeight()>this.right.getRightHeight()){
                this.right.rightRotate();
                this.leftRotate();
            }else{
                this.leftRotate();
            }
            return;
        }

        //如果平衡因子>1,需要右旋
        if(this.getLeftHeight()-this.getRightHeight()>1){
            if (this.left!=null && this.left.getRightHeight()>this.left.getLeftHeight()){
                this.left.leftRotate();
                this.rightRotate();
            }else{
                this.rightRotate();
            }
            return;
        }

进行测试:

    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};
        //       10                  10                    8
        //       /\     左结点        /\     当前结点      /  \
        //      7  11   ---->     8    11   ---->       7     10
        //     / \       左旋    / \         右旋       /      / \
        //    6   8           7    9                  6      9   11
        //         \         /
        //          9      6
        AVLTreeNode t1 = new AVLTreeNode(arr[0]);
        for (int i = 1; i < arr.length; i++) {
            t1.add(new AVLTreeNode(arr[i]));
        }
        //中序遍历
        t1.infixOrder();
        System.out.println("树的高度为"+t1.height());
        System.out.println("左子树的高度为"+t1.getLeftHeight());
        System.out.println("右子树的高度为"+t1.getRightHeight());

    }

测试结果
09 树结构的实际运用【数据结构与算法学习笔记(Java)】_第36张图片

你可能感兴趣的:(数据结构与算法学习笔记,数据结构,二叉树,java)