二分查找刷题套路
理论
- 每次划分完,一定是要缩小解空间的(这样就不会造成死循环)
- 需要注意:循环条件的写法 (start <= end, start + 1 < end)
- 取中间元素的写法: 统一: mid = start + ((end - start) >> 1);
- java中可以这么写: mid = (start + end) >>> 1;
- 为了可读性好:
mid = start + (end - start)/2
;
模板
模板1: low <= high
这种循环条件,保证二分的数组至少有一个元素。
int ans = -1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (target <= A[mid]) { // find first match
ans = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return ans;
模板2: low + 1 < high
始终保证二分时,至少有三个元素,这样不管怎么缩减空间,都不会造成死循环。
while (low + 1 < high) {
int mid = low + (high - low) / 2;
if (A[mid] < A[mid+1]) {
low = mid;
} else {
high = mid;
}
}
模板3: low < high
至少有两个元素才进行二分,结束条件就是只有一个元素了 low==high,或者 low > high
// 一定需要注意缩短空间的写法:(原因: [1,2] 取中点,倾向于左边,所以得到1)
while (low < high) {
int mid = low + (high - low) / 2;
if (xxx) { // remove left
low = mid + 1;
} else { // remove right part
high = mid;
}
}
模板的应用
- ① 在有序数组中,精确查找某个数,返回下标,不存在返回-1 【写法1】
- ② 在有序数组中,寻找第一个满足条件的位置,不存在返回-1 【写法1】
- 例如:在有序数组中,找到第一个 >= target 的元素位置 (例如:查找插入位置)
- 查找第一个等于target的元素位置
- ③ 在有序数组中,寻找最后一个满足条件的位置,不存在返回-1 【写法1】
- 在有序数组中,找到最后一个 < target 的元素位置
- 寻找最后一个 = target的位置
- ④ 在有序数组中,寻找最接近target的元素位置 【写法2】
- e.g. find peak
练习题
34. 在排序数组中查找元素的第一个和最后一个位置
class Solution { // time: O(log n), space: O(1)
int searchFirst(int[] nums, int target) {
int left = 0, right = nums.length - 1, ans = -1;
while (left <= right) {
int mid = (left + right) >>> 1;
if (nums[mid] == target) {
ans = mid;
right = mid - 1; // 重点是这句,相等之后,仍然往左继续找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return ans;
}
int searchLast(int[] nums, int target) {
int left = 0, right = nums.length - 1, ans = -1;
while (left <= right) {
int mid = (left + right) >>> 1;
if (nums[mid] == target) {
ans = mid;
left = mid + 1; // 重点是这句,相等之后,仍然往右继续找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return ans;
}
public int[] searchRange(int[] nums, int target) {
int left = searchFirst(nums, target);
int right = searchLast(nums, target);
return new int[] {left, right};
}
}
35. 搜索插入位置 - [好题] 写法1更好
需要注意下ans的初始化。不过跟上面找最后一个元素是一样的。
class Solution { // time: O(log n), space: O(1)
public int searchInsert(int[] nums, int target) {
if (nums == null) {
return -1;
}
int start = 0, end = nums.length - 1;
// 找到最后一个比 target 小的元素位置: ans
int ans = -1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] < target) { // 找到一个满足条件的,不要停。继续move
ans = mid;
start = mid + 1;
} else {
end = mid - 1;
}
}
return ans + 1;
}
}
28 · 搜索二维矩阵 - LintCode
这个题目,只能先按照第1列二分,找最后一个 <= target的位置,然后再在这一行进行二分。
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int m = matrix.length, n = matrix[0].length;
// find last index <= target in first column
int low = 0, high = m - 1;
int ans = -1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (matrix[mid][0] == target) {
return true;
} else if (matrix[mid][0] < target) {
ans = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
if (ans == -1) {
return false;
}
low = 0;
high = n - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (matrix[ans][mid] == target) {
return true;
} else if (matrix[ans][mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return false;
}
38 · 搜索二维矩阵(二) - LintCode
思路:这个题就是从右上角或者左下角开始,一次排除一行或一列。简单。思维有点急转弯。
74 · 第一个错误的代码版本 - LintCode
int firstBadVersion(int n) {
int low = 1, high = n;
int ans = 1;
// find first bad version in range [1, n] inclusive, answer must be exist
while (low <= high) {
int mid = low + (high - low) / 2;
if (isBadVersion(mid)) {
ans = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return ans;
}
162. 寻找峰值 - 非常好,写法2
思路:二分最后还剩下2个元素,只要有三个就继续二分。比较条件,就是判断mid 和 mid + 1的大小。
public int findPeakElement(int[] nums) {
if (nums == null || nums.length == 0) {
return -1;
}
int low = 0, high = nums.length - 1;
while (low + 1 < high) { // at least 3 elements between [low, high]
int mid = low + (high - low) / 2;
if (nums[mid] < nums[mid + 1]) {
low = mid;
} else {
high = mid;
}
}
return nums[low] > nums[high] ? low : high;
}
248 · Count of Smaller Number - LintCode
思路:很好的题目。从有序数组中找第一个大于等于target的元素。需要注意ans的初始值。
public List<Integer> countOfSmallerNumber(int[] a, int[] queries) {
List<Integer> ans = new ArrayList<>();
if (a == null || a.length == 0) {
for (int x : queries) {
ans.add(0);
}
return ans;
}
Arrays.sort(a);
for(int x : queries) {
int count = findFirstGreatOrEqual(a, x);
ans.add(count);
}
return ans;
}
private int findFirstGreatOrEqual(int[] a, int x) {
int left = 0, right = a.length - 1;
int ans = a.length; // 这里需要注意:表示while循环里没有任何匹配,也就是ans没有被赋值过。 ==> a[i] < x
while (left <= right) {
int mid = left + (right - left) / 2;
if (a[mid] >= x) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
33. 搜索旋转排序数组
思路:难题。
旋转数组,精确查找,返回下标。
经验:画图演示时,在直角坐标系下面画图,后面视频中就是这么画的,方便理解。
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
代码:
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = ((left + right) >>> 1);
if (target == nums[mid]) {
return mid;
}
// check [left, mid] is sorted
if (nums[left] <= nums[mid]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // [mid, right] is sorted
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
81. 搜索旋转排序数组 II
153. 寻找旋转排序数组中的最小值
154. 寻找旋转排序数组中的最小值 II
69. x 的平方根
这个题目本质是求一个y使得 y*y <= x. y尽可能大一点。 可以认为是在有序的数组中,找最后一个满足条件的值。
class Solution {
public int mySqrt(int x) { // time: O(log n)
if (x <= 1) {
return x;
}
int left = 1, right = x / 2, ans = 1;
while (left <= right) {
int mid = (left + right) >>> 1;
if (mid <= x / mid) { // mid*mid <= x 时 继续往右寻找
ans = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
return ans;
}
}