一、11.10题目讲解
1. 例题1:
解析:答案为C,不会保留系数,大O表示法是当问题规模N趋近于正无穷的时候主要产生影响的部分,当N趋近于正无穷的时候,系数可以忽略不记,因此系数仅保留1
2. 例题2
解析:答案为D。
方法1:i * 2 * 2... = n。相当于n / 2 / 2 .....。满足n / 某个数一直当n = 1或0的条件,满足log n 级别的时间复杂度
方法2:假设最深层循环的某一语句执行次数为x的时候循环退出(此处这个语句为i = i * 2),则此时的i = 2^x。即为i > n 等价于 2 ^ x > n。即x = log2(n)
3. 例题3
解析:最深层循环的语句a[i][j] = i * j的执行次数即为时间复杂度,也就是O(n ^ 2)级别
4. 例题4
解析:本题目的代码实现思路是采用双引用法来实现。
(1)
left = 0; 指向数组最小值
right = arr.length - 1;指向数组最大值
(2)因为是比较sum和 a + b之间的接近程度,因此计算差值的绝对值,绝对值越小,则越接近
令ret = arr[left] + arr[right];dif = Math.abs(sum - ret) (注意:这里的Math.abs是求绝对值的)
当ret > sum,则说明大了,需要变小,left已经是最小了,因此right --
当ret < sum,则说明小了,需要变大,left ++
(3)重复步骤2,当本次的dif小于上次的dif,则将本次对应的left 和 right的值用变量a和b做接收。否则不做改动。
int a = left;
int b = right;
(4)当left > right的时候,遍历完成整个数组,此时a 和 b保存的就是对应所求
结论:回归原题,由于只需要一次循环就可以完成查找或者说最坏需要对比n次才能查找完成,因此时间复杂度为O(n)数量级。
注意:二分查找有序数组的前提是查找一个目标元素的时候使用
5. 例题5
解析:对于递归函数来说,时间复杂度等于递归函数执行的次数。在这里也就是n + 1次,也就是O(n)数量级
两种解题思路:
方法1:递归展开图,看一下节点的个数(也就是函数执行次数),求节点的个数就是函数的调用次数
方法2:代入一个实际的值来看一下
T(3) = T(2) + 3 = T(1) + 2 + 3 = T(0) + 1 + 2 + 3。因此执行次数为n + 1次
6. 例题6
解析:当问题规模N趋近于正无穷的时候,这个常数级别的二维数组对于空间复杂度的影响近乎没有,因此为O(1)
7. 例题7
解析:空间复杂度是为了解决问题而额外开辟的空间大小。在这里也就是二维数组。二维数组的行为n,列为从n到1递减。因此二维数组大小为1 + 2 + 3 + 4 + 5 + 6 +...+ n。也就是 n(1 + n) / 2。也就是O(N^2)数量级
8. 面试题1
面试题 17.04. 消失的数字 - 力扣(Leetcode)https://leetcode.cn/problems/missing-number-lcci/description/题目解析:输入的是一个长度为n的数组arr(也就是nums缺失整数后的数组),nums数组是一个长度为n + 1 的数组。
(1)解法1(时间复杂度为O(nlog)因为最快的排序算法就是这个数量级)
A. 思路:
a. 排序
b. arr从左到右依次遍历,若发现数组的元素值和下标值不相同,则说明缺失的值为该下标的值
c. 若遍历完成后都没有发现,则说明缺失的值是最后一个值,这个缺失值 = arr.length
B. 举例
C. 代码实现
(2)解法2:数学运算。时间复杂度为O(N)
A. 思路:nums数组的值之和 - arr数组的值之和即为消失的数字
B. 代码实现:
(3)解法3:映射
A. 思路:
a. 创建一个长度为n+1的数组hash来存储元素是否出现(或是元素出现的次数)。
b. 第一次遍历arr数组给新数组hash赋值,来建立映射关系
由于hash数组是从0~n+1的,而arr数组是打乱的随机的,因此循环中用hash[arr[i]] = true来赋值
c. 第二次遍历新数组,if(hash[i] == false),则说明是消失的数字。由于hash是n+1长度的,且一定存在消失的数字,因此不可能在循环中找不到
d. 在循环外return -1,因为消失的数字范围为0~n,这里表示万一出现错误导致在hash中找不到,就返回-1
B. 图文(实际上这个nums可以是打乱的,只要按照上述的方法赋值即可,左边是arr,右边是hash)
C. 代码
9. 面试题2
189. 轮转数组 - 力扣(Leetcode)jhttps://leetcode.cn/problems/rotate-array/(1)解法1:循环数组法
A. 思路:
a. 复制一个和原数组nums一模一样的新数组newNums来存储原数组的值,之后操作原数组nums的值并返回nums即可(为了防止值的覆盖)
b. 通过遍历,修改nums数组的值
循环中的逻辑部分:
当i + k <= num.length - 1的时候,可以直接移动,nums[i + k] = newNums[i]。这里其实也满足下列的取余操作,也就是i + k 没超过 nums.length等于其本身nums[i + k % nums.legnth] = newNums[i];
当i + k > num.lenth -1的时候,则需要进行取余操作,即nums[(i + k) % nums.length] = newNums[i]
总结:nums[(i + k) % nums.length] = newNums[i]
注意1:取模运算a % b的值是在[0,b)超过b的部分从0开始向右偏移
注意2:这里为什么不能用i + k - nums.length,当k 超过了数组长度,则会溢出。当然这里可以对k % nums.legnth来保证k一定是小于7的来解决这个问题。
注意3:比较标准的做法其实是复制一个新数组,操作新数组,最后一个循环写回
B. 图文逻辑
C. 代码
(2)解法2:反转数组法
A. 思路:
a. k可能会超过数组的长度,为了后续遍历数组时候不会数组越界访问,则对k取模nums.length ,保证k是小于nums.length的
b. 首先整个翻转nums(为了将后面的移动到前面去)
c . 然后分别翻转nums[0~k-1]和nums[k~nums.length -1]的子数组即可(反转后的子数组元素还原为原数组的顺序)
B. 图文逻辑
C. 代码
(3)解法3:求最大公约数,拓展了解
三、泛型机制原理了解
1. day1泛型复习部分
(1)泛型类的定义
A. 语法:
class 类名称<类型参数,一般用大写字母代替>{
}
B. 举例
class Point<T>{
T t;
}
(2)泛型类的使用:产生泛型类对象
Point<String> p1 = new Point<>();
Point<Integer> p2 = new Point<>();
(3)泛型方法的定义
A. 语法:
权限修饰符 <类型参数> 返回值类型 方法名称(参数列表)
B. 例子:
//第一个T是声明类型参数,第二个T是表示返回值类型也为T,第三个T表示形参t的类型也为T
public <T> T fun(T t){
return t;
}
C. 注意:当泛型类和泛型方法共存的时候,泛型方法始终以自己的泛型类型参数为准
2. 泛型原则:泛型只存在于编译阶段,运行阶段没有泛型(类型擦除)
(1)泛型只能用在成员域,不能用在静态域。即为static修饰的内容不能使用泛型
A.原理:静态域和类相关,泛型是必须产生对象才能进行类型擦除,也就是依附于对象。因此矛盾不能使用
B. 示例
a. 静态变量
b. 静态方法
(2)产生泛型对象的时候,具体的类型不能使用基本数据类型,要用基本数据类型的话使用其对应的包装类
A. 原理:基本数据类型不能直接被Object类接收,需要自动拆装箱。
B. 示例:
(3)不能直接创建和实例化泛型数组,要使用泛型数组,统一使用Object数组
A. 原理:new关键字是在运行阶段JVM开辟空间,但是泛型仅存在于编译阶段,也就是在执行的时候JVM不能识别E和T到底是什么类型的。
B. 示例:注意:不能new一个泛型数组
C. 正确定义和使用泛型数组:统一使用在泛型类中定义Object数组,然后在类中定义CURD(增删查改基本操作)的时候使用泛型类的类型参数作为CURD的形参或者返回值
a. 定义部分
b. 使用部分
注意:这里的add一定得赋值为Integer类型,因为values[size] = e,这一步操作是向上转型。为了保证get()中的强转操作一定不会失败,则这里一定要输入Integer类型。
3. 类型擦除:泛型的信息只存在编译阶段,在进入到JVM运行之前的class文件,与泛型有关的所有信息都会被编译器擦除掉,专业术语称为“类型擦除”。即,泛型类和普通类在进入JVM之后都一样,在进入JVM后没有泛型这个东西
证明1:
证明2:
结论: