1.概念
二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。
需求 :在有序 数组 A A A 内,查找值 t a r g e t target target
- 如果找到返回索引
- 如果找不到返回 − 1 -1 −1
前提 | 给定一个内含 n n n 个元素的有序数组 A A A,满足 A 0 ≤ A 1 ≤ A 2 ≤ ⋯ ≤ A n − 1 A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1} A0≤A1≤A2≤⋯≤An−1,一个待查值 t a r g e t target target |
---|---|
1 | 设置 i = 0 i=0 i=0, j = n − 1 j=n-1 j=n−1 |
2 | 如果 i > j i \gt j i>j,结束查找,没找到 |
3 | 设置 m = f l o o r ( i + j 2 ) m = floor(\frac {i+j}{2}) m=floor(2i+j) , m m m 为中间索引, f l o o r floor floor 是向下取整( ≤ i + j 2 \leq \frac {i+j}{2} ≤2i+j 的最小整数) |
4 | 如果 t a r g e t < A m target < A_{m} target<Am 设置 j = m − 1 j = m - 1 j=m−1,跳到第2步 |
5 | 如果 A m < t a r g e t A_{m} < target Am<target 设置 i = m + 1 i = m + 1 i=m+1,跳到第2步 |
6 | 如果 A m = t a r g e t A_{m} = target Am=target,结束查找,找到了 |
2.基础版
java
public static int binarySearch(int[] arr, int target) {
int min = 0, max = arr.length - 1;
while (min <= max) {
int mid = (max + min) >>> 1;//无符号右移
int midVal = arr[mid];
if (target < midVal) {
max = mid - 1;
} else if (midVal < target) {
min = mid + 1;
} else {
return mid;
}
}
return -1;
}
3.改动版
java
public static int binarySearch(int[] arr, int target) {
int min = 0, max = arr.length;
while (min < max) {
int mid = (max + min) >>> 1;//无符号右移
int midVal = arr[mid];
if (target < midVal) {
max = mid;
} else if (midVal < target) {
min = mid + 1;
} else {
return mid;
}
}
return -1;
}
4.衡量算法的好坏
对比线性查找
java
for (int i = 0; i < arr.length; i++) {
if(arr[i] == target){
return i;
}
}
return -1;
事前分析法
计算机科学中,时间复杂度是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本
- 不依赖于环境因素
如何表示时间复杂度呢?
-
假设算法要处理的数据规模是 n n n,代码总的执行行数用函数 f ( n ) f(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) 进行化简,应当抓住主要矛盾,找到一个变化趋势与之相近的表示法
大 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 ) f(n) f(n) 一致的 n 的函数
渐进上界
渐进上界(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 ) = 3 ∗ n + 3 f(n) = 3*n+3 f(n)=3∗n+3
- g ( n ) = n g(n) = n g(n)=n
- 取 c = 4 c=4 c=4,在 n 0 = 3 n_0=3 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))
已知 f ( n ) f(n) f(n) 来说,求 g ( n ) g(n) g(n)
- 表达式中相乘的常量,可以省略,如
- f ( n ) = 100 ∗ n 2 f(n) = 100*n^2 f(n)=100∗n2 中的 100 100 100
- 多项式中数量规模更小(低次项)的表达式,如
- f ( n ) = n 2 + n f(n)=n^2+n f(n)=n2+n 中的 n n n
- f ( n ) = n 3 + n 2 f(n) = n^3 + n^2 f(n)=n3+n2 中的 n 2 n^2 n2
- 不同底数的对数,渐进上界可以用一个对数函数 log n \log n logn 表示
- 例如: l o g 2 ( n ) log_2(n) log2(n) 可以替换为 l o g 10 ( n ) log_{10}(n) log10(n),因为 l o g 2 ( n ) = l o g 10 ( n ) l o g 10 ( 2 ) log_2(n) = \frac{log_{10}(n)}{log_{10}(2)} log2(n)=log10(2)log10(n),相乘的常量 1 l o g 10 ( 2 ) \frac{1}{log_{10}(2)} log10(2)1 可以省略
- 类似的,对数的常数次幂可省略
- 如: l o g ( n c ) = c ∗ l o g ( n ) log(n^c) = c * log(n) log(nc)=c∗log(n)
常见大 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 ! ) O(n!) O(n!)
空间复杂度
与时间复杂度类似,一般也使用大 O O O 表示法来衡量:一个算法执行随数据规模增大,而增长的额外空间成本
二分查找性能
下面分析二分查找算法的性能
时间复杂度
- 最坏情况: O ( log n ) O(\log n) O(logn)
- 最好情况:如果待查找元素恰好在数组中央,只需要循环一次 O ( 1 ) O(1) O(1)
空间复杂度
- 需要常数个指针 i , j , m i,j,m i,j,m,因此额外占用的空间是 O ( 1 ) O(1) O(1)
5.平衡版
思想:
- 左闭右开的区间, i i i 指向的可能是目标,而 j j j 指向的不是目标
- 不奢望循环内通过 m m m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i i i)
- j − i > 1 j - i > 1 j−i>1 的含义是,在范围内待比较的元素个数 > 1
- 改变 i i i 边界时,它指向的可能是目标,因此不能 m + 1 m+1 m+1
- 循环内的平均比较次数减少了
- 时间复杂度 Θ ( l o g ( n ) ) \Theta(log(n)) Θ(log(n))
java
public static int binarySearch(int[] arr, int target) {
int min = 0, max = arr.length;
while (1 < max - min) {
int mid = (min + max) >>> 1;
if (target < arr[mid]) {
max = mid;
} else {
min = mid;
}
}
return (arr[min] == target) ? min : -1;
}
6.Leftmost 查询最左侧重复元素
java
public static int leftMost(int[] arr, int target) {
int min = 0, max = arr.length - 1;
int result = -1;//记录候选位置
while (min <= max) {
int mid = (min + max) >>> 1;
int midVal = arr[mid];
if (target < midVal) {
max = mid - 1;
} else if (midVal < target) {
min = mid + 1;
} else {
result = mid;
max = mid - 1;
}
}
return result;
}
返回 >=target 的最靠左索引
- leftmost 返回值的另一层含义: < t a r g e t \lt target <target 的元素个数
- 小于等于中间值,都要向左找
java
public static int leftMost(int[] arr, int target) {
int min = 0, max = arr.length - 1;
while (min <= max) {
int mid = (min + max) >>> 1;
if (target <= arr[mid]) {
max = mid - 1;
} else {
min = mid + 1;
}
}
//返回 >=target 的最靠左索引
return min;
}
7.Rightmost 查询最右侧重复元素
java
public static int rightMost(int[] arr, int target) {
int min = 0, max = arr.length - 1;
int result = -1;
while (min <= max) {
int mid = (min + max) >>> 1;
int midVal = arr[mid];
if (target < midVal) {
max = mid - 1;
} else if (midVal < target) {
min = mid + 1;
} else {
result = mid;
min = mid + 1;
}
}
return result;
}
返回<=target 的最靠右索引
大于等于中间值,都要向右找
java
public static int rightMost(int[] arr, int target) {
int min = 0, max = arr.length - 1;
while (min <= max) {
int mid = (min + max) >>> 1;
if(target < arr[mid]){
max = mid - 1;
}else{
min = mid + 1;
}
}
//返回<=target 的最靠右索引
return min - 1;
}