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

本文详细介绍了数据结构中的树在排序、压缩存储、查找等方面的应用,包括堆排序、赫夫曼树和二叉排序树(BST)。堆排序通过构建大顶堆实现高效排序;赫夫曼树用于最小带权路径长度的构建;二叉排序树(BST)提供了快速的查找、插入和删除操作,但在极端情况下可能退化为链表。此外,文章还探讨了平衡二叉树(AVL树)的概念,通过旋转操作保持树的平衡,提高查询效率。
摘要由CSDN通过智能技术生成

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

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

十(补)树快速复习

在这里插入图片描述
主要是二叉排序树:

  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点可能会被打破
    因此采用左右旋来维持平衡性
    在这里插入图片描述
    首先,插入的节点必须是红色

选择跳表而不是红黑树:
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. 大顶堆举例说明
    在这里插入图片描述
  5. 小顶堆举例说明
    在这里插入图片描述
  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. .假设给定无序序列结构如下
    在这里插入图片描述
  2. .我们将序列变成一个大顶堆,其实就是从下往上、从右到左,将每个非终端结点(非叶结点)当做根结点,将其和子树调整成大顶堆。由完全二叉树的性质:
    在这里插入图片描述
  • 性质:二叉树的根结点从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互换位置完成这个根结点的大顶堆。
    在这里插入图片描述
  1. .找到第二个非叶节点,它的标号是【上一个非叶结点标号 - 1】 即【1-1=0】号,值为4,同理,由于[4,9,8]中左孩子 9 最大,根4 和 9 交换。
    在这里插入图片描述
  2. 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。
    在这里插入图片描述
    此时,我们就将一个无序序列构造成了一个大顶堆。
  • 步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
  1. .将堆顶元素 9 和末尾元素 4 进行交换
    在这里插入图片描述
  2. .重新调整结构,使其继续满足堆定义
    在这里插入图片描述
  3. .再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8.
    在这里插入图片描述
  4. 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
    在这里插入图片描述
    再简单总结下堆排序的基本思路:
    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个

红包金额最低5元

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

抵扣说明:

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

余额充值