目录
前言:如何学好数据结构:
1.多画图
2.多思考
3.多写代码
前置知识:
1.泛型
2.包装类(包装类):
Java中集合类的背后就是数据结构,其类型又有很多,有些书把这些类也会叫集合框架,若要学习数据结构,就应先学习其对应的集合类,要学习集合类,就要先学习其对应的集合框架。
3.为什么会又这么多中数据结构:
要先了解什么是数据结构:数据结构 = 数据 + 结构 ==》描述或者组织一些数据
描述或者组织一些数据的方式不同,因而会有那么多的数据结构,每一个集合类,其描述和组织数据的方式都是不同的,在实际应用中,都会有不同的应用场景。
开始学习:
一 初识集合框架
1.什么是集合框架???
Java集合框架(Java Collection Framework),又被称为容器(container),是定义在java.util包下的一组接口interfaces和其实现类classes。
将多个元素放在一个单元中,对这些元素进行快速,便捷的存储,检索,管理,即增删改查CRUD。
下图是类和接口的一个总览图,这里先放在此处留个眼熟,后续会慢慢了解。

2.背后涉及的数据据结构以及算法
2.1什么是数据结构:
计算机存储,组织数据的方式,(是按顺序放,还是无序随便放,是按正序还是按照逆序,能不能重复放相同的数据等等等等),指数据元素之间相互存在一种或者多种特定的关系的集合。
2.2容器背后对应的数据结构:
此处对应于上面的总览图进行大致了解即可,后续会分别详细介绍并模拟实现:
1.Collection:是一个总的接口,包含了大部分容器常用的方法。
2.List:是一个接口,规范声明了ArrayList和LinkedList这两个实现类要实现的方法。
ArrayList:实现List接口,底层为动态类型顺序表。
LinkedList:实现了List接口,底层为双向链表。
3.Stack:底层是栈,栈是一种特殊的顺序表。
4.Queue:底层是队列,队列是一种特殊的顺序表。
5.Deque:是一个接口。
6.Set:集合,是一个接口,里面放置的是K模型。
HashSet:底层为哈希桶,查询的时间复杂度为O(1)
TreeSet:底层为红黑树,查询到时间复杂度为O(log2N),关于Key有序
7.Map:映射,里面存储的是K-V模型的键值对
HashMap:底层为哈希桶,查询的时间复杂度为O(1)
TreeMap:底层为红黑树,查询的时间复杂度为O(log2N),关于Key有序
2.3什么是算法:
定义一个良好的计算过程,取一个或一组值作为输入,产生一个或者一组作为输出。简而言之,算法就是一系列的计算过程,将输入转变为输出的计算过程。(类比于高中数学题,有些题目普通学生可能需要推理半天过程,而有些学霸,一个二级公式咣咣咣三行就得到了结果,学霸的二级公式,和普通学生十几行的推理过程,都是算法)
2.4如何学好数据结构和算法呢
1.死磕就行,奶奶滴,劳资当年数学题不会就算了,这b代码,实在不会,我就先全部死磕背会...

