携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
二分查找是一种效率较高的查找方法,一般在有序数组中查找元素时,会使用到二分查找方法。
查找过程大致为:
- 将查找元素与查找集合的中间元素比较,如果相等,则查找成功;否则:
- 如果查找元素比查找集合的中间元素大,那么我们到查找集合的右半部分;
- 如果查找元素比查找集合的中间元素小,那么我们到查找集合的左半部分
重复以上过程,直到我们查找到元素;或者将集合无法进一步缩小,表示集合中没有待查找的元素。
二分查找的代码模板:
// 查找指定元素的数组下标
public int binarySearch(int[] nums, int target){
if(nums == null || nums.length == 0) {
return -1;
}
int left = 0, right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == target){
return mid;
}
else if(nums[mid] < target) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
return -1;
}
二分查找的示意图:
69. x 的平方根 - 简单
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例:
输入:x = 4
输出:2
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
题解:
我们声明两个变量
l = 0,r = x,求[l,r]区间的中点mid。如果中点
mid的平方等于x,那么直接返回结果;如果
mid的平方值小于x,那么到右半区间查找;如果
mid的平方值大于x,那么到左半区间查找。需要注意使用乘法可能会整形溢出,有两种解决方法:
- 使用
long类型- 将乘法转换为除法
代码:
public int mySqrt(int x) {
if (x == 0) {
return 0;
}
int l = 1, r = x;
while (l <= r) {
int mid = l+(r-l)/2;
int sqrt = x / mid;
if (mid == sqrt) {
return mid;
}else if (mid < sqrt) {
l = mid+1;
}else {
r = mid-1;
}
}
return r;
}
1351. 统计有序矩阵中的负数 - 简单
给你一个 m * n 的矩阵 grid,矩阵中的元素无论是按行还是按列,都以非递增顺序排列。 请你统计并返回 grid 中 负数 的数目。
示例:
输入:grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]]
输出:8
解释:矩阵中共有 8 个负数。
题解:
我们要在二维数组中统计负数的个数,二维数组的元素又是一个降序数组,因此我们可以考虑使用二分查找。
我们遍历二维数组中的每个一维数组,对其统计。
对于一维数组,我们使用二分查找,如果中间元素是非负数,我们往右边区间搜索。
如果中间元素是负数,我们往左边区间搜索。
❗❗ 注意:这里我们是不断的搜索,不断地缩小区间,使得区间最后缩小在第一个负数处,我们知道一维数组的元素个数,那么我们也就知道了非负数的个数和负数的个数。
代码:
class Solution {
public int countNegatives(int[][] grid) {
int n = grid.length, m = grid[0].length;
int count = 0;
for (int i = 0; i < n; i++) {
count += getCount(grid[i], m);
}
return count;
}
private int getCount(int[] nums, int m) {
int l = 0, r = m-1;
// 确定第一个负数的出现位置
while (l <= r) {
int mid = l + (r-l)/2;
if (nums[mid] >= 0) { // 非负数,我们往右半区间查找
l = mid+1;
}else { // 负数,往左半区间查找
r = mid-1;
}
}
// 返回负数的个数
return m-l;
}
}
34. 在排序数组中查找元素的第一个和最后一个位置 - 中等
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
题解:
从升序数组中查找目标值,考虑用二分查找。
我们分两次二分查找,第一次查找目标元素的开始出现位置,第二次查找目标元素的最后出现位置。
如果集合中间的元素大于等于目标值,往左区间找;反之往右区间找。这样我们就能够找到目标值的第一次出现位置,返回右边界即可。
如果集合中间的元素小于等于目标值,往右区间找;反之往左区间找。这样我们就能够找到目标值的最后出现位置,返回左边界-1即可,因为搜索结束后,结果刚好在目标值右边一个位置。
❗❗ 需要注意,我们搜索目标值第一次出现的位置时,可能会返回一个无效值,即
nums.length,表示数组中并没有该元素。同理,搜索最后出现位置也有可能返回一个无效值。
针对这种情况,我们只需判断返回的第一次出现位置的值是否等于
nums.length或者该位置的值是否是target判断是否有目标值。
代码:
public int[] searchRange(int[] nums, int target) {
int first = firstIndex(nums, target);
int last = lastIndex(nums, target);
if (first >= nums.length || nums[first] != target) {
return new int[]{-1,-1};
}
return new int[]{first, last};
}
public int firstIndex(int[] nums, int target) {
int l = 0, r = nums.length;
while (l < r) {
int mid = (r-l)/2+l;
if (nums[mid] >= target) {
r = mid;
}else {
l = mid+1;
}
}
return r;
}
public int lastIndex(int[] nums, int target) {
int l = 0, r = nums.length;
while (l < r) {
int mid = (r-l)/2+l;
if (nums[mid] <= target) {
l = mid+1;
}else {
r = mid;
}
}
return l-1;
}
81. 搜索旋转排序数组 II - 中等
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
你必须尽可能减少整个操作步骤。
示例:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
题解:
数组是部分有序的,可能不会想到可以用二分查找解决。
- 确定查找区间的中点
mid,如果是目标值,返回结果- 否则,我们可以区间为左区间
[l,mid],右区间[mid+1,r]- 判断哪个区间是升序的,如果
nums[mid] > nums[l],那么左区间是升序的,否则右区间是升序的。也有可能两个区间都是升序的。- 如果左区间升序,那么判断
target是否会在该区间内,如果可能在该区间内,我们在左半区间进行下一轮搜索,将搜索集合缩小为左半区间- 如果右区间升序,那么判断
target是否会在该区间内,如果可能在该区间内,我们在右半区间进行下一轮搜索,将搜索集合缩小为右半区间需要注意,由于数组中会存在重复元素,我们要判断区间是否升序时,是通过
nums[mid] > nums[l]判断的,但是这两个值可能相等,此时我们可以通过移动 l 缩小集合来规避这种情况。
代码:
public boolean search(int[] nums, int target) {
int l = 0, r = nums.length-1;
while (l <= r) {
int mid = (r-l)/2+l;
if (nums[mid] == target) {
return true;
}
if (nums[l] == nums[mid]) {
l++;
}else if (nums[l] < nums[mid]) { // 左区间是否升序
if (target >= nums[l] && target < nums[mid]) {
r = mid-1;
}else {
l = mid+1;
}
}else { // 右区间升序
if (target > nums[mid] && target <= nums[r]) {
l = mid+1;
}else {
r = mid-1;
}
}
}
return false;
}
154. 寻找旋转排序数组中的最小值 II - 中等
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
示例:
输入:nums = [1,3,5]
输出:1
输入:nums = [2,2,2,0,1]
输出:0
题解:
本题解法与 81. 搜索旋转排序数组 II 类似。
找到升序的半区间,然后判断区间内的左边界元素(该区间的最小值)是否比我们之前找到的更小。
- 首先确定中点
mid,如果mid与集合左边界l元素相等,那么更新最小值ans并移动l指针缩小集合范围,进行新的一轮搜索;- 如果左半区间有序,那么更新最小值
ans为左边界元素- 如果右半区间有序,那么更新最小值
ans为nums[mid]
代码:
public int findMin(int[] nums) {
int l = 0, r= nums.length-1;
int ans = Integer.MAX_VALUE;
while (l <= r) {
int mid = (r-l)/2 + l;
if (nums[l] == nums[mid]) {
ans = Math.min(ans, nums[l]);
l++;
}else if (nums[l] < nums[mid]) { // 如果左区间有序
ans = Math.min(nums[l], ans);
l = mid+1;
}else { // 如果右区间有序
ans = Math.min(nums[mid], ans);
r = mid-1;
}
}
return ans;
}
540. 有序数组中的单一元素 - 中等
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。
实例:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
题解:
题目要求我们使用 O(log n) 时间复杂度和 O(1) 空间复杂度,对于 O(log n) 时间复杂度,并且数组是有序的,我们很容易想到二分查找,可以尝试解决看。
我们可以将数组中的相邻两个元素当成一个元素来看待,从而进行二分查找。
- 把两个元素当成一个元素看待,那么数组的长度
len也要减半- 确定集合中点
mid,如果当前元素相等,那么单一元素只可能出现在右半区间,往右半区间查找- 否则,往左半区间查找
❗❗ 需要注意中点只有一个元素的情况,例如
[1,1,2]。
代码:
public int singleNonDuplicate(int[] nums) {
int len = nums.length/2+1;
int l = 0, r = len-1;
while (l <= r) {
int mid = (r-l)/2 + l;
int n1 = nums[mid*2];
// 需要注意当前遍历只有一个元素的情况
int n2 = mid*2+1 < nums.length ? nums[mid*2+1] : -n1;
if (n1 != n2) {
r = mid-1;
}else {
l = mid+1;
}
}
return nums[l*2];
}