须要更多算法动图详解,能够微信搜索[袁厨的算法小屋]java
今天给你们带来的是二分查找及其变种的总结,你们必定要看到最后呀,用心满满,废话很少说,让导演帮咱们把镜头切到袁记菜馆吧!程序员
袁记菜馆内。。。。算法
店小二:掌柜的,您进货回来了呀,哟!今天您买这鱼挺大呀!数组
袁厨:那是,这是我今天从我们江边买的,以前一直去菜市场买,那里的老贵了,你猜猜我今天买的多少钱一条。微信
店小二:以前的鱼,30个铜板一条,今天的我猜26个铜板。3d
袁厨:贵了。指针
店小二:还贵呀!那我猜20个铜板!code
袁厨:仍是贵了。blog
店小二:15个铜板。排序
袁厨:便宜了
店小二:18个铜板
袁厨:恭喜你猜对了
上面的例子就用到了咱们的二分查找思想,若是你玩过相似的游戏,那二分查找理解起来确定很轻松啦,下面咱们一块儿征服二分查找吧!
二分查找也称折半查找(Binary Search),是一种在有序数组中查找某一特定元素的搜索算法。咱们能够从定义可知,运用二分搜索的前提是数组必须是有序的,这里须要注意的是,咱们的输入不必定是数组,也能够是数组中某一区间的起始位置和终止位置
经过上面二分查找的定义,咱们知道了二分查找算法的做用及要求,那么该算法的具体执行过程是怎样的呢?
下面咱们经过一个例子来帮助咱们理解。咱们须要在 nums 数组中,查询元素 8 的索引
int[ ] nums = {1,3,4,5,6,8,12,14,16}; target = 8
(1)咱们须要定义两个指针分别指向数组的头部及尾部,这是咱们在整个数组中查询的状况,当咱们在数组
某一区间进行查询时,能够输入数组,起始位置,终止位置进行查询。
(2)找出mid,该索引为 mid =(left + right)/ 2,可是这样写有可能溢出,因此咱们须要改进一下写成
mid = left +(right - left)/ 2 或者 left + ((right - left ) >> 1) 二者做用是同样的,都是为了找到两指针的中
间索引,使用位运算的速度更快。那么此时的 mid = 0 + (8-0) / 2 = 4
(3)此时咱们的 mid = 4,nums[mid] = 6 < target,那么咱们须要移动咱们的 left 指针,让left = mid + 1,下次则能够在新的 left 和 right 区间内搜索目标值,下图为移动前和移动后
(4)咱们须要在 left 和 right 之间计算 mid 值,mid = 5 + (8 - 5)/ 2 = 6 而后将 nums[mid] 与 target 继续比较,进而决定下次移动left 指针仍是 right 指针,见下图
(5)咱们发现 nums[mid] > target,则须要移动咱们的 right 指针, 则 right = mid - 1;则移动事后咱们的 left 和 right 会重合,这里是咱们的一个重点你们须要注意一下,后面会对此作详细叙述。
(6)咱们须要在 left 和 right 之间继续计算 mid 值,则 mid = 5 +(5 - 5)/ 2 = 5 ,见下图,此时咱们将 nums[mid] 和 target 比较,则发现两值相等,返回 mid 便可 ,若是不相等则跳出循环,返回 -1。
二分查找的执行过程以下
1.从已经排好序的数组或区间中,取出中间位置的元素,将其与咱们的目标值进行比较,判断是否相等,若是相等
则返回。
2.若是 nums[mid] 和 target 不相等,则对 nums[mid] 和 target 值进行比较大小,经过比较结果决定是从 mid
的左半部分仍是右半部分继续搜索。若是 target > nums[mid] 则右半区间继续进行搜索,即 left = mid + 1; 若
target < nums[mid] 则在左半区间继续进行搜索,即 right = mid -1;
动图解析
下面咱们来看一下二分查找的代码,能够认真思考一下 if 语句的条件,每一个都没有简写。
public static int binarySearch(int[] nums,int target,int left, int right) { //这里须要注意,循环条件 while (left <= right) { //这里须要注意,计算mid int mid = left + ((right - left) >> 1); if (nums[mid] == target) { return mid; }else if (nums[mid] < target) { //这里须要注意,移动左指针 left = mid + 1; }else if (nums[mid] > target) { //这里须要注意,移动右指针 right = mid - 1; } } //没有找到该元素,返回 -1 return -1; }
二分查找的思路及代码已经理解了,那么咱们来看一下实现时容易出错的地方
1.计算 mid 时 ,不能使用 (left + right )/ 2,不然有可能会致使溢出
2.while (left < = right) { } 注意括号内为 left <= right ,而不是 left < right ,咱们继续回顾刚才的例子,若是咱们设置条件为 left < right 则当咱们执行到最后一步时,则咱们的 left 和 right 重叠时,则会跳出循环,返回 -1,区间内不存在该元素,可是不是这样的,咱们的 left 和 right 此时指向的就是咱们的目标元素 ,可是此时 left = right 跳出循环
3.left = mid + 1,right = mid - 1 而不是 left = mid 和 right = mid。咱们思考一下这种状况,见下图,当咱们的target 元素为 16 时,而后咱们此时 left = 7 ,right = 8,mid = left + (right - left) = 7 + (8-7) = 7,那若是设置 left = mid 的话,则会进入死循环,mid 值一直为7 。
下面咱们来看一下二分查找的递归写法
public static int binarySearch(int[] nums,int target,int left, int right) { if (left <= right) { int mid = left + ((right - left) >> 1); if (nums[mid] == target) { //查找成功 return mid; }else if (nums[mid] > target) { //新的区间,左半区间 return binarySearch(nums,target,left,mid-1); }else if (nums[mid] < target) { //新的区间,右半区间 return binarySearch(nums,target,mid+1,right); } } //不存在返回-1 return -1; }
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。若是目标值不存在于数组中,返回它将会被按顺序插入的位置。
你能够假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
这个题目彻底就和我们的二分查找同样,只不过有了一点改写,那就是将我们的返回值改为了 left,具体实现过程见下图
class Solution { public int searchInsert(int[] nums, int target) { int left = 0, right = nums.length-1; //注意循环条件 while (left <= right) { //求mid int mid = left + ((right - left ) >> 1); //查询成功 if (target == nums[mid]) { return mid; //右区间 } else if (nums[mid] < target) { left = mid + 1; //左区间 } else if (nums[mid] > target) { right = mid - 1; } } //返回插入位置 return left; } }
上面咱们说了如何使用二分查找在数组或区间里查出特定值的索引位置。可是咱们刚才数组里面都没有重复值,查到返回便可,那么咱们思考一下下面这种状况
此时咱们数组里含有多个 5 ,咱们查询是否含有 5 能够很容易查到,可是咱们想获取第一个 5 和 最后一个 5 的位置应该怎么实现呢?哦!咱们可使用遍历,当查询到第一个 5 时,咱们设立一个指针进行定位,而后到达最后一个 5 时返回,这样咱们就能求的第一个和最后一个五了?由于咱们这个文章的主题就是二分查找,咱们可不能够用二分查找来实现呢?固然是能够的。
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
若是数组中不存在目标值 target,返回 [-1, -1]。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
这个题目很容易理解,咱们在上面说了如何使用遍历解决该题,可是这个题目的目的就是让咱们使用二分查找,咱们来逐个分析,先找出目标元素的下边界,那么咱们如何找到目标元素的下边界呢?
咱们来重点分析一下刚才二分查找中的这段代码
if (nums[mid] == target) { return mid; }else if (nums[mid] < target) { //这里须要注意,移动左指针 left = mid + 1; }else if (nums[mid] > target) { //这里须要注意,移动右指针 right = mid - 1; }
咱们只需在这段代码中修改便可,咱们再来剖析一下这块代码,nums[mid] == target 时则返回,nums[mid] < target 时则移动左指针,在右区间进行查找, nums[mid] > target时则移动右指针,在左区间内进行查找。
那么咱们思考一下,若是此时咱们的 nums[mid] = target ,可是咱们不能肯定 mid 是否为该目标数的左边界,因此此时咱们不能够返回下标。例以下面这种状况。
此时 mid = 4 ,nums[mid] = 5,可是此时的 mid 指向的并非第一个 5,因此咱们须要继续查找 ,由于咱们要找
的是数的下边界,因此咱们须要在 mid 的值的左区间继续寻找 5 ,那咱们应该怎么作呢?咱们只需在
target <= nums[mid] 时,让 right = mid - 1便可,这样咱们就能够继续在 mid 的左区间继续找 5 。是否是听着有点绕,咱们经过下面这组图进行描述。
其实原理很简单,就是咱们将小于和等于合并在一块儿处理,当 target <= nums[mid] 时,咱们都移动右指针,也就是 right = mid -1,还有一个须要注意的就是,咱们计算下边界时最后的返回值为 left ,当上图结束循环时,left = 3,right = 2,返回 left 恰好时咱们的下边界。咱们来看一下求下边界的具体执行过程。
动图解析
计算下边界代码
int lowerBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { //这里须要注意,计算mid int mid = left + ((right - left) >> 1); if (target <= nums[mid]) { //当目标值小于等于nums[mid]时,继续在左区间检索,找到第一个数 right = mid - 1; }else if (target > nums[mid]) { //目标值大于nums[mid]时,则在右区间继续检索,找到第一个等于目标值的数 left = mid + 1; } } return left; }
计算上边界时算是和计算上边界时条件相反,
计算下边界时,当 target <= nums[mid] 时,right = mid -1;target > nums[mid] 时,left = mid + 1;
计算上边界时,当 target < nums[mid] 时,right = mid -1; target >= nums[mid] 时 left = mid + 1;恰好和计算下边界时条件相反,返回right。
计算上边界代码
int upperBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { //求mid int mid = left + ((right - left) >> 1); //移动左指针状况 if (target >= nums[mid]) { left = mid + 1; //移动右指针状况 }else if (target < nums[mid]) { right = mid - 1; } } return left; }
题目完整代码
class Solution { public int[] searchRange (int[] nums, int target) { int upper = upperBound(nums,target); int low = lowerBound(nums,target); //不存在状况 if (upper < low) { return new int[]{-1,-1}; } return new int[]{low,upper}; } //计算下边界 int lowerBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { //这里须要注意,计算mid int mid = left + ((right - left) >> 1); if (target <= nums[mid]) { //当目标值小于等于nums[mid]时,继续在左区间检索,找到第一个数 right = mid - 1; }else if (target > nums[mid]) { //目标值大于nums[mid]时,则在右区间继续检索,找到第一个等于目标值的数 left = mid + 1; } } return left; } //计算上边界 int upperBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + ((right - left) >> 1); if (target >= nums[mid]) { left = mid + 1; }else if (target < nums[mid]) { right = mid - 1; } } return right; } }
咱们在上面的变种中,描述了如何找出目标元素在数组中的上下边界,而后咱们下面来看一个新的变种,如何从数组或区间中找出第一个大于或最后一个小于目标元素的数的索引,例 nums = {1,3,5,5,6,6,8,9,11} 咱们但愿找出第一个大于 5的元素的索引,那咱们须要返回 4 ,由于 5 的后面为 6,第一个 6 的索引为 4,若是但愿找出最后一个小于 6 的元素,那咱们则会返回 3 ,由于 6 的前面为 5 最后一个 5 的索引为 3。好啦题目咱们已经了解,下面咱们先来看一下如何在数组或区间中找出第一个大于目标元素的数吧。
找出第一个大于目标元素的数,大概有如下几种状况
1.数组包含目标元素,找出在他后面的第一个元素
2.目标元素不在数组中,数组内的部分元素大于它,此时咱们须要返回第一个大于他的元素
3.目标元素不在数组中,且数组中的全部元素都大于它,那么咱们此时返回数组的第一个元素便可
4.目标元素不在数组中,且数组中的全部元素都小于它,那么咱们此时没有查询到,返回 -1 便可。
既然咱们已经分析完全部状况,那么这个题目对我们就没有难度了,下面咱们描述一下案例的执行过程
nums = {1,3,5,5,6,6,8,9,11} target = 7
上面的例子中,咱们须要找出第一个大于 7 的数,那么咱们的程序是如何执行的呢?
上面的例子咱们已经弄懂了,那么咱们看一下,当 target = 0时,程序应该怎么执行呢?
OK!咱们到这一步就能把这个变种给整的明明白白的了,下面咱们看一哈程序代码吧,也是很是简单的。
public static int lowBoundnum(int[] nums,int target,int left, int right) { while (left <= right) { //求中间值 int mid = left + ((right - left) >> 1); //大于目标值的状况 if (nums[mid] > target) { //返回 mid if (mid == 0 || nums[mid-1] <= target) { return mid; } else{ right = mid -1; } } else if (nums[mid] <= target){ left = mid + 1; } } //全部元素都小于目标元素 return -1; }
经过上面的例子咱们应该能够彻底理解了那个变种,下面咱们继续来看如下这种状况,那就是如何找到最后一个小于目标数的元素。仍是上面那个例子
nums = {1,3,5,5,6,6,8,9,11} target = 7
查找最后一个小于目标数的元素,好比咱们的目标数为 7 ,此时他前面的数为 6,最后一个 6 的索引为 5,此时咱们返回 5 便可,若是目标数元素为 12,那么咱们最后一个元素为 11,仍小于目标数,那么咱们此时返回 8,便可。这个变种其实算是上面变种的相反状况,上面的会了,这个也彻底能够搞定了,下面咱们看一下代码吧。
public static int upperBoundnum(int[] nums,int target,int left, int right) { while (left <= right) { int mid = left + ((right - left) >> 1); //小于目标值 if (nums[mid] < target) { //看看是否是当前区间的最后一位,若是当前小于,后面一位大于,返回当前值便可 if (mid == right || nums[mid+1] >= target) { return mid; } else{ left = mid + 1; } } else if (nums[mid] >= target){ right = mid - 1; } } //没有查询到的状况 return -1; }
哎嘛写着写着咋就那么多了,太长了你们就不爱看啦,剩下的就放在下篇吧,我们下篇见呀!
若是这篇文章对您有一丢丢帮助的话,或者是能感觉到这篇文章的用心的话,那么感谢您的点赞,在看,转发呀,这样我就满血复活啦。
我是袁厨,一个酷爱用动图解算法的年轻人,一个酷爱作饭的程序员,一个想和你一块儿进步的小老弟。