2.多画图,多动脑思考,把过程用纸和笔自己推演一下,慢慢的就会很清晰
3.刷题 + 复盘,牛客网力扣 and so on
二 时间和空间复杂度
引入:如何衡量一个算法的好坏呢?
在Javaee的初阶语法学习中,大家都已大致掌握了敲代码,那我们这种初学java敲出的代码和几十年程序猿敲的代码好坏如何评判呢?
例如:打印一个金字塔
我们直接
System.out.println("*");
System.out.println("**");
System.out.println("***");
System.out.println("*****");
.....
而大佬直接一个双层循环直接薄纱我们,虽然结果一样,但过程却明显我们的比较拉跨....
1.算法效率
算法效率分为两种:时间效率(时间复杂度) 空间效率(空间复杂度)。时间复杂度衡量的是算法的运行速度(同样大家跑的是五千米,我三分配跑完,小明七分配跑完,要举办运动会,班主任当然会选我去参加比赛) ,空间复杂度衡量的是一个算法所需要的额外空间。
(早期计算机发展时候,存储容量都很小,大家对其存储的大小容量还是十分在乎,如今动辄手机都512G,电脑固态硬盘1T,2T的容量使得现在大家不是很注意空间复杂度了)
2.时间复杂度
2.1时间复杂度的概念:
算法的时间复杂度是一个数学函数,定量了描述了算法的运行时间。(一个算法的运行时间,按理讲,应该是具体在某台机器上跑起来才可能知道,这种方法十分麻烦)所以,算法中的基本操作(在该算法中,被执行最多的操作)的执行次数,就是时间复杂度。
2.2用大O渐进法来表示时间复杂度
1.用常数1取代运行时间中所有的加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶的项存在且不是1,则去除这个最高阶的系数
这样三步之后,得到的结果就是大O阶(大O渐进法去掉了那些对结果影响不大的项)(在实际中一般关注算法的最坏运行情况)
2.3常见的时间复杂度计算举例
【实例1】
// 计算func2的时间复杂度?
void func2(int N) {
int count = 0;
for (int k = 0; k < 2 * N ; k++) {
count++;
}
int M = 10;
while ((M--) > 0) {
count++;
}
System.out.println(count);
}
解析:基本操作实行了2N + 10次,根据大O渐进法,时间复杂度为O(N) (如果不理解,请严格按照2.2中的三步走)
【实例2】
// 计算func3的时间复杂度?
void func3(int N, int M) {
int count = 0;
for (int k = 0; k < M; k++) {
count++;
}
for (int k = 0; k < N ; k++) {
count++;
}
System.out.println(count);
}
解析:基本操作执行了M + N次,时间复杂度为O(M + N)
【实例3】
// 计算func4的时间复杂度?
void func4(int N) {
int count = 0;
for (int k = 0; k < 100; k++) {
count++;
}
System.out.println(count);
}
解析:基本操作执行了100次,时间复杂度为O(1) (这里为什么是1而不是100?请看2.2三步走)
【实例4】
// 计算bubbleSort的时间复杂度?
void bubbleSort(int[] array) {
for (int end = array.length; end > 0; end--) {
boolean sorted = true;
for (int i = 1; i < end; i++) {
if (array[i - 1] > array[i]) {
Swap(array, i - 1, i);
sorted = false;
}
}
if (sorted == true) {
break;
}
}
}
解析:

这是我自己动手大概写了一下过程,希望大家可以看懂。
最好的情况,就是全部都是有序的,1,2,3,4,5,只需要完成for(int i = 1;i < end; i++)中的循环,N次,发现sorted 仍然等于false,跳出循环,只需要进行N次。
【实例5】
// 计算binarySearch的时间复杂度?
int binarySearch(int[] array, int value) {
int begin = 0;
int end = array.length - 1;
while (begin <= end) {
int mid = begin + ((end-begin) / 2);
if (array[mid] < value)
begin = mid + 1;
else if (array[mid] > value)
end = mid - 1;
else
return mid;
}
return -1;
}
解析:二分法

对时间复杂度的计算,不仅要看代码,还要结合其算法的思想
【实例6】
// 计算阶乘递归factorial的时间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
解析:递归算法,递归的复杂度 = 递归的次数 * 每次递归执行的次数

【实例7】
// 计算斐波那契递归fibonacci的时间复杂度?
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
解析:斐波那契递归

总结:
O(1) < O(log2N) < O(N) < O(log2N * N) < O(N^2)
3.空间复杂度
3.1空间复杂度的概念
一个算法在运行过程中临时占用存储空间大小的量度。注意:空间复杂度并不是程序占用了多少bytes的空间,而算的是变量的个数,也用大O渐进法表示。
3.2空间复杂度计算举例
因空间复杂度并不经常关注,此处仅举例三个,供大家了解。
【实例1】
// 计算bubbleSort的空间复杂度?
void bubbleSort(int[] array) {
for (int end = array.length; end > 0; end--) {
boolean sorted = true;
for (int i = 1; i < end; i++) {
if (array[i - 1] > array[i]) {
Swap(array, i - 1, i);
sorted = false;
}
}
if (sorted == true) {
break;
}
}
解析:仅仅是在每次进行交换的时候,使用了两个常数的额外空间,所以空间复杂度O(1)
【实例2】
// 计算fibonacci的空间复杂度?
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次就开辟了N个空间,所以空间复杂度为O(N)
【实例3】
// 计算阶乘递归Factorial的空间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N-1)*N;
}
解析:递归了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。O(N)
三 包装类&简单认识泛型
1 包装类
Java中,基本类型(int,byte,short,long,float...)并不是类,不继承自Object,为了在泛型代码中可以支持对应的基本类型,Java给每个基本类型都对应了一个包装类型。
1.1基本数据类型和对应的包装类

