1.介绍
二分查找,也称折半查找(Binary Search),它是一种效率较高的查找方法,实现原理简单,但细节相对复杂的算法。关于二分查找,有个经典的理解,思路很简单,细节是魔鬼 。 二分查找的常用场景一般包括:寻找一个数、寻找左侧边界、寻找右侧边界。而细节,主要体现在,while循环中用 <
还是 <=
,mid
是否应该加1等。 下面从常用场景,结合 leetcode
题目,简单介绍下。
2. 寻找一个数
这是最简单的二分查找应用场景,即从有序数组中搜索一个目标数。
题目描述
给定一个n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1。
示例:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
题目解析
这个题目即是典型的二分查找,为有序数组中,查找目标值,我们定义 mid
为数组中点,即是比较 nums[mid]
与 tagert
大小:
如果 nums[mid] = target
,则下标 mid
即为要寻找的下标;
如果 nums[mid] > target
,则 target
只可能在下标 mid
的左侧;
如果 nums[mid] < target
,则 target
只可能在下标 mid
的右侧。
代码实现
int search(int* nums, int numsSize, int target) {
int l = 0;
int r = numsSize - 1;
while (l <= right) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
return mid; // 找到目标值,返回下标
else if (nums[mid] < target)
l = mid + 1;
else if (nums[mid] > target)
r = mid - 1;
}
return -1; //目标值不存在,返回-1
}
现在关于细节问题,做下分析理解:
2.1 为什么while循环里要用 <=
?
我们注意到,在初始化时,left
值为 0
,而 right
值为 numsSize - 1
,即最后一个元素的索引为 numsSize - 1
,相当于我们的搜索区间为 [left, right]
,两端都闭的区间。如果 right
初值为 numsSize
时,while
循环语句里需要用 <
作为条件判断。
2.2 为什么 left = mid + 1
和 right = mid - 1
?
如上文描述,我们的搜索区间为 [left, right]
,以mid为中点,逐步缩小查找范围,如果在索引 mid
里没有找到 target
,接下来就要搜索 [left, mid - 1]
和 [mid + 1, right]
区间范围内是否有目标值。
3.寻找左侧边界
题目描述<一>
我们对上面 leetcode 704题目做下修改,在有序数组中,存在元素相等的情况,找到数组中和target相等的 最左侧元素下标
示例:
输入: nums = [-1,0,3,5,9,9,12], target = 9
输出: 4
解释: 9 出现在 nums
中 最左侧下标为 4
题目解析 结合上面寻找一个数思路分析,在比较 nums[mid]
与 tagert
大小时,如果 nums[mid] = target
,下标 mid
即为要寻找的下标,此时不返回,收缩右侧边界接着找下一个r = mid - 1
,其他情况和寻找一个数实现相同。
代码实现
int search(int* nums, int numsSize, int target) {
int l = 0;
int r = numsSize - 1;
while (l <= right) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
r = mid - 1; // 找到目标值,收缩右侧边界
else if (nums[mid] < target)
l = mid + 1;
else if (nums[mid] > target)
r = mid - 1;
}
if (l < 0 || nums[l] != target)
return -1;
return l;
}
题目描述<二>
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
题目解析
l
和 r
指针不断向中间移动,进行二分,则有2种情况:
当前节点 比 r
节点的值小, 即 nums[mid] < nums[r]
,则 r
等于 mid
;
当前节点大于等于 r
节点的时候,即 nums[mid] >= nums[r]
,则让 l 等于 mid + 1
;
代码实现
int findMin(int* nums, int numsSize){
int l = 0;
int r = numsSize - 1;
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] < nums[r])
r = mid;
else
l = mid + 1;
}
return nums[l];
}
4.寻找右侧边界
我们对上面 leetcode 704
题目做下修改,在有序数组中,存在元素相等的情况,找到数组中和target相等的 最右侧元素下标
示例:
输入: nums = [-1,0,3,5,9,9,12], target = 9
输出: 5
解释: 9 出现在 nums
中最右侧下标为 5
题目解析 结合上面寻找一个数思路分析,在比较 nums[mid]
与 tagert
大小时,如果 nums[mid] = target
,下标 mid
即为要寻找的下标,此时不返回,收缩左侧边界接着找下一个l = mid + 1
,其他情况和寻找一个数实现相同
代码实现
int search(int* nums, int numsSize, int target){
int l = 0;
int r = numsSize - 1;
while (l <= right) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
l = mid + 1; // 找到目标值,收缩左侧边界
else if (nums[mid] < target)
l = mid + 1;
else if (nums[mid] > target)
r = mid - 1;
}
if (r < 0 || nums[r] != target)
return -1;
return r;
}
题目描述
给定一个按照升序排列的整数数组 nums
,和一个目标值 target
。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
题目解析
这道题目,要找到目标值在数组中的开始位置和结束位置,就是找到 左边界和右边界,可以先找左侧边界,再以左边界为起始点,找右边界。
代码实现
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
int *res = (int*)malloc(sizeof(int) * 2);
res[0] = -1;
res[1] = -1;
int left = 0;
int right = numsSize - 1;
*returnSize = 2;
/* 找左边界 */
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1;
else if(nums[mid] > target)
right = mid - 1;
else
right = mid - 1;
}
/* 以左边界,为起始,找右边界 */
if (left < numsSize && nums[left] == target) {
res[0] = left;
while (left < numsSize - 1) {
if (nums[left + 1] == target)
left++;
else
break;
}
res[1] = left;
}
return res;
}