基础数据结构一篇-----二分查找

一、算法概念

1.1 事前分析法

  1. 分析最差的执行情况
  2. 假设每行语句执行的时间一样

1.2 时间复杂度

时间复杂度:用来衡量一个算法的执行,随数据规模增大而增长的时间成本

不依赖于(软硬件)环境因素

如何表示时间复杂度?

  • 假设算法要处理的数据规模是 n,代码总执行行数用 f(n) 表示,例如:

    • 线性查找算法函数: f ( n ) = 3 ∗ n + 3 f(n) = 3 * n + 3 f(n)=3n+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 表示法

大O表示法

  • c , c 1 , c 2 c,c_1,c_2 cc1c2 :常量系数
  • 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) cg(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)=5floor(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)=100n2 中的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)=clog(n)

1.3.5 常见的 O 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(nlog(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;
}

基础版存在问题:

  1. i <= j 与 i < j 的问题

    • i == j :i,j 共同指向的元素也会参与比较
    • i < j :只有 m 指向的元素参与比较
  2. (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;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值