九章算法01:二分法
二分法第一重境界: 套模板
public class Solution {
/**
* @param nums an integer array sorted in ascending order
* @param target an integer
* @return an integer
*/
public int findPosition(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int start = 0, end = nums.length - 1;
while (start + 1 < end) { // 要点1
int mid = start + (end - start) / 2; // 要点2
if (nums[mid] == target) { // 要点3
end = mid; // 找第一个出现位置时 // 要点4
// or end = start 找最后一个出现位置时
} else if (nums[mid] < target) { // 要点3
start = mid;
// or start = mid + 1
} else { // 要点3
end = mid;
// or end = mid - 1
}
}
if (nums[start] == target) { // 要点4
return start;
}
if (nums[end] == target) {
return end;
}
return -1;
}
}
上面模板,有四点需要注意:
-
while循环的条件是
start + 1 < end
,这样写是为了避免死循环: 循环体内start
和end
永不相邻,导致mid
不至于跟start
或end
之一重合,造成bug.考虑到一个情况:
start == end+1
,此时若要求推出条件为start == end
,则根据后面的代码,永远不可能退出. -
取中点的操作
mid = start + (end - start) / 2
,这个是为了防止大数相加溢出. -
将
==
,>
,<
三种情况分开来写,不要合并.因为写完之前,没人知道这三种情况是否会有些许的不同.另外在
<
和>
两种情况下,适当放宽条件,将下个区间起始位置指定在不可能是答案的end
上,在不降低时间复杂度的同时减少了bug发生的可能. -
在循环中不进行任何返回,循环的作用是缩小区间,将答案限制在我们可以数的过来的两个值上.
另外根据我们要找的具体条件,这个模板还要在两个地方进行改写.
- 若题目要求找到任何一个出现位置即可,我们代码可以直接运行.
- 若题目要求找到第一个出现位置时: 16行
==
情况下区间左缩,且27行先判断start
在判断end
. - 若题目要求找到最后一个出现位置时: 16行
==
情况下区间右缩,且27行先判断end
在判断start
.
上面一种模板是对应于后文
OOXX
情况的,也就是没有明确分界点的情况,如果有准确的分界点target
的话,也可以用while (start <= end)
且在while循环中直接return,但这样的话就需要把子区间转移的条件写的准确一些,防止死循环(start=end+1
,end=mid-1
).
public class Solution {
/**
* @param nums an integer array sorted in ascending order
* @param target an integer
* @return an integer
*/
public int findAnyPosition(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int start = 0, end = nums.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
start = mid + 1
} else {
end = mid - 1
}
}
return -1;
}
}
题目:
二分法第二重境界: 找OOXX
所谓OOXX的造型,就是把所有小于target
的项标记为O
,大于等于target
的项标记为X
,然后去找第一个X
即可.
题目:
-
278. First Bad Version: 将good version视为
O
,将bad version视为X
. -
153. Find Minimum in Rotated Sorted Array:
对于旋转有序数组
nums
,两个threshold分别为nums[0]
和nums[-1]
,应该取哪一个作为区分O
和X
的threshold呢?应该取
nums[-1]
作为threshold: 将nums[i]>nums[-1]
视为O
;将nums[i]<=nums[-1]
视为X
.(若取nums[0]
作为threshold的话,就无法满足单调递增数组的情况,单调递增数组也是旋转有序数组).对于154. Find Minimum in Rotated Sorted Array II这道题,有一个特点在于原数组中的数据不是严格递增(相邻的可能会相等).这种非严格性在一般情况下不会产生问题,但是在旋转数组的两侧会产生问题:当
nums[start]==nums[end]
时,难以确定O
和X
的分界依据,因此需要我们在程序开始时将start
向右划过直到nums[start]<numsend]
,就可以按照153. Find Minimum in Rotated Sorted Array来做. -
852. Peak Index in a Mountain Array:
根据比较
nums[i]
和nums[i+1]
的关系来区分O
和X
:- 取
nums[i]<nums[i+1]
为O
- 取
nums[i]>nums[i+1]
为X
- 取
二分法第二重境界: 二分位置
在这类题目中,无法形成OOXX
的造型,但是仍然能够根据具体情况硬核分析出保留哪一半,去掉哪一半.
-
162. Find Peak Element: 在一个有多个峰的数组中找到任意一个峰
这道题目不能构成
OOXX
的造型,然而可以根据mid
所处的位置来具体分析应该留下哪一半作为解:造型1 造型2 造型3 造型4 - 造型1中,
mid
落在了峰上,直接返回即可. - 造型2中,
mid
落在了谷上,两边都有峰,留哪一边都行. - 造型3中,
mid
右侧必有一峰,留右边. - 造型4中,
mid
左侧必有一峰,留左边.
- 造型1中,
-
33. Search in Rotated Sorted Array: 在循环有序数组中查询
这道题中的数组不严格有序,不能直接二分查找,可以仿效153. Find Minimum in Rotated Sorted Array的思路,先找两段数组的分界点,再在两段有序数组上分别进行二分查找.
但是如果题目要求只进行一次二分查找的话,就要根据
mid
所处的位置来具体分析应该留下哪一半作为解:造型1 造型2 通过比较
nums[mid]
和nums[-1]
值的大小关系,可以确定mid
具体位于哪个区间,然后根据target
值所在区间来判断留下哪一半.