整理发现:除int --> Integer, char --> Character,其他基本数据类型的包装类均为首字母大写
1.2 装箱和拆箱
装箱:把一个基本数据类型 转化为 包装类型 的过程
分为自动装箱 和 显示装箱
int a = 10;
Integer b = a; //自动装箱
System.out.println(b);
Integer c = Integer.valueOf(a); //显示装箱
System.out.println(c);
拆箱:将包装类型 转化为 基本数据类型 的过程
Integer a = new Integer(10); //这段代码在JDK9版本之后会报错
Integer a = new Integer.valueOf(10); //JDK9版本之后推荐使用这行代码
int b = a;//自动拆箱
int c = a.intValue();//显示拆箱
int d = a.doubleValue();//显示拆箱,可以拆箱为指定的基本数据类型
System.out.println(b);
System.out.println(c);
System.out.println(d);

可以打开字节码文件看一下装箱和拆箱的底层实现
打开方法如下(IDE集成开发环境)

会跳转到下面的目录中

此处返回上一层目录,并点击进入out目录下

打开对应的工程目录,在目录处输入cmd回车

输入javap -c 空格 + 全路径
注意:这里的全路径是类的位置,如果在testDemo包下的类Test,路径是:testDemo.Test


可以发现,虽然我代码直接是Integer b = a;但底层实现仍然是Integer.valueOf方法。java为了减少开发者的负担,提供了自动装箱和拆箱的机制。
【阿里巴巴曾经的一道面试题】
请问我下面代码输出结果是什么?

运行结果:
为什么呢???让我们一点一点庖丁解牛式搞明白其中的奥秘。
首先,我们肯定知道,这一定使用了装箱的方法,将127这个int类型自动装箱成了Integer包装类。通过上文,我们知道,装箱的底层肯定是通过valueOf()方法进行的,可以在IDE中找到valueOf方法的实现(把鼠标移到valueOf处,按住ctrl键,单击valueOf即可跳转到其方法体处)

观察可知,valueOf方法,如果输入的int类型i是在IntegerCache.low 和 IntegerCache.high范围内,就会返回一个cache数组内的元素,否则就会返回一个new Integer。
那IntegerCache.low和IntegerCache.high范围是什么呢?

鼠标放在IntegerCache.low处显示其值为-128

按住Ctrl键后点high,进行跳转,可以发现high = h,h = 127
这样我们就知道了,如果输入的数字在-128 -- 127之间,就会返回cache数组内的元素
-128 -- 127 共有256个元素

如果输入的 i 等于-128,[i + (-Integer.low)] 下标为0,即是在cache数组下标为0的位置存放数据-128
如果输入的i等于127,[i + (-Integer.low)] 下标为255,即是在cache数组下标为255的位置存放数据127

如果输入的数字在这个范围内,则会直接在数组中拿出值,然后return回一个Integer包装类
如果不在这个范围内,则会return new Integer(i),new一个新的Integer,并返回。
知道了valueOf的实现逻辑,此时再回头看面试题,第一组是127,第二组是128

127在范围内,直接从数组中返回,所以a == b --> true
128不在范围内,new了新的Integer返回,所以 c == d --> false
怎么样,你理解了吗???
2.什么是泛型
一般的类和方法,只能使用其固定具体的类型,要么是基本类型,要么是自定义类型。如果要编写可以应用于多种类型的代码,这么刻板的限制会对代码的束缚很大。
泛型是在Jdk1.5中引入的语法,泛型:就是适用于许多许多类型。从代码角度讲,就是把类型实现了参数化。
3.引出泛型
实现一个类,类中有一个类型为Object的数组成员,使得数组中可以存放任何类型的数据,同样也可以根据成员方法返回数组中的某个下标的值。
以前数组的定义,都是固定的类型,例如String[], int[], double[],但是,可以想到,所有的类的父类都是Object,数组是否可以创建为Object???
实现如下:

