1. 认识时间复杂度
常数时间的操作:一个操作如果和数据量没有关系,每次都是固定的时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的指标。常用O(读作big O)来表示。具体来说,在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分记为f(N),那么时间复杂度为O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。
一个简单的理解时间复杂度的例子:
一个有序数组 A (无重复值),另一个有序数组 B(无重复值),请打印公共部分,A数组长度为 N ,B 数组长度为 M
算法流程1:对于数组 B 中的每一个数,都在 A 中通过遍历的方式找一下;O(M*N)
算法流程2:对于数组 B 中的每一个数,都在 A 中通过二分的方式找一下;O(M*logN)
因为 A 数组有序,每次都取中值与 B 中的数比较,根据比较结果取中值左边或右边继续
算法流程3:先把数组 B 排序,然后用类似外排的方式打印所有在 A 中出现的数;O(M+N)
A B 开头各一个指针,那个指针所指的值小,哪个指针向下移动
分析:
O(N*logM) 的肯定要比 O(N^2) 的好,因为流程2的存在,所以就不考虑流程1。那么流程2和流程3谁更好一些呢?不能直观的认为流程3比流程2好,它们的好坏取决于M和N的大小。
如果 M=4,N=2^32,流程2=O(4*32)<流程3=O(4+2^32);
如果 M=2^32,N=4,流程3=O(2^32+4)<流程2=O(2^32*2);
2. 认识空间复杂度
空间复杂度一般用来指算法所使用的额外空间,额外空间不包括输入所占用的空间和要求的返回所占用的空间。
3. 对数器的概念和使用
- 有一个你想要测的方法a,
- 实现一个绝对正确但是复杂度不好的方法b,
- 实现一个随机样本产生器
- 实现比对的方法
- 把方法a和方法b比对很多次来验证方法a是否正确。
- 如果有一个样本使得比对出错,打印样本分析是哪个方法出错
- 当样本数量很多时比对测试依然正确,可以确定方法a已经正确
- 对数器也可以用来验证贪心策略的正确性
4. 冒泡排序细节的讲解与复杂度分析
- 从数组中第一个数开始,每一个数都和后面的数比较,大的数往后换
- 每一趟都可以确定一个最大值(第二次冒泡的范围从 [0,N-1] 变成 [0,N-2] )
- 时间复杂度是一个等差数列,所以是 O(N^2)
- 空间复杂度 O(1)
public static void bubbleSort(int[] arry){
if(arr == null || arr.length < 2){
return;
}
// 因为每轮确定一个最大值,所以 end 不断提前
for(int end = arr.length-1; end>0; end--){
// i - end 范围上选出一个最大值
for(int i=0; i
if(arr[i]>arr[i+1]){
swap(arr, i, i+1);
}
}
}
}
5. 选择排序的细节讲解与复杂度分析
- [0,N-1]范围找出最小放在开始位置
- [1,N-1]范围找出最小放在开始位置
- ……
- 时间复杂度 O(N^2)
- 空间复杂度 O(1)
public static void selectionSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
// i 是寻找最小值的范围的左边界,因为每轮确定一个最小,所以左边界不断向后移
for(int i=0; i1 ; i++){
int minIndex=i;
// i - N-1 范围找最小值的下标
for(int j=i+1; j
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
// 把最小的换到前面去
swap(arr, i, minIndex);
}
}
6. 插入排序的细节讲解与复杂度分析
- 数组的前部有一个初始大小为1的有序域,不断扩大,每次增大1
- 新进来的数与有序域内从后往前每个数依次比较,小的话就插入
- 类似于扑克牌整牌,每次抽牌后从后往前滑,遇到合适的位置就放进去
- 最好时间复杂度能达到 O(N),即数组有序的情况下
- 最差时间复杂度 O(N^2)
- 空间复杂度 O(1)
public static void insertionSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
// i 是有序域右边界,不断扩大
for(int i=1; i
// 从有序域右边界往左依次与右边界比较,选出最小作为新的右边界
for(int j=i-1; j>=0 && arr[j]>arr[j+1]; j--){
swap(arr, j, j+1);
}
}
}
7. 剖析递归行为和递归行为时间复杂度的估算
递归函数是借由系统栈来完成的,栈中保存了函数的参数、代码执行到第几行等一系列信息。
Master 公式:
T( N ) = a * T( N / b ) + O( N ^ d )
- log( b, a ) > d -> 复杂度为O( N ^ log( b, a ) )
- log( b, a ) = d -> 复杂度为O( N ^ d * logN )
- log( b, a ) < d -> 复杂度为O( N ^ d )
8. 小和问题
在一个数组中,将每一个数左边小于等于当前数的所有数字累加起来,叫做这个数组的小和。求一个数组的小和。
例子:
[1,3,4,2,5]
1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数,1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16
改写归并排序解决小和问题
归并排序中每次 merge 的过程,都累加计算 左边的每一个数的值 * 右边中比左边这个数大的数字的个数
import java.util.*;
public class Main {
public static void main (String[] args) {
// 处理输入
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt();
int[] nums = new int[m];
for (int i = 0; i < nums.length; i++) {
nums[i] = scanner.nextInt();
}
scanner.close();
if (nums.length < 2 || nums == null) {
System.out.println(0);
}
System.out.println(mergeSort(nums, 0, nums.length - 1));
}
public static long mergeSort(int[] arr, int L, int R) {
if (L == R) {
return 0;
}
int mid = L + ((R - L) >> 1); // L和R中点的位置
return mergeSort(arr, L, mid) + mergeSort(arr, mid + 1, R) + merge(arr, L, mid, R);
}
public static long merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
long res = 0; // 小和的累加值可能超过 int 的存储范围
while (p1 <= m && p2 <= r) {
// 此时左半部分右半部分都已排好序,若 arr[p1] < arr[p2]
// ( r - p2 + 1 ) 代表右半部分有多少个比 arr[p1] 大的数字
// 将 (r - p2 + 1) * arr[p1] 累加到小和中
res += arr[p1] <= arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
//两个必有且只有一个越界
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}
}
9. 逆序对问题
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请计算共有多少逆序对。
例子:
4,3,5,0,6
逆序对:
(4,3)(4,0)(3,0)(5,0)
改写归并排序解决逆序对问题
与小和问题类似,只不过变成了右边中有哪些数比左边的大
public class InversePairs {
public static void main(String[] args) {
int[] nums = {4, 3, 5, 0, 6};
System.out.println(mergeSort(nums, 0 , nums.length - 1));
}
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ( ( r - l ) >> 1 );
return mergeSort(arr, l, mid) + mergeSort(arr, mid + 1, r) + merge(arr, l, mid, r);
}
public static int merge(int[] arr, int l, int m, int r){
int[] help = new int[ r - l + 1];
int res = 0;
int i = 0;
int index1 = l;
int index2 = m + 1;
while (index1 <= m && index2 <= r) {
// 此时左半部分右半部分都已排好序,若 arr[index1] > arr[index2]
// index2 - (m + 1) + 1 代表右边有多少个数比 arr[index1] 小
// 累加 index2 - m ,计算逆序对数量
res += arr[index1] > arr[index2] ? index2 - m : 0;
help[i++] = arr[index1] < arr[index2] ? arr[index1++] : arr[index2++];
}
while (index1 <= m) {
help[i++] = arr[index1++];
}
while (index2 <= m) {
help[i++] = arr[index2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}
}