前言:
🌈✨之前小怡给大家分享了JavaSE的知识,今天小怡要给大家分享一下数据结构基础知识。
一、初识集合框架
1.什么是集合框架
Java集合框架Java Collection Framework, 又称为容器container,是定义在Java.util 包下的一组接口interfaces和其实现类classes。
其主要表现为将多个元素elment置于一个单元中,用于对这些元素进行快速、便捷的存储store、检索retrieve、管理manipulate,即平时我们俗称的增删查改CRUD。
2.集合框架的重要性
- 使用成熟的集合框架,有助于我们便捷、快速的写出高效、稳定的代码;
- 学习背后的数据结构知识,有助于我们理解各个集合的优缺点及使用场景。
3.背后所涉及的数据结构以及算法
3.1 什么是数据结构
数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
3.2 相关Java知识
- 泛型Generic;
- 自动装箱autobox和自动拆箱autounbox;
- Object的equals方法;
- Comparable和Comparator接口。
3.3 什么是算法
算法:就是定义良好的计算过程,取一个或一组为输入,并产生出一个或一组值作为输出。简单来说,算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
二、时间和空间复杂度
1.算法效率
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称为空间复杂度。时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。
2.时间复杂度
2.1 时间复杂度的概念
在计算机科学中,算法的时间复杂度是一个数学函数,它定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
2.2 大O的渐进表示法
计算一下fun1基本操作执行了多少次:
void fun1 ( int N){
int count = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
count++;
}
}
for (int k = 0; k < 2 * N; k++) {
count++;
}
int M = 10;
while ((M--) > 0) {
count++;
}
System.out.println(count);
}
fun1执行的基本操作次数:
F(N)=N^2+2*N+10
实际上我们计算时间复杂度时,只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O符号:是用于描述函数渐进行为的数学符号。
2.3 推导大O阶方法
- 用常数1取代运行时间中的所有加法常数;
- 在修改后的运行次数函数中,只保留最高阶项;
- 如果最高阶项不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界);
平均情况:任意输入规模的最小运行次数;
最好情况:任意输入规模的最小运行次数(下界);
平时所有的时间复杂度/空间复杂度,都是指在最坏情况下。
2.4 常见的时间复杂度计算举例
【实例1】
基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为O(N)
【实例2】
基本执行力M+N次,有两个未知数M和N,时间复杂度为O(M+N)
【实例3】
基本操作执行了100次,通过推导大O阶方法,时间复杂度为O(1)
【实例4】
long factorial(int N){
return N<2?N:factorial(N-1)*N;
}
通过计算分析发现基本操作递归了N次,时间复杂度为O(N)
【实例5】
int factorial(int N){
return N<2?N:fibonacci(N-1)+fibonacci(N-2);
}
通过计算分析发现基本操作递归了2^N次,时间复杂度为O(2^N)
3.空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
【实例】
int[] fibonacci(int n){
long[] fibArray=new long[n+1];
fibArray[0]=0;
fibArray[1]=1;
for(int i=2;i<=n;i++){
fibArray[i]=fibArray[i-1]+fibArray[i-2];
}
return fibArray;
}
开辟了N个空间,空间复杂度为O(N)。
三、包装类&认识泛型
1.包装类
在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。
1.1 基本数据类型和对应的包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
1.2 装箱/装包和拆箱/拆包
装箱:把基本数据类型变为包装类类型的过程。
装箱操作:新建一个Integer类型对象,将的值放入对象的某个属性。
int a=10;
Integer i=Integer.valueOf(a);//显示装箱
Integer i2=10;//自动装箱,隐式装箱
拆箱:把包装类类型变为基本数据类型的过程。
拆箱操作:将Integer对象中的值取出,放到一个基本数据类型中。
Integer a=10;
int b=a;//自动拆箱
int c=a.intValue();//显示拆箱
2.泛型
一般的类和方法,只能使用具体的类型:要么是基本的类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚会很大。
泛型:就是适用于许多类型。从代码上讲,就是对类型实现了参数化。
泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去检查。
2.1 语法
class 泛型类名称<类型形参列表>{
//这里可以使用类型参数
}
class ClassName<T1,T2,……,Tn>{
}
class 泛型类名称<类型形参列表> extends 继承类{
//这里可以使用类型参数
}
class ClassName<T1,T2……,Tn> extends ParentClass<T1>{
//可以只使用部分类型参数
}
- E表示Element;
- K表示Key;
- V表示Value;
- N表示Number;
- T表示Type;
- S、U、V等等-第二、第三、第四个类型。
3.泛型类的使用
3.1 语法
泛型类 <类型实参>变量名;//定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参);//实例化一个泛型类对象
【实例】
MyArray<Integer>list=new MyArray<Integer>();
3.2 类型推导
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写。
MyArray<Integer>list=new MyArray<Integer>();
可以推导出实例化需要的类型实参为Integer。
4.泛型的上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
4.1 语法
class 泛型类名称<类型形参 extends 类型边界>{
}
【实例】
public class MyArray<E extends Number>{
}
只接受Number的子类作为E的类型实参。没有指定类型边界E,可以视为E extends Object。
【复杂实例】
public class MyArray<E extends Comparable<E>>{
}
E必须是实现类Comparable接口的。
5.泛型方法
5.1 定义语法
方法限定符<类型形参列表>返回值类型 方法名称(形参列表){
}
public static <E> void swap(E[] array,int i,int j){
E t=array[i];
array[i]=array[j];
array[j]=t;
}
🌈✨今天的分享到这里结束啦,小怡和大家一起分享一起进步一起学习,“成功的秘诀在于坚持自己的目标和信念”。