的确可以实现,但发现有两个问题
1.任何类型的数据都可以存放(下面会解释)
2.下标为0的数据本身就是字符串,但如果不强制类型转化,会出现这样的报错,必须要进行强制类型转换
为什么任何类型的数据都可以存放是问题呢,我们的初衷不就是要创建一个可以存放任何类型数据的数组吗?
其实,更多情况下,我们只是希望他能够接收所有数据,但是只能够持有一种数据类型,而不是同时拥有这么多类型。所以,泛型的目的是:指定当前容器,要持有什么类型的对象,让编译器去检查。此处就要把类型当作参数,需要什么类型,就传入什么参数。
3.1泛型语法
class 泛型类名称<类型形参列表>{
这里可以使用类型参数
}
修改为泛型则为
如果此时在main方法中,仍然有代码
myArray2.setArray(1,10);//在下标为1的位置插入整型10
String str2 = myArray2.getArray(1);
System.out.println(str2);
会出现如下报错

这就是引入泛型的好处,利用<T>,使得,这个数组成为一个可以接收任何类型的数组,然后我们再在使用这个类的时候,传入类参数,指定具体的类型String,之后编译器便会替我们自动检查类型是否正确。
相关知识:
类名后的<T>代表占位符,表示当前类是一个泛型类
了解:【规范】类型形参一般使用一个大写字母表示,常用的名称有
E 表示 Element
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
4.泛型类的使用
4.1语法
泛型类<类型实参> 变量名; //定义一个泛型类引用
new 泛型类<泛型实参>(构造方法实参); //实例化一个泛型类对象
示例:
MyArray<String> list = new MyArray<String>(); //等号后面的<>中的String可省略,注意是<>中的可以省略,可不要全部省略为 new MyArray();
注意:泛型只能接收类,所有的基本数据类型必须使用包装类(这也是前面讲解包装类的原因)
4.2类型推导
编译器是可以根据上下文推导出类型实参的
MyArray<Integer> list2 = new MyArray<>(); //可以推导出实例化需要的类型实参为Integer
小结:
1.泛型是将类型参数化,进行传递
2.使用<T>表示当前类是一个泛型类
3.泛型优点:数据类型参数化,编译器自动进行类型检查和转换
5泛型是如何编译的???
5.1擦除机制

编译中,把所有的<T>替换为Object这种机制,就是擦除机制,这个机制较难理解,此处了解有这个机制即可。
6.泛型的上界
在定义泛型类的时候,有时需要对传入的类型变量做一定的约束
6.1语法
class 泛型类名称<类型形参 extends 类型边界> //此处的extends不是继承的意思了,可以理解为扩展
实例
public class MyArray<T extends Number> {
}
MyArray泛型只能接受Number本身及其Number的子类作为E的类型实参,换句话说就是,T只能是Number或者是Number的子类
MyArray<Integer> list1; //right, Integer是Number的子类
MyArray<String> list2; //err,String不是Number的子类
如果没有指定边界,则默认为extends Object
6.2使用
写一个类,在类里面实现findMax方法
大家肯定都能写出这样的代码,大体上是正确的,但需要注意的是arr[i] > max这行代码发生了报错,为什么呢?
Object类的元素是无法进行比较的,前面了解了擦除机制,将所有的T都擦除为了Object,在Object中是没有比较的方法的,所以无法比较。
此时就可以使用泛型的上界了

T extends Compara<T> 表示 T 一定是实现Comparable接口的
实现Comparable接口,使得其Object元素可以进行比较,此时更能理解到extends可以翻译为拓展的意思了吧!!!
此时即可正常输出最大值98
如果任意有其他类Z,并未实现Comparable接口,则会直接报错,编译器会自动检查,并且及时报错

此时实现Comparable接口之后,则不会报错,可以正常编译

补充:静态的泛型方法

注意静态的泛型方法,需要在static后用<>声明泛型类型的参数

629

被折叠的 条评论
为什么被折叠?



