Bootstrap

数据结构的基础(集合框架算法,复杂度和泛型)

一.什么是集合框架

        Java 集合框架 Java Collection Framework ,又被称为容器 container ,是定义在 java.util 包下的一组接口 interfaces 和其实现类 classes
        其主要表现为将多个元素 element 置于一个单元中,用于对这些元素进行快速、便捷的存储 store 、检索 retrieve 、 管理 manipulate ,即平时我们俗称的增删查改 CRUD
        例如,一副扑克牌( 一组牌的集合 ) 、一个邮箱 ( 一组邮件的集合 ) 、一个通讯录 ( 一组姓名和电话的映射关系 ) 等等。
        
        上面的几行都是一些接口的继承关系,倒数第二行是类的名称,最后一行是两个栈。左上角也有详细说明。

二. 集合框架的重要性

        使用成熟的集合框架,有助于我们便捷、快速的写出高效、稳定的代码。

        学习背后的数据结构知识,有助于我们理解各个集合的优缺点及使用场景。

三.背后所涉及的数据结构以及算法

3.1 什么是数据结构

        数据结构(Data Structure) 是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

3.2 容器背后对应的数据结构

1. Collection 是一个接口,包含了大部分容器常用的一些方法
2. List 是一个接口,规范了 ArrayList LinkedList 中要实现的方法               
                ArrayList:实现了 List 接口,底层为动态类型顺序表
                LinkedList:实现了List 接口,底层为双向链表
3. Stack :底层是栈,栈是一种特殊的顺序表
4. Queue :底层是队列,队列是一种特殊的顺序表
5. Deque :是一个接口
6. Set :集合,是一个接口,里面放置的是 K 模型
                HashSet:底层为哈希桶,查询的时间复杂度为O(1)
 7. Map :映射,里面存储的是 K-V 模型的键值对

3.3 什么是算法

        算法(Algorithm): 就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

四.时间和空间复杂度

4.1 算法效率

        算法效率分析分为两种:第一种是时间效率,第二种是空间效率 时间效率被称为时间复杂度,而空间效率被称作 空间复杂度 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间 , 在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

4.2 时间复杂度

        4.2.1 概念
        时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个数学函数 ,它定量描述了该算法的运行时间。一个 算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我 们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算 法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
 
4.2.2 大O的渐进表示法

        Func1 执行的基本操作次数 :

        

        实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们 使用大 O 的渐进表示法。
O 符号( Big O notation ):是用于描述函数渐进行为的数学符号。
4.2.3 计算原则
1 、用常数 1 取代运行时间中的所有加法常数。
2 、在修改后的运行次数函数中,只保留最高阶项。
3 、如果最高阶项存在且不是 1 ,则去除与这个项目相乘的常数。得到的结果就是大 O 阶。
 所以上面的计算结果就是
 
        通过上面我们会发现大O 的渐进表示法 去掉了那些对结果影响不大的项 ,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数 ( 上界 )
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数 ( 下界 )
例如:在一个长度为 N 数组中搜索一个数据 x
最好情况: 1 次找到
最坏情况: N 次找到
平均情况: N/2 次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为 O(N)。
4.2.4 算法例子
例一

        先求执行次数的和,就是2N+10,根据上面的规则,最终答案就是O(N)。

例二:

计算次数就是M+N,根据计算规则结果就是O(M+N)。

例三

        执行了一百次,常数都看作是1,所以是O(1)。

例四

        要结合着算法思想,如图算法,可设array.length=n,第一次执行了n-1次,第二次执行了n-2次,.........,所以总和就是n-1+n-2+n-3+.......+2,等差数列求和公式算出总和,再根据算法规则,可得结果是O(N的平方)。

例五

        array数列必须是有序的,可以升序也可以降序。

在上述二分查找的代码中,每次循环都会将搜索范围缩小一半

假设数组的长度为 n

在第一次循环时,搜索范围是 n 个元素。

第二次循环时,搜索范围缩小为 n/2 个元素。

第三次循环时,搜索范围缩小为 n/4 个元素。

以此类推,直到找到目标元素或者确定目标元素不存在。

假设循环了 k 次找到了目标元素或者确定目标元素不存在,那么有 n / 2^k = 1,即 2^k = n,所以 k = log₂(n)

因此,二分查找的时间复杂度为 O(log₂(n)) 。这意味着随着数组长度的增加,二分查找的运行时间增长速度相对较慢,具有较好的性能。

例六
递归公式

        递归的时间复杂度=递归的次数 * 每次递归执行的次数

        

     

        上面都是符合2的n次方的规律的,只有最后一行不符合,但是这个计算本来也是粗略的,所以可以把最后一行也看作符合规律,不影响结果,所以就是2的0次方+2的1次方,一直加到2的n-1次方,等比数列求和得到是2的n次方-1,根据算法规则,等到O(2的n次方)。

        4.3 空间复杂度

