一、算法概念
1.1 事前分析法
- 分析最差的执行情况
- 假设每行语句执行的时间一样
1.2 时间复杂度
时间复杂度:用来衡量一个算法的执行,随数据规模增大而增长的时间成本
不依赖于(软硬件)环境因素
如何表示时间复杂度?
-
假设算法要处理的数据规模是 n,代码总执行行数用 f(n) 表示,例如:
- 线性查找算法函数: f ( n ) = 3 ∗ n + 3 f(n) = 3 * n + 3 f(n)=3∗n+3
- 二分查找算法函数: f ( n ) = ( f l o o r ( l o g 2 ( n ) ) + 1 ) ∗ 5 + 4 f(n) = (floor(log_2(n)) + 1) * 5 + 4 f(n)=(floor(log2(n))+1)∗5+4
-
为了对 f ( n ) f(n) f(n)进行化简,应当抓住主要矛盾,找到一个变化趋势与之相近的表示法
1.3 大 O O O 表示法
- c , c 1 , c 2 c,c_1,c_2 c,c1,c2 :常量系数
- f ( n ) f(n) f(n) :实际执行代码行数与 n 的函数
- g ( n ) g(n) g(n):经过化简,变化趋势与 f(n) 一致的 n 的函数
1.3.1 渐进上界(asymptotic upper bound)
从某个常数 n 0 n_0 n0 开始, c ∗ g ( n ) c * g(n) c∗g(n) 总是位于 f ( n ) f(n) f(n) 上方,记作 O ( g ( n ) ) O(g(n)) O(g(n))
代表算法执行最差的情况
例1:
- f ( n ) f(n) f(n) = 3 * n n n + 3
- g ( n ) g(n) g(n) = n n n
- 取 c c c = 4,在 n 0 n_0 n0 = 3 之后, g ( n ) g(n) g(n) 可以作为 f ( n ) f(n) f(n) 的渐进上界,因此为 O ( n ) O(n) O(n)
例2:
- f ( n ) = 5 ∗ f l o o r ( l o g 2 ( n ) ) + 9 f(n) = 5 * floor(log_2(n)) + 9 f(n)=5∗floor(log2(n))+9
- g ( n ) = l o g 2 ( n ) g(n) = log_2(n) g(n)=log2(n)
- O ( l o g 2 ( n ) ) O(log_2(n)) O(log2(n))
例3:
- $f(n) = n 2 n{^2} n2 + 100$
- $g(n) = 2 * n 2 n^2 n2
- O O O( n 2 n^2 n2)
1.3.2 渐进下界(asymptotic lower bound)
从某个常数 n 0 n_0 n0 开始,c * g(n) 总是位于 f(n) 下方,那么记作 Ω \Omega Ω(g(n))
代表算法执行最佳的情况
1.3.3 渐进紧界(asymptotic tight bounds)
从某个常数 n 0 n_0 n0 开始,c * g(n) 总是在 c 1 c_1 c1 * g(n) 和 c 2 c_2 c2 * g(n) 之间,那么记作 Θ \Theta Θ(g(n))
即代表算法执行最差的情况,也可以代表算法执行最佳的情况
1.3.4 总结规律:已知 f ( n ) 求 g ( n ) : f(n) 求 g(n): f(n)求g(n):
- 表达式中相乘的常量可以省略,如
- f ( n ) = 100 ∗ n 2 f(n) = 100 * n^2 f(n)=100∗n2 中的100
- 多项式中数量规模更小(低次项)表达式,如
- f ( n ) = n 2 + n f(n) = n^2 + n f(n)=n2+n 中低 n n n
- f ( n ) = n 3 + n 2 中的 n 2 f(n) = n^3 + n^2 中的 n^2 f(n)=n3+n2中的n2
- 不同底数的对数,渐进上界可以用一个对数函数
l
o
g
log
log
n
n
n 表示,如
- l o g 2 ( n ) 可以替换为 l o g 1 0 ( n ) ,因为 l o g 2 ( n ) = l o g 1 0 ( n ) l o g 1 0 ( 2 ) ,相乘的常量 1 l o g 1 0 ( 2 ) 可以省略 log_2(n) 可以替换为 log_10(n),因为 log_2(n) = \frac{log_10(n)}{log_10(2)},相乘的常量 \frac{1}{log_10(2)}可以省略 log2(n)可以替换为log10(n),因为log2(n)=log10(2)log10(n),相乘的常量log10(2)1可以省略
- 类似的,对数的常数次幂可省略,如
- l o g ( n c ) = c ∗ l o g ( n ) log(n^c) = c * log(n) log(nc)=c∗log(n)
1.3.5 常见的 O O O 表示法
按时间复杂度从低到高:
- 黑色横线 O ( 1 ) O(1) O(1),常量时间,意味着算法时间不随数据规模而变化
- 绿色 O ( l o g ( n ) ) O(log(n)) O(log(n)),对数时间
- 蓝色 O ( n ) O(n) O(n),线性时间,算法时间与数据规模成正比
- 橙色 O ( n ∗ l o g ( n ) ) O(n * log(n)) O(n∗log(n)),拟线性时间
- 红色 O ( n 2 ) O(n^2) O(n2),平方时间
- 黑色朝上 O ( 2 n ) O(2^n) O(2n),指数时间
- O ( n ! ) , n 的阶乘 O(n!),n 的阶乘 O(n!),n的阶乘
1.3 空间复杂度
衡量算法在空间占用上的好坏
与时间复杂度类似,一般也用大 O O O 表示法来衡量
一个算法执行随数据规模增大,而增长的额外空间成本
以二分查找算法来看空间占用情况,int 类型的 i,j,m 总共占用 4 * 3 = 12 字节,随着数组增大,没有额外的内存空间占用。空间复杂度为:O(1)
二、二分查找
2.1 概念
在有序数组 A 内,查找某一值 target,找到返回 target 所在位置的索引,找不到返回 -1
前提条件: 数组为有序数组,排序为升序,可存在重复元素
2.2 执行步骤
1、设置两个指针 i = 0,j = n-1
2、设置循环退出条件:当 i > j 时结束查询,未找到 target,返回 -1
3、设置中间索引 m, m = floor((i + j) / 2),floor 向下取整
4、判断 target 与 Am 的值:
- 如果 target < Am,设置 j = m - 1,跳到第2步
- 如果 target > Am,设置 i = m + 1,跳到第2步
- 如果 target = Am,结束查询,返回 m
2.3 基础版二分查找
/**
* 基础版
*/
public static int binarySearchBasic(int[] a, int target){
// 设置指针初始值
int i = 0, j = a.length -1;
// 范围内查询
while(i <= j){
// 中间索引赋值
int m = (i + j) / 2;
// 目标在中间值左边
if(target < a[m]){
j = m - 1;
}else if(a[m] < target){
i = m + 1;
}else{
// 找到了
return m;
}
}
// 未找到
return -1;
}
基础版存在问题:
-
i <= j 与 i < j 的问题
- i == j :i,j 共同指向的元素也会参与比较
- i < j :只有 m 指向的元素参与比较
-
(i + j)/ 2,边界问题
在Java中,当计算超出该数据类型的边界值时,会改变数据符号
改用 无符号右移的二进制计算方式可以避免此问题: (i + j)>>> 1
2.4 改动版二分查找
/**
* 改动版
*/
public static int binarySearchBasic(int[] a, int target){
// 第一处:j 只做为边界,指向的一定不是查找目标
int i = 0, j = a.length;
// 第二处:j = a.length; i <= j,在查找一个不存在的 target 时,死循环
while(i < j){
// 中间索引赋值
int m = (i + j) >>> 2;
// 目标在中间值左边
if(target < a[m]){
// 第三处:此时 Am 的值一定不是要查找的 target
j = m;
}else if(a[m] < target){
i = m + 1;
}else{
// 找到了
return m;
}
}
// 未找到
return -1;
}
2.5 二分查找性能分析
- 时间复杂度
- 最坏情况: O ( l o g n ) O(log\ n) O(log n)
- 最好情况:如果待查询元素恰好在数组中央,只循环一次 O ( 1 ) O(1) O(1)
- 空间复杂度
- 需要常数个指针 i , j , m i,j,m i,j,m,因此额外占用的空间是 O ( 1 ) O(1) O(1)
2.6 平衡版二分查找
优势: 减少循环内的平均比较次数,体现在数据量较大的情况
缺点: 如果待查询元素在中间时,要等 while 遍历结束,不存在只循环一次,时间复杂度为 O ( 1 ) O(1) O(1)情况
时间复杂度: Θ \Theta Θ(log(n))
/**
* 平衡版
*/
public static int binarySearchBasic(int[] a, int target){
// 左闭右开,i 指向的是有效元素的位置,j 指向的是有效元素之外的位置
int i = 0, j = a.length;
// j - i 表示数组内待查找元素个数
// 当 j - i = 1 时退出循环,此时只剩下 a[i] 元素未比较
while(1 < j - i){
// 中间索引赋值
int m = (i + j) >>> 2;
if(target < a[m]){
// 目标在中间值左边,缩小右边界
j = m;
}else{
// 目标在中间值右边,缩小左边界
// 目标可能是中间 m,也可能在 m 的右侧元素
i = m;
}
}
// 循环外比较 a[i] 与 target
if(a[i] == target){
// 找到了
return i;
}else {
// 未找到
return -1;
}
}
2.7 Java 中的二分查找
实现类: Arrays.binarySearch(),采用基础版二分查找
返回结果:
- 找到了要查询的值,返回该值在数组中的索引位置
- 未找到,返回 − ( 插入点 ) − 1 -(插入点) - 1 −(插入点)−1,加上 -1 是为了区分插入点为 0 的情况
2.8 LeftRightmost(最左/最右版)二分查找
public static int binarySearchBasic(int[] a, int target){
// 设置指针初始值
int i = 0, j = a.length -1;
// 初始化候选位置
int candidate = -1;
// 范围内查询
while(i <= j){
// 中间索引赋值
int m = (i + j) / 2;
// 目标在中间值左边
if(target < a[m]){
j = m - 1;
}else if(a[m] < target){
i = m + 1;
}else{
// 找到了,记录该位置作为候选位置
candidate = m;
// 缩小右边 j 界位置,继续向左找
j = m - 1;
// 缩小左侧 i 边界位置,继续向右找
// i = m + 1;
}
}
return candidate;
}
Leftmost 返回值优化
public static int binarySearchBasic(int[] a, int target){
// 设置指针初始值
int i = 0, j = a.length -1;
// 范围内查询
while(i <= j){
// 中间索引赋值
int m = (i + j) / 2;
// 目标在中间值左边
if(target <= a[m]){
j = m - 1;
}else{
i = m + 1;
}
}
// 返回 >= target 的最靠左的索引位置
return i;
}
Rightmost 返回值优化
public static int binarySearchBasic(int[] a, int target){
// 设置指针初始值
int i = 0, j = a.length -1;
// 范围内查询
while(i <= j){
// 中间索引赋值
int m = (i + j) / 2;
// 目标在中间值左边
if(target < a[m]){
j = m - 1;
}else{
i = m + 1;
}
}
// 返回 <= target 的最靠右的索引位置
return i - 1;
}
返回值优化后的应用场景:
- 给定一个 target 求 target 在数组中的所处位置,即求排名,采用 Leftmost + 1
- 给定一个 target 求 target 在数组中所处位置的前一个元素,即求前任,采用 Leftmost - 1
- 给定一个 target 求 target 在数组中所处位置的后一个元素,即求前任,采用 Rightmost + 1
- 给定一个 target 求数组中 大于、大于等于、小于、小于等于 target 的元素,即范围查询
2.8 递归版二分查找
public static int binarySearch(int[] a, int target, int i, int j){
if(i > j){
return -1;
}
int m = (i + j) >>> 1;
if(target < a[m]){
return binarySearch(a, target, i, m-1);
}else if(a[m] < target){
return binarySearch(a, target, m+1, j);
}else{
return m;
}
}