【声明】 图源:牛客网
应用场景
- 在有序序列中查找一个数,时间复杂度O(logN)(经典场景)
- 给定有序数组arr,判断m是否存在arr中
- 并不一定非要在有序序列中才能得到应用

考察点
- 边界条件(处理不当会发生死循环、漏掉某个数的情况),需要仔细设计对中间划分点的逻辑判断以及设计循环的终止条件
- 给定处理或查找的对象不同(数组有无重复元素)
- 判断条件不同
- 要求返回的内容不同
- 有序循环数组中进行二分搜索
- 有序循环数组:有序数组左边任意长度的部分拿到右边去,右边部分拿到左边来形成的数组

注意点
- $mid=(left+right)/2$,虽然是常见写法,但要注意当
left和right下标值过大时,可能会导致溢出。因此更安全的写法应该是$mid=left+(right-left)/2$。
经典案例
案例一
题目
求局部最小值
给定一个无序数组arr,已知任意相邻的两个元素,值都不重复。请返回任意一个局部最小的位置。
所谓局部最小的位置是指,如果arr[0]<arr[1],那么位置0就是一个局部最小的位置。如果arr[N-1](也就是arr最右边的数)小于arr[N-2],那么位置N-1也是局部最小的位置。如果位置i既不是最左位置也不是最右位置,那么只要满足arr[i]同时小于它左右两侧的值即(arr[i-1]和arr[i+1]),那么位置i也是一个局部最小的位置。
分析
二分搜索实现,时间复杂度O(logN)
arr为空或长度为0,返回-1,表示局部最小位置不存在
如果arr长度为1,返回0,因为此时0是局部最小位置
如果arr长度大于1,先检测两边的位置:
- 如果arr[0]<arr[1],直接返回0
- 如果arr[N-1]<arr[N-2],直接返回N-1
- 如果上述两个条件不满足,则观察arr[mid]的情况:
- 如果arr[mid]同时小于其左右两边的数,则mid就是一个局部最小位置,直接返回mid
- 如果arr[mid]小于其右边的数,大于其左边的数,则从mid向左看整体趋势减小,此时mid左侧存在局部最小位置。此时对左边继续进行同样逻辑的二分搜索。
- 如果arr[mid]大于其右边的数,小于其左边的数,则从mid向右看整体趋势减小,此时mid右侧存在局部最小位置。此时对右边继续进行同样逻辑的二分搜索。
- 如果arr[mid]大于左右两边的数,则任选一边,继续进行同样逻辑的二分搜索。

考点
二分搜索并不一定要在有序的数组上才可进行,只要在查找的过程中能够淘汰一半,就可以使用
实现
public int getLessIndex(int[] arr) { |
案例二
题目
元素最左出现
给定一个有序数组arr,再给定一个整数num,请在arr中找到num这个数出现的最左边的位置。
分析
- 定义一个变量res记录最近一次找到目标值的下标位置。查找方式采用二分查找。

实现
public int findPos(int[] arr, int n, int num) { |
案例三
题目
循环有序数组最小值
给定一个有序循环数组arr,返回arr中的最小值。有序循环数组是指,有序数组左边任意长度的部分放到右边去,右边的部分拿到左边来。比如数组[1,2,3,3,4],是有序循环数组,[4,1,2,3,3]也是。
分析
- 注意:当arr[L]=arr[M]=arr[R]时,只能通过遍历的方式进行查找。

实现
public int getMin(int[] arr, int n) { |
案例四
题目
最左原位
给定一个有序数组arr,其中不含有重复元素,请找到满足arr[i]==i条件的最左的位置,如果所有位置上的数都不满足条件,返回-1.
分析
- 定义res,记录最后一次发生arr[i]==i的位置
- 如果arr[0]>N-1,则arr[0]~arr[N-1]上的位置均大于N-1,返回-1
- 如果arr[N-1]<0,则arr[0]~arr[N-1]上的位置均小于0,返回-1
- 考虑位置M,如果arr[M]>M,在0~M-1的范围上继续二分查找;如果arr[M]<M,在M+1~N的范围上继续二分查找;如果arr[M]==M,用res记录当前的M值,之后再从0~M-1的范围继续二分搜索

实现
public int findPos(int[] arr, int n) { |
案例五
题目
完全二叉树计数
给定一棵完全二叉树的头节点head,返回这棵树的节点个数,如果完全二叉树的节点数为N,请实现时间复杂度低于O(N)的解法。
分析
- 普通遍历方式,时间复杂度O(N)。
- 最优解,时间复杂度O(logH^2),H是二叉树的高度。实质上是利用二分来确定右子树的最左节点的位置
- 首先找到二叉树最左的节点,统计完全二叉树的高度。
- 找到二叉树右子树的最左节点,如果头节点右子树的最左节点能到达最后一层,说明头节点左子树一定是一棵满二叉树,可以根据公式计算满二叉树的节点个数,再加上头节点。之后利用递归的方式求解右子树的节点个数。
- 如果头节点右子树的最左节点不能到达最后一层,说明头节点的右子树是一棵满二叉树,不过比左子树少一层,可以根据公式计算满二叉树的节点个数,再加上头节点。之后利用递归的方式求解左子树的节点个数。

实现
class TreeNode { |
案例六
题目
快速N次方
如何更快的求一个整数k的N次方。如果两个整数相乘并得到结果的时间复杂度为O(1),得到整数k的N次方的过程请实现时间复杂度为O(logN)的方法。
分析
- 普通方法,时间复杂度O(N)

- 二分查找,时间复杂度O(logN)

实现
public int getPower(int k, int N) { |
扩展
为什么要对
1000000007取模?
其实不止1e9+7,还有1e9+9和998244353。这三个数都是一个质数,同时小于$2^{30}$
- 所有模过之后的数在加法操作在int范围内不会溢出,即$a,b<2^{30}, a+b<2^{31}$
- 在乘法操作后在long long范围内不会溢出,即$ab<2^{60}$
- 对于$[1,p)$中的整数,可进行mod P乘法群意义下的除法,即可以使用扩展欧几里得算法求得$gcd(x,P)=ax+bP+1$中的a。