4.3.1 概念
        空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少 bytes 的空 间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也 使用 O 渐进表示法
  4.3.2 例子
例一

        上述冒泡排序的代码在执行过程中没有额外开辟新的数组或其他数据结构来存储数据,它只是在原数组上通过交换元素的位置来进行排序操作。

因此,它开辟的额外空间为 O(1),即常量级别的空间。

例二

        如图,通过new关键字开辟了n+1个空间所以空间复杂度是O(n)。

例三

        实例3递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)。

五.包装类

   5.1 概念

        在Java 中,由于基本类型不是继承自 Object ,为了在泛型代码中可以支持基本类型, Java 给每个基本类型都对应了 一个包装类型。
    

5.2 基本数据类型和对应的包装类

5.3 拆箱和装箱操作

        装箱就是把数据放到包装类中,拆箱就是从包装类中拿出来。

5.4 自动装箱和拆箱

        直接让它等于,Java会帮我们进行装箱和拆箱的操作。

        思考一下会输出什么?

        原因就是

        这是因为在 Java 中,对于值在 -128 到 127 之间的 Integer 对象,通过自动装箱操作(如 Integer a = 127; )得到的对象会被缓存起来,当再次创建相同值的 Integer 对象时,会直接返回缓存中的对象,所以 a 和 b 指向同一个对象,a == b 为 true 。

        而对于值不在 -128 到 127 之间的 Integer 对象,每次装箱都会创建新的对象,所以 c 和 d 是两个不同的对象,c == d 为 false 。

        5.5 转换类型

        可以把int类型的a包装后,转换为double类型。

六 初识泛型

        6.1 什么是泛型

        泛型是 Java 等编程语言中的一个特性,它允许在定义类、接口和方法时使用类型参数,使得这些代码可以在多种数据类型上进行操作,同时在编译时进行类型检查,以增强代码的类型安全性和可读性。

        通过使用泛型,可以编写更通用、灵活且类型安全的代码。例如,在定义一个集合类(如 ArrayList )时,可以使用泛型来指定集合中元素的类型,这样在编译阶段就可以检查添加到集合中的元素类型是否正确,避免了运行时的类型转换错误。

        简单来说,泛型让代码能够处理不同类型的数据,同时保证类型的正确性,提高了代码的复用性和可维护性。

6.2 引出泛型

        我们要完成实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个 下标的值。

我们要怎么实现呢?

        这样可以,但是太乱了,还需要强制类型转换,否则会报错。

        我们之前学的int String数组都只能存放一些特定类型的值,无法存放任意类型的值,那么我们要怎么实现呢?接下来我们就得用到泛型这个概念了。

      6.3 泛型的语法

class 泛型类名称 < 类型形参列表 > {
// 这里可以使用类型参数
}
class ClassName < T1 , T2 , ..., Tn > {
}
class 泛型类名称 < 类型形参列表 > extends 继承类 /* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName < T1 , T2 , ..., Tn > extends ParentClass < T1 > {
// 可以只使用部分类型参数
}

 

        T是标识符的一种。

        Integer限制只能填正数,如果想填字符串,可以再建一个,换一下标识符内容即可。

        如下图所示

6.3.1 标识符

        

        6.4:泛型的擦除机制

        6.4.1 概念

                在 Java 中,泛型的擦除机制是指在编译过程中,将所有的泛型参数替换为特定的类型,这个类型通常是Object,或者如果有限定类型,则使用该限定类型。

        6.4.2 例子

        

        输出了类型加地址。

        我们希望我们的泛型也是这样。如下图:

        

        

        

        testdemo是我们的包,不用管,而后面它把Integer和String擦除了,在使用的时候也会把T擦除为Object。

6.5:泛型的上界

        

        加个上界Number。

        

        发现报错了,加了extends Number之后表示,只能写Number类或者是Number的子类,可知Integer是Number的子类,而String不是。

       6.6:泛型实现接口

   

        我们先实现一个功能,定义一个泛型找最大值。

        

为什么会报错呢?

        对于泛型类型 T ,不能直接使用小于号 < 进行比较。因为在编译阶段,泛型的具体类型是不确定的,编译器无法确定 T 类型是否支持 > 、 < 这样的操作。

        怎么改呢?

        现在实现了Comparable接口,所以可以使用CompareTo方法来比较。

        

如图进行了修改。

此时就可以了。

        为什么报错,因为Person没有实现Comparable接口。

        

        这样就可以了。

6.7 泛型方法

        如图所示,将泛型写到方法里面,也是可以的。

        不想实例化对象调用finmax方法的话,需要在public后面加一个static即可。

七.结束语

        感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。  感谢大家的一键三连。

;