二分搜索算法
二分搜索算法
有序向量的二分搜索算法能够有效的加快查找的速度.从无序的 O(n)变为 有序的O(logn)演化.
二分法的实现
基础版
public int binarySearch(int[] nums, int target) {
int lo = 0, hi = nums.length;
while (lo < hi) {
int mi = lo + (hi - lo) / 2;
if (nums[mi] == target) {
return mi;
} else if (nums[mi] > target) {
hi = mi - 1;
} else {
//nums[mi] < target
hi = mi + 1;
}
}
return -1;
}
上面这种算法是我们最常见二分搜索实现.这种搜索只能返回当前有序队列中是否包含了target元素,同时数组元素个数不同,返回的rank也有可能不一样.这种实现的功能比较简单.例如,当希望向有序数组中插入一个新元素的 target的时候,希望找出插入的位置的时候这个时候这种实现就不够好了.
对基础版的改进
上面算法的实现中将有序数组分为两部分,逐步缩小空间,其中左右两边比较的次数不相同.转向左侧的比较有两次,转向右侧的比较有三次.而我们在计算mid的时候,是一分为二的.这里可以优化一下,在计算mid的时候偏向右边多点,这样使得左边区间多一些,右边区间少一些.这样就能够保证平衡.
语义清晰版本
对于二分查找,我们需要找出一些位置,以便于我们插入,或者当系统中有多个等值的时候,我们需要知道最大的或者最小的哪个.这样的查询才会有意义.
如下图:
在一个升序数组中,我们最希望确定的有四个rank.分别是第一个>= n
,第一个>n
,最后一个<n
,最后一个 <= n
的值.其中下面两个的rank可以通过上面两个的rank -1 获取到.
例如我们希望将target的元素插入到n后面,这个时候,这样我们可以通过二分查找找到最后一个>=n
rank,然后插入到后面一个的位置上.
我们只实现上面的两个第一个>=n
和 第一个>n
的rank.
第一个>=n的rank
public int binarySearchFirstGE(int[] nums, int target) {
/**
* 升序数组,找出 >= target的第一个的rank
*/
// nums[0,lo) < target <= nums[hi,n)
int lo = 0, hi = nums.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] < target) {
lo = mid + 1;
} else {
// nums[mid] >= target
hi = mid;
}
}
return lo;
}
如其中的注释,我们可以看到其中的不变形的证明.
第一个 >n的rank
public int binarySearchFirstG(int[] nums, int target) {
/**
* 升序数组,找出 > target的第一个的rank
*/
// 不变性 nums[0,lo) <= target < nums[hi,n) 最终 结果 nums[lo == hi] = FirstG
int lo = 0, hi = nums.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (target < nums[mid]) {
hi = mid;
} else {
// nums[mid] >= target
lo = mid + 1;
}
}
return lo;
}
二分法使用范例
剑指offer 68题
有序数组中找出第一个>= target的元素的下标.
这个题的做法和标准做法一样.
public int searchInsert(int[] nums, int target) {
/**
* 在升序数组中,找出 第一个>= target元素的位置.
* 找出第一个 > target的rank
*/
int lo = 0, hi = nums.length;
while (lo < hi) {
//不变量 nums[0,low) < target <= [high,n)
int mid = lo + (hi - lo) / 2;
if (nums[mid] < target) {
// [lo,mid+1) < target
lo = mid + 1;
} else {
// target<= nums[mid] target <= [mid,high)
hi = mid;
}
}
return lo;
}
剑指offer 8题
和大于等于 target 的最短子数组
public int minSubArrayLen(int target, int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
/**
* 找出 连续子数组的 的长度 >= target的最短的长度
* 使用 一个 数组记录以及记录前序和.
* 然后 记录 [hi - lo ]之间的差 其中 <= target的结果.
*/
int[] sums = new int[nums.length + 1];
for (int i = 1; i < sums.length; i++) {
sums[i] = sums[i - 1] + nums[i - 1];
}
int minLength = sums.length;
for (int i = 0; i < sums.length - 1; i++) {
int rank = binarySearch(sums, i + 1, sums.length, sums[i] + target);
if (rank >= sums.length) {
break;
}
minLength = Math.min(minLength, (rank - i));
}
return minLength == sums.length ? 0 : minLength;
}
/**
* @param nums
* @param lo
* @param hi
* @param target
* @return
*/
private int binarySearch(int[] nums, int lo, int hi, int target) {
/**
* 找出 第一个 >= target元素的下表
* [0,low) < target <= [high,n)
*/
int left = lo, right = hi;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
//[lo,mid+1)< target
left = mid + 1;
} else {
//nums[mid] >= target target <= [mid,high)
right = mid;
}
}
return right;
}
这个题的实现的关键点:
1.使用前缀和的方式来计算子序列的和.其中第一个位置是0.
2.依次寻找第一个 >=(sums[i] + target)的元素的rank
剑指offer 73题
求狒狒最慢的速度,在给定时间内能吃完香蕉.
public int minEatingSpeed(int[] piles, int h) {
/**
* 猴子吃香蕉:
*
* 不变量[0,lo) < speed <= [hi,n) 时间不够 < pipe <= 时间刚好多种多的
* 找出第一个 speed >= cal(h)的. speed 提升, 速度愉快.
* 最终 找到第一个 speed >= target的速度.
*
*/
int maxSpeed = Arrays.stream(piles).max().getAsInt();
int low = 1, high = maxSpeed + 1;
while (low < high) {
int speed = low + (high - low) / 2;
int costTime = calTime(speed, piles);
if (h < costTime) {
// speed < target 速度慢了.
low = speed + 1;
} else {
//speed >= target 速度快了.
high = speed;
}
}
return low;
}
private int calTime(int speed, int[] piles) {
int h = 0;
for (int i = 0; i < piles.length; i++) {
h += piles[i] / speed;
if (piles[i] % speed != 0) {
h++;
}
}
return h;
}
这个题是一个变种,最慢的速度是1,最快的速度是max(piles[i]).其中速度越大,消耗的时间越短.求出最小的speed 使得 hour>= target.
这个也可以用二分法来求解.
剑指offer 71题 按权重生成数
public class Solution {
/**
* 给定 一个 [1,3]
*/
private int total = -1;
private int[] pre = null;
public Solution(int[] w) {
pre = new int[w.length];
pre[0] = w[0];
total = w[0];
for (int i = 1; i < pre.length; i++) {
pre[i] = pre[i - 1] + w[i];
total += w[i];
}
}
/**
* [1,4]
*
* @return
*/
public int pickIndex() {
int picRank = (int) (Math.random() * this.total) + 1;
int rank = binarySearch(pre, picRank);
return rank;
}
/**
* 找出 nums中第一个 >= target的rank
*
* @param nums
* @param target
* @return
*/
private int binarySearch(int[] nums, int target) {
/**
* [0,lo) < target <= [hi,n)
* lo = hi ==> >= target
*/
int lo = 0, hi = nums.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (target <= nums[mid]) {
hi = mid;
} else {
//target > nums[mid]
lo = mid + 1;
}
}
return lo;
}
}
安权重分配随机数的方式:
首先记录下权重,然后使用前缀和.统计出每个数的位置.
然后生成一个随机数
.以加权结果为base.
在前缀和数组中找出第一个 >= 随机数
的下表rank.这个rank就是被选中的哪个值.
剑指offer 72 求一个数的平方根的题
如果有小数,将小数抹掉.例如:输入8,返回2.
public int mySqrt(int x) {
/**
* 求整数的平方根.
* 这里的实现方式是: 一个数是从 [0,x] 之间.
* 元素越大 ,平方越大.
* 求 [0,x]之间 ,最大的 i*i <= x ==> 求 最小的 k*k > x 值. 然后 返回 k-1
*
* 区间是 [0,lo) <= target < [hi,n)
* 最终 hi= lo为 > x的平方根 最小的值
* 其中可能会出现 上溢出的问题,这里需要讲
*/
long lo = 0;
long hi = (long)x + 1;
while (lo < hi) {
long mid = lo + (hi - lo) / 2;
if ((long)mid * mid <=(long)x) {
lo = mid + 1;
} else {
// (mid * mid) > x
hi = mid;
}
}
return (int)(lo - 1);
}
题目:这个也可以用一个二分查找来求解.
一个数组[0,x+1)当i越大的时候,i*i越大.求最大的i,使得i*i>=x
.
同时这个x可能会出现 越界.这里需要将所有的结果 适用 long来记录i的值.防止溢出.
剑指offer 69题.
求山峰型数组,其中山峰的位置.
public int peakIndexInMountainArray(int[] arr) {
/**
* 因为是一个山峰的数组所以:会出现两种情况
*/
int left = 1, right = arr.length - 2;
int result = 0;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] > arr[mid + 1]) {
result = mid;
//这里是下山
right = mid - 1;
} else {
//这里是上山
left = mid + 1;
}
}
return result;
}
这个题使用二分法:使用当前元素和后一个元素进行比较.如果是升序,说明在右侧.否则在左侧.