复杂度分析
算法决定了程序的性能,性能可以从完成同一项任务所使用的时间的长短、占用内存的大小两个方面去考量
《计算机程序设计艺术》的作者高德纳(Donald Knuth)提出了一种方法,这种方法的核心思想很简单,就是一个程序的运行时间主要和两个因素有关:
- 执行每条语句的耗时。
- 执行每条语句的频率。
前者取决于硬件,后者取决于算法本身和程序的输入。
大O表示法
参数n表示数据的规模,这里的O表示量级(order),比如说“二分查找的时间复杂度是O(logn)”,表示它需要“通过logn量级的操作去查找一个规模为n的数据结构”
- 在采用大O表示法表示复杂度时,可以忽略系数,这也是我们可以忽略不同性能的计算机执行时间差异的原因
迭代复杂度分析
1.常数阶,O(1)
不存在循环语句、递归语句
- 赋值语句
2.多项式,O(n)、O(n2)、O(n3)
-
关注循环执行次数最多的那一段代码
-
例子:
如果是一层n的循环,那么时间复杂度就是O(n);如果嵌套了两层n的循环,那么时间复杂度就是O(n2)
3.对数阶 O(logn)和O(nlogn)
- 多见于二分查找和一些排序算法
二分查找的时间复杂度通常是O(logn),一些基于比较的排序算法的时间复杂度可以达到O(nlogn),典型的有快速排序、归并排序
4.指数阶 O(2^n)
指数的增长非常恐怖,一个指数阶的算法通常是不可用的,或者存在优化的空间。
- 例子 斐波拉契数列 f(n)=f(n-1)+f(n-2) 在回溯的过程总存在重复的计算,可以通过存储的方式进行优化
5.阶乘复杂度O(n!)
旅行商问题(Travelling Salesman Problem,TSP)是著名的NP问题,暴力的解法可以枚举点的排列,这种算法的时间复杂度是O(n!)。另外一个比较典型的问题是全排列。
-
全排列
复杂度的计算=n*(n-1)(*n-2)……
递归算法复杂度分析
递归算法采用的是分治的思想,把一个大问题分解为若干个相似的子问题求解。识别子问题之间的递归公式是进行递归复杂度分析的前提和根本。这里介绍两种方法来分析递归的时间复杂度,分别是递归树法和代入法。
1.递归树法
递归树分析是一种将递归过程描述成树结构的方法。初始树只有一个节点,随着每一次递归,树的深度加1。每一层的节点数取决于具体的场景。
在得到递归树后,将树每层中的节点求和,然后将所有层的节点求和,就大致可以得出树的总节点数n了。而整个算法的基本操作数等于树上每一个节点的基本操作数t乘以树的总节点个数n。
复杂度计算=nx2xlog(n)
2.代入法
T(n)=T(n-1)+T(0)指的是规模为n的问题,可以转化为规模为n-1的子问题和一个常数的操作。
T(n)=T(n-1)+T(0)
=T(n-2)+T(0)+T(0)
=T(n-3)+T(0)+T(0)+T(0)
=T(0)+…+T(0)+T(0)+T(0)
=n×T(0)
可以得出其时间复杂度为O(n)。
总结
-
掌握某种算法的复杂度规律后,有时根据题目的时间复杂度要求,就可以猜测出可能用到的算法,比如算法要求时间复杂度为O(logn),那么就有可能是二分法
-
有关对数阶
-
对数阶
int count = 1;
while(count < n)
{count = count*2;
//时间复杂度O(1)的程序步骤序列
…}
-
线性对数阶
for(int i=0; j<n; i++){ j=1; while(j < n){ j = j*2; } }
对数阶的计算重复n次
-
数学之美
2.1 两数之和[1]
给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回它们的索引值。你可以假设每种输入只会对应一个答案,但是,你不能重复利用这个数组中同样的元素。示例
给定 nums=[2,7,11,15],target=9
因为 nums[0]+nums[1]=2+7=9,所以返回[0,1]
- 空间换时间
可以通过一个辅助的哈希表来降低时间复杂度,具体思路是:对数组进行遍历,遍历每一项时都判断target-nums[i](其中i是当前数组项的索引值)是否在之前遍历中遇到过,如果是,则直接返回;如果不是,则将其放在哈希表中,然后继续遍历下一项。
-
优点
这种算法对于这种需要返回索引的场景非常有效。比如题目要求返回的两个索引值按照在原数组中出现的先后顺序返回;又或者题目要求按照元素值的大小返回,我们也只需要拿出来比较一下,返回即可。
2.2 三数之和[15]
给定一个包含n个整数的数组nums,判断nums中是否存在3个元素a、b、c,使a+b+c=0。找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。
例如,给定数组 nums=[-1,0,1,2,-1,-4],满足要求的三元组集合为[[-1,0,1],[-1,-1,2]]。
-
该题重点在于去除重复的项
-
相当于双指针
class Solution { public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> res = new ArrayList<>(); int n = nums.length; if (nums.length <= 2 || nums == null) return res; // 进行排序 Arrays.sort(nums); // 从第一个开始 for (int i = 0; i < n - 2; i++) { if (nums[i] > 0) break; // 当后面一个和前面一个是一样时,跳过这个情况 if (i > 0 && nums[i] == nums[i - 1]) continue; int target = -nums[i]; int left = i + 1; int right = n - 1; while (left < right) { // 进行双指针搜索 if ((nums[left] + nums[right]) < target) { left++; } else if ((nums[left] + nums[right]) > target) { right--; } else { ArrayList<Integer> temp = new ArrayList<>(); temp.add(nums[i]); temp.add(nums[left]); temp.add(nums[right]); res.add(temp); //重复项的去除 事实上只遍历了一遍数组 [-2, -1, -1, -1, 3, 3, 3] while (left < right && nums[left] == nums[left + 1]) left ++; while (left < right && nums[right] == nums[right - 1]) right --; left++; right--; } } } return res; } }
复杂度分析
- 时间复杂度:上述代码for循环内部虽然有两个while循环,但是这两个while循环也仅仅会扫描一遍数组(最里面的while循环只是跳过一些重复的元素而已),因此总的时间复杂度仍然是O(n2),而不是O(n3),其中n为数组长度。
- 空间复杂度:不确定,取决于内部排序算法的具体实现。
2.3 四数之和[18]
给定一个包含n个整数的数组nums和一个目标值target,判断nums中是否存在4个元素a、b、c、d,使a+b+c+d的值与target相等?找出所有满足条件且不重复的四元组。注意:答案中不可以包含重复的四元组。示例
给定数组nums=[1,0,-1,0,-2,2]和target=0。
满足要求的四元组集合为[[-1,0,0,1],[-2,-1,1,2],[-2,0,0,2]]。
-
回溯法
回溯法的基本思路是,首先固定1个元素,然后固定第2元素……,直到全部元素(在这里是4个元素)确定下来,判断是否满足要求(在这里是不重复且和为target)。如果满足要求,则将其加入结果集;如果不满足要求,则回退一步走其他分支。这种每次面临多个选择,选择其中一个走到头,之后回退到选择点继续其他选择的方法,被称为回溯法。
复杂度分析
- 时间复杂度:时间复杂度取决于组合数,由排列组合原理可知,组合数共有n(n-1)(n-2)(n-3)个,因此时间复杂度为O(n4)个,其中n为数组长度。
- 空间复杂度:由于使用了hashmap来存储所有访问过的组合,因此空间复杂度为O(n4),其中n为数组长度。
-
分治法
将问题进行分解,将子问题求解后进行合并
可以先将四数和four_sum分解为两数和,即twoSum(a,threeSum(A)),其中a是数组中的任意数,A是除a之外的其他数的集合;接下来继续对三数和threeSum进行分解,将其分解为twoSum(b,twoSum(B))。
2.4 四数之和Ⅱ[454]
给定4个包含整数的数组列表A、B、C、D,计算有多少个元组(i,j,k,l)能使A[i]+B[j]+C[k]+D[l]=0。
为了使问题简单化,所有的A、B、C、D具有相同的长度n,且0≤n≤500。所有整数的范围在-228到228-1之间,最终结果不会超过231-1。示例
输入:A=[1,2],B=[-2,-1],C=[-1,2],D=[0,2]
输出:2
解释:
两个元组如下。
● (0,0,0,1)→A[0]+B[0]+C[0]+D[1]=1+(-2)+(-1)+2=0
● (1,1,0,0)→A[1]+B[1]+C[0]+D[0]=2+(-1)+(-1)+0=0
这是四数之和的第2个版本。这道题不再是1个数组,而是4个数组,并在每个数组中挑选一个数,使其相加等于0。
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
HashMap<Integer, Integer> mapper = new HashMap<>();
for (int i : nums1) {
for (int j : nums2) {
//检查这个是否存在
mapper.merge(i + j, 1, Integer::sum);
}
}
int result = 0;
for (int i : nums3) {
for (int j : nums4) {
if (mapper.get(-(i + j)) != null)
result += mapper.get(-(i + j));
}
}
return result;
}
}
-
两两组合进行结果的保存,去第二批当中寻找第一批当中的相反数
-
学习新的HashMap的API——merge
hashmap.merge(key, value, remappingFunction)
**注:**hashmap 是 HashMap 类的一个对象。
参数说明:
- key - 键
- value - 值
- remappingFunction - 重新映射函数,用于重新计算值
返回值
如果 key 对应的 value 不存在,则返回该 value 值,如果存在,则返回通过 remappingFunction 重新计算后的值。
使用
int returnedValue = prices.merge("Shirt", 100, (oldValue, newValue) -> oldValue + newValue); // String returnedValue = countries.merge("Washington", "USA", (oldValue, newValue) -> oldValue + "/" + newValue);
2.5 最接近的三数之和[16]
给定一个包括n个整数的数组nums和一个目标值target。找出nums中的3个整数,使它们的和与target最接近。返回这3个数的和。假定每组输入只存在唯一答案。
例如,给定数组nums=[-1,2,1,-4]和target=1。
与target最接近的3个数的和为2,即-1+2+1=2。
class Solution {
public int threeSumClosest(int[] nums, int target) {
//容错检验
if (nums.length < 3) return -1;
int n = nums.length;
//进行排序
Arrays.sort(nums);
//初始化
int result = nums[0] + nums[1] + nums[2];
for (int i = 0; i < n - 2; i++) {
//去除重复定向
if (i > 0 && nums[i] == nums[i - 1]) continue;
//重置左右指针
int l = i + 1;
int r = n - 1;
//开始根据这个定向,进行收缩
while (l < r) {
//得到当前的值
int sum = nums[i] + nums[l] + nums[r];
//如果这个值相等就返回这个
if (sum == target) return sum;
//置换小的那个值
if (Math.abs(target - sum) < Math.abs(target - result)) result = sum;
//根据大小进行移动
if (sum < target) {
l += 1;
} else {
r -= 1;
}
}
}
return result;
}
}
N数之和
2.6 最大子序列和[53]
求数组中最大连续子序列和,例如,给定数组A=[1,3,-2,4,-5],则最大连续子序列和为6,即1+3+(-2)+4=6。
首先明确一下题意。
● 题目说的子数组是连续的。
● 题目只需要求和,不需要返回子数组的具体位置。
● 数组中的元素是整数,可能是正数、负数和0。
● 子序列的最小长度为1。比如:
● 对于数组[1,-2,3,5,-3,2],应该返回3+5=8。
● 对于数组[0,-2,3,5,-1,2],应该返回3+5+(-1)+2=9。
● 对于数组[-9,-2,-3,-5,-3],应该返回-2。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOPMYklN-1667799778476)(D:\HONOR Magic-link\Huawei Share\capture-2022-03-28-20-51-04.jpg)]
最大数[179]
class Solution {
public String largestNumber(int[] nums) {
PriorityQueue<String> heap = new PriorityQueue<>((x, y) -> (y + x).compareTo(x + y));
for (int x : nums) heap.offer(String.valueOf(x));
StringBuilder res = new StringBuilder();
while (heap.size() > 0) res.append(heap.poll());
if (res.charAt(0) == '0') return "0";
return res.toString();
}}
分数到小数[166]
class Solution {
public String fractionToDecimal(int numerator, int denominator) {
long numeratorLong = (long) numerator;
long denominatorLong = (long) denominator;
if (numeratorLong % denominatorLong == 0) {
return String.valueOf(numeratorLong / denominatorLong);
}
StringBuffer sb = new StringBuffer();
if (numeratorLong < 0 ^ denominatorLong < 0) {
sb.append('-');
}
// 整数部分
numeratorLong = Math.abs(numeratorLong);
denominatorLong = Math.abs(denominatorLong);
long integerPart = numeratorLong / denominatorLong;
sb.append(integerPart);
sb.append('.');
// 小数部分
StringBuffer fractionPart = new StringBuffer();
Map<Long, Integer> remainderIndexMap = new HashMap<Long, Integer>();
long remainder = numeratorLong % denominatorLong;
int index = 0;
while (remainder != 0 && !remainderIndexMap.containsKey(remainder)) {
remainderIndexMap.put(remainder, index);
remainder *= 10;
fractionPart.append(remainder / denominatorLong);
remainder %= denominatorLong;
index++;
}
if (remainder != 0) { // 有循环节
int insertIndex = remainderIndexMap.get(remainder);
fractionPart.insert(insertIndex, '(');
fractionPart.append(')');
}
sb.append(fractionPart);
return sb.toString();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xqhXBItx-1667799778477)(D:\HONOR Magic-link\Huawei Share\capture-2022-03-28-21-34-27.jpg)]
质数排列[1175]
class Solution {
public int numPrimeArrangements(int n) {
//0--100内的质数
int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97};
//数量
int cnt = 0;
//统计输入内的范围内的质数数量
for (int prime : primes) if (n >= prime) cnt++;
//进行阶乘运算
long ans = 1;
final int MOD = (int) 1e9 + 7;
for (int i = 1; i <= cnt; i++) ans = ans * i % MOD;
for (int i = 1; i <= n - cnt; i++) ans = ans * i % MOD;
return (int) ans % MOD;
}
}