一、复习部分
1. 数据结构:计算机如何存储数据的问题。ds关心的是如何高效的进行数据的读写。
2. 算法:在特定的数据集上,如何利用数据完成特定的功能。本质上是一系列运算的集合。
二、时间复杂度和空间复杂度
1. 作用:评价一个算法的好坏的两种维度
2. 时间复杂度:衡量一个算法的运行速度(理论值)
A. 利用大O表示法描述一个算法的基本操作的执行次数,就是时间复杂度
B. 大O表示法规则:当问题规模N趋于正无穷的时候,哪部分影响最大就保留
C. 时间复杂度求解:
a. 顺序执行的代码忽略
b. 计算循环或者递归的执行次数和n的关系
c. 如果有多层嵌套只需要关注最深层嵌套即可
D. 大O实际计算规则:
a. 用常数1来代替函数中所有的加法常数(只要是常数,常量,都是1,无论常量有多大,用1替代,哪怕常量1w,10w,统统都是1)因为常数和传入数值N变量毫无关系,此算法是一个稳定的时间效率算法,和变量无关
b. .只保留函数的最高阶项,所有的低阶省略不计
c. 若最高阶项存在,且系数不为1,统一都将最高阶的系数看做1。例如10*N^2 => N ^2
E. 例子:时间复杂度:O(N^2)
F. 一个算法的时间复杂度存在最高好,最坏,平均时间复杂度。平常衡量一个算法的时间复杂度一般看最坏时间复杂度(算法的上界)
a. 概念:
最坏(算法的上界):在最坏的情况下(执行次数最大),算法的时间复杂度
最好:在最好的情况下(执行次数最少),算法的时间复杂度
平均:所有可能输入等概率出现的时候,算法的平均时间复杂度
b. 例子: 顺序查找一个长度为N的数组中的一个数组x
最坏情况:数组查完还没找到或者x是数组最后一个元素。最好时间复杂度:O(N)
最好情况:第一次比对就找到了。最好时间复杂度:O(1)
平均情况:假设每一种输入的概率等可能为1/N. 则平均次数=1/N(1+2+3+...+N) = (N+1)/2 =O(N)
G. 几个算法的最坏时间复杂度分析
a. 例题1:时间复杂度为O(N+M)
解析:大O阶看的是问题规模变量的最高阶,若存在多个问题规模变量,都需要保留各个变量的最高阶。此时N和M都是问题规模变量,因此都需要保留最高阶,即为O(N+M)
一个小补充:图的邻接表的遍历的时间复杂度就是O(N+M),其中N是边长,M是顶点个数
b. 例题2:时间复杂度为O(1)
解析:这里的循环的执行次数为常量100和N无关,因此换算为大O阶为O(1)
c. 例题3:冒泡排序:时间复杂度 = 比较次数 + 交换次数
d. 例题4:时间复杂度为O(N),O(1000*N) = O(N)
解析:
e. 例题5:二分查找时间复杂度
注意1:小技巧1:
注意2:小技巧2:
增速快慢: 1 < log2(n) < n < n log2(n) < n^2 < n^3 < 2^n < n! < n^n
f. 例题六: 递归调用的时间复杂度: 将递归展开,看函数执行的次数
斐波那契数列为例
时间复杂度为 O(log2(N))。斐波那契数列的调用可以看作大致是一个完全二叉树,完全二叉树的高度如下,因此完全二叉树的节点个数为O(2^N), 因此时间复杂度为O(2^N)
g. 例题七:时间复杂度:O(N)
3. 空间复杂度:衡量一个算法的正常执行所需要的额外空间(不是需要解决的问题的数据集占用的空间,而是为了解决该问题新开辟的空间)大小
A. 空间复杂度 = 递归的调用的深度(递归执行次数) + 存储参数的空间(临时变量,形参等等)。同样使用大O表示法
补充:为什么递归要分析调用的深度
补充2:一个做题技巧
B. 空间复杂度规则(重中之重):
C. 例子
a. 例子1:O(1),因为临时变量只有i , j , flag。操作的数组是待排序的数据集
b. 例题2:空间复杂度:O(N)
解析:开辟了一个长度为N+1的数组,和几个变量,用大O表示法即为O(N)数量级
c. 例题3:递归函数的空间复杂度:O(N) + O(1) = O(N).前面是递归函数调用次数,后者是临时变量的开辟
四、泛型
1. 泛型的引出(java是一个强类型语言: 定义一个变量的时候,必须定义其类型)
A. 场景:要定义一个坐标类Point{
x;
y;
}
坐标的x和y两个属性可能有一下三种数据类型
x = 10, y = 20; //int类型
x = "东经108°",y = "北纬39°";//String类型
x = 10.3,y = 30.6;//double类型
B. 解决方案:Object是java中的最高参数统一化,能接收所有的引用类型;有了包装类的自动拆装箱后,Object还能接收基本数据类型(自动拆装箱)。即为Object可以接收所有数据类型。如下图
C. 但是用Object做接收存在一些问题,如下示例:由于用户的不规范输入,导致类型转换异常,出现异常之后,程序就退出了,其他用户无法正常使用坐标类,且这个问题发生在运行阶段,编译阶段正确执行
D. 结论:由于Object虽然可以接收所有类型,但是Object转为其他类型都需要进行强制类型转换,调用get方法的时候,需要根据具体类型,将Object强转为子类(隐藏的风险:类型转换异常,发生在运行阶段)
2. 上述结论的场景下,需要有一种新的机制,在定义类的时候,成员变量可以接收多种类型,但是在具体产生对象时明确类型,当有不同类型设置时,编译阶段就能发现错误,因此泛型应运而生
3. 泛型定义:JDK1.5后引入的泛型机制。所谓泛型,即在定义类或方法时,没有明确参数的类型,而是在使用该类的时候,明确类型。不需要进行类型强转,编译阶段就会在语法阶段检查类型是否匹配的机制。类型的编译截断守门员,不会让类型不匹配的问题进入到运行阶段
4. 泛型类的使用
(1)语法:泛型类的定义语法如下
注意:类型参数一般用大写的英文字母,实际上写什么都行
类名称<类型参数1,类型参数2...>{
类型参数1 成员变量名称//
类型参数2 成员变量名称//
...
}
(2)示例:一个类型参数的泛型类
A. 泛型类的定义
B. 泛型类的使用
(3)示例2:多个类型参数的泛型类(下面的例子中由于x和y的类型参数不同,因此在生成NewPoint对象的时候可以使得x和y的类型相同,也可以使得x和y的类型不相同)
p1是不同类型
p2是相同类型
5. 关于类型参数命名的说法
(1)T:一般指代任何类
(2)E:一般指代元素Element,也有用来指代异常Exception
(3)K和V一般搭配使用,描述一个键值对的对象,K表示key(不重复),V表示value(可以重复)
6. 泛型方法
(1)语法:
权限修饰符 <类型参数> 方法返回值类型 方法名称(){
}
(2)注意1:这两个根据上述语法规定都不是泛型方法
技巧(重中之重):看是不是泛型方法就看有没有尖括号
(3)示例
注意:泛型方法以自己的类型参数为准。也就是泛型方法中的T和泛型类中的T指代的不是一个。一般不会这么写,都是用不同的字母表示