基础数据结构篇
初识算法
二分查找
算法描述
需求:在有序数组 A 内,查找值 target
- 如果找到返回索引
- 如果找不到返回 -1
算法 描述 | 方法一 | 方法二 |
---|---|---|
前提 | 给定一个内含 n 个元素的有序数组 A,满足 A 0 A_0 A0 ≤ \leq ≤ A 1 A_1 A1 ≤ \leq ≤ A 2 A_2 A2 ≤ \leq ≤ . . . ≤ \leq ≤ A n − 1 A_{n-1} An−1 ,一个待查值 target | 给定一个内含 n 个元素的有序数组 A,满足 A 0 A_0 A0 ≤ \leq ≤ A 1 A_1 A1 ≤ \leq ≤ A 2 A_2 A2 ≤ \leq ≤ . . . ≤ \leq ≤ A n − 1 A_{n-1} An−1 ,一个待查值 target |
1 | 设置 i = 0, j = n - 1 (索引 i 和 j 指向的元素都参与运算) | 设置 i = 0, j = n (索引 j 指向的元素不参与运算) |
2 | 如果 i > j ,结束查找,没找到 | 如果 i >= j ,结束查找,没找到 |
3 | 设置 m = floor( i + j 2 \frac{i+j}{2} 2i+j) , m为中间索引,floor 是向下取整 | 设置 m = floor( i + j 2 \frac{i+j}{2} 2i+j) , m为中间索引,floor 是向下取整 |
4 | 如果 target < \lt < A m A_m Am 设置 j = m - 1, 跳到第 2 步 | 如果 target < \lt < A m A_m Am 设置 j = m , 跳到第 2 步 |
5 | 如果 A m A_m Am < \lt < target 设置 j = m +1, 跳到第 2 步 | 如果 A m A_m Am < \lt < target 设置 j = m +1, 跳到第 2 步 |
6 | 如果 A m A_m Am = target 结束查找,找到了 | 如果 A m A_m Am = target 结束查找,找到了 |
**总结:**方法一和方法二的不同主要是因为索引 j 指向的元素是否参与运算
代码实现
package Just.binary;
import java.util.Scanner;
public class BinarySearch {
public static void main(String[] args) {
//待查找的目标值——target,手动输入
Scanner sc = new Scanner(System.in);
System.out.println("输入待查找值:");
int target = sc.nextInt();
//待查找的升序数组
int [] arr = {7,13,21,30,38,44,52,53};
//定义变量,用来接收返回值
//调用方法一
//int num = binarySearchBasic1(arr,target);
//调用方法二
int num = binarySearchBasic2(arr,target);
if(num == -1){
System.out.println("未找到");
}else{
System.out.println("成功找到,索引为" + num);
}
}
//二分查找
//方法一:
public static int binarySearchBasic1(int [] arr,int target){
//定义数组的开头元素索引
int i = 0;
//定义数组的结尾元素索引
int j = arr.length - 1;
//while循环对数组进行遍历
while(i <= j){
//定义数组中索引i和索引j之间的中间索引
int m = (i + j)/2;
//把待查值target与中间索引对应的值相比较,确定待查值target所在区间
if(target < arr[m]){
//target值小于arr[m],说明如果target存在,在m索引左边,让j = m - 1,继续循环判断
j = m - 1;
}else if(target > arr[m]){
//target值大于arr[m],说明如果target存在,在m索引右边,让j = m + 1,继续循环判断
i = m + 1;
}else{
//target既不大于也不小于,说明target等于arr[m],返回索引
return m;
}
}
//如果在数组中不存在,返回-1
return -1;
}
//方法二:
public static int binarySearchBasic2(int [] arr,int target){
//定义数组的开头元素索引
int i = 0;
//定义数组的结尾元素索引
int j = arr.length;
//while循环对数组进行遍历
while(i < j){
//定义数组中索引i和索引j之间的中间索引
int m = (i + j) >>> 1;
//把待查值target与中间索引对应的值相比较,确定待查值target所在区间
if(target < arr[m]){
//target值小于arr[m],说明如果target存在,在m索引左边,让j = m - 1,继续循环判断
j = m;
}else if(target > arr[m]){
//target值大于arr[m],说明如果target存在,在m索引右边,让j = m + 1,继续循环判断
i = m + 1;
}else{
//target既不大于也不小于,说明target等于arr[m],返回索引
return m;
}
}
//如果在数组中不存在,返回-1
return -1;
}
}
------------------------------------------------------------------------------------------
运行结果:
输入待查找值:
7
成功找到,索引为0
输入待查找值:
66
未找到
注意:
- 求中间值索引:m = ( i + j )/2 ;可以使用 无符号右移运算符 >>> 1
m = ( i + j ) >>> 1;
衡量算法好坏
时间复杂度
计算机科学中,时间复杂度是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本
- 不依赖于环境
如何表示时间复杂度呢?
- 假设算法要处理的数据规模是n,代码总的执行行数 f(n) 来表示,例如:
- 线性查找算法的函数 f(n) = 3*n + 3
- 二分查找算法的函数 f(n) = ( floor( l o g 2 log_2 log2(n) ) + 1) * 5 + 4
- 为了对 f(n) 进行化简,应当抓住主要矛盾,找到一个变化趋势与之相近的表示方法
大 O 表示法
其中
- c c c , c 1 c_1 c1 , c 2 c_2 c2 都是一个常数
- f ( n ) f(n) f(n) 是实际执行代码行数与 n n n 的函数
- g ( n ) g(n) g(n) 是经过简化,变化趋势与 f ( n ) f(n) f(n) 一致的 n 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))
- 举例: f ( n ) = n 2 + 100 f(n)=n^2+100 f(n)=n2+100 ,从 n 0 = 10 n_0=10 n0=10 时, g ( n ) = 2 ∗ n 2 g(n)=2*n^2 g(n)=2∗n2 是它渐进上界,记作 O ( n 2 ) O(n^2) O(n2)
例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中的 n n n
-
多项式中数量规模较小(低次项)的表达式可以省略,如
- 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 n n
-
不同底数的对数,渐进上界可以用一个对数函数 l o g n log_n logn表示
- 例如: l o g 2 ( n ) log_2(n) log2(n)可以替换为 l o g 1 0 ( n ) log_10(n) log10(n)
-
类似的,对数的常数次幂可以省略
- 如: 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表示法来衡量:一个算法执行随数据规模增大,而增长的额外空间成本
举例:
public static int binarySearchBasic1(int [] arr,int target){
int i = 0,int j = arr.length - 1; //设置指针和初值
while(i <= j){ //i~j 范围内有东西
int m = (i + j)/2;
if(target < arr[m]){ // 目标在左边
j = m - 1;
}else if(target > arr[m]){ // 目标在右边
i = m + 1;
}else{ // 找到了
return m;
}
}
return -1;
}
空间复杂度——额外空间成本
在本例中,额外的空间成本是 i、j 和 m 占用的空间,由于其三个都是整形,所以占12个字节,空间复杂度: O ( 1 ) O(1) O(1)
二分查找性能
下面分析二分查找算法的性能
时间复杂度
- 最坏情况: O ( l o g 2 ( n ) ) O(log_2(n)) O(log2(n))
- 最好情况:如果待查找元素恰好在数组中央,只需要循环一次 O ( 1 ) O(1) O(1)
空间复杂度
- 需要常数个指针 i ,j,m,因此额外暂用的空间是 O ( 1 ) O(1) O(1)