前面算法系列文章有写过分治算法基本原理和实践,对于分治算法主要是理解递归的过程。二分法是分治算法的一种,相比分治算法会简单不少,由于少了递归的存在。css
在计算机科学中,二分查找算法(英语:binary search algorithm),也称折半搜索算法(英语:half-interval search algorithm)、对数搜索算法(英语:logarithmic search algorithm)[2],是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,若是中间元素正好是要查找的元素,则搜索过程结束;若是某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,并且跟开始同样从中间元素开始比较。若是在某一步骤数组为空,则表明找不到。这种搜索算法每一次比较都使搜索范围缩小一半。html
二分查找算法在状况下的复杂度是对数时间。二分查找算法使用常数空间,不管对任何大小的输入数据,算法使用的空间都是同样的。除非输入数据数量不多,不然二分查找算法比线性搜索更快,但数组必须事先被排序。尽管特定的、为了快速搜索而设计的数据结构更有效(好比哈希表),二分查找算法应用面更广。python
二分查找算法有许多中变种。好比分散层叠能够提高在多个数组中对同一个数值的搜索。分散层叠有效的解决了计算几何学和其余领域的许多搜索问题。指数搜索将二分查找算法拓宽到无边界的列表。二叉搜索树和B树数据结构就是基于二分查找算法的。算法
对二分法的概念了解后,下面来看一道示例:数组
已知一个长度为 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 ,它原来是一个升序排列的数组,并按上述情形进行了屡次旋转。请你找出并返回数组中的最小元素 。post
示例 1:学习
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次获得输入数组。编码
示例 2:url
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次获得输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次获得输入数组。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 中的全部整数 互不相同
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转
下面来看一下我写的一个失败版的答案,此时的我还没入门二分法:
class Solution { public int findMin(int[] nums) { int left = 0; int right = nums.length-1; while (left<=right) { int middle = left + (right -left)/2; if (nums[middle] > nums[left]) { left = middle + 1; }else { right = right-1; } } return nums[left]; } }
输入:[4,5,6,7,8,9,10,0,1,2,3]
输出:10
结果:0
能够看到结果是不对,那这里的问题是什么呢?都说失败是成功之母,咱们只有分析清楚为啥咱们的解法会存在问题,才能更好地明白二分法的精髓。
先从答案分析,这里输出 10,为啥会是 10。
看上面这张图,代码逻辑写的是 middle > left,那么 left = middle +1; 这个逻辑这么写是没有问题的。
接着看,当不知足 middle > left,说明 middle 处于最小值的右半部分,这时候咱们让 right--。那若是 right 就是最小值呢,这时候就会错过最小值。
还有若是 middle 是最大值呢?那么 left= middle +1 就是最小值,此时你再去计算 middle ,就直接把最小值错过了。好比输入数组:[5,6,7,8,9,0,1,2,3,4];
还要考虑一种特殊状况,若是此时只有两个元素了,有两种状况 [1,2],[2,1] ,这时候若是按照 right--,就会直接取到第一个元素。因此在 middle 和 left 相等的时候也要在作额外的判断。
完整版经过代码以下:
class Solution { public int findMin(int[] nums) { int left = 0; int right = nums.length-1; while (left<right) { int middle = left + (right -left)/2; if (nums[middle] > nums[left] && nums[middle] > nums[right]) { left = middle +1;
// 说明最小值就在最右边,此时处于只有两个元素的时候 } else if(middle == left && nums[left] > nums[right]) { left = right; } else { right = right-1; } } return nums[left]; } }
当你看到这段代码后,你懵逼了,这仍是二分法嘛,分析下来这么复杂。
那咱们来看下官方给的代码:
class Solution { public int findMin(int[] nums) { int low = 0; int high = nums.length - 1; while (low < high) { int pivot = low + (high - low) / 2;
// 最小值必定是在和 high 在一个区间内的,因此这里要判断 pivot 和 high 的大小关系,不能去判断和 low 的关系 if (nums[pivot] < nums[high]) { high = pivot; } else { low = pivot + 1; } } return nums[low]; } }
是否是以为官方代码简洁易懂。
那为啥这两个解法的代码会差这么多,答案在于 middle 究竟是应该和 left 比较,仍是应该和 right 比较。
这也说明了方向的选择的重要性。但是咱们应该怎么选择呢。这个主要是在分析问题的时候要想清楚。我的以为也能够这么理解:
本题是找最小值的。从最小值到最右端,这其实就是单调递增的,所以咱们只要关注右半部分,抛弃左半部分就好。
那么本题错误缘由就是跟左边进行比较,你再怎么找,最后得出值都不在这一部分上,就致使你得添加不少额外的逻辑来确保能够找到值。
PS:对于二分法要时刻关注只有两个元素的状况。这时候 middle = left。这时候注意 left 和 right 之间的关系。
经过这道题目相信你们已经对二分法有必定的认识了。
二分查找的思想就一个:逐渐缩小搜索区间。 以下图所示,它像极了「双指针」算法,left 和 right 向中间走,直到它们重合在一块儿:
根据看到的中间位置的元素的值 nums[mid] 能够把待搜索区间分为三个部分:
这样就能够得到二分法基本模板:
class Solution { public int search(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 确保 left 和 right 都在数组可取范围内 while (left <= right) { // < 仍是 <= 按照本身的习惯便可 int mid = left + (right -left)/2; if (nums[mid] == target) { // 找到结果就返回 return mid; }else if(nums[mid] > target) { right = mid-1; } else { left = mid +1; } }
// 退出循环就说明没找到 return -1; } }
虽然咱们看到的写法有不少,但思想就这一个;为何老是有朋友以为二分难?由于有不少二分的写法,虽然都对,可是对于新手朋友们来讲有必定干扰,由于不一样的写法其实对应着不一样的前提和应用场景,比起套用模板,审题、练习和思考更重要。「二分查找」就几行代码,彻底不须要记忆,也不该该用记忆的方式解题.
下面解释一些细节:
一、模板的结束条件是 left <= right ,也就是结果必定是在 while 里面找到的。不然就是没找到。
二、有些学习资料上说 while (left < right) 表示区间是 [left..rihgt) ,为何你这里是 [left..rihgt]?
区间的右端点究竟是开仍是闭,彻底由编写代码的人决定,不须要固定。主要仍是看你 left 和 right 的取值。 若是 right = nums.length ; 那么其实 right 这个位置是取不到的,也就是开区间了。因此开闭就是看点位能不能取到。
三、有些学习资料给出了三种模板,例如「力扣」推出的 LeetBook 之 「二分查找专题」,应该如何看待它们?
回答:三种模板其实区别仅在于退出循环的时候,区间 [left..right] 里有几个数。
while (left <= right) :退出循环的时候,right 在左,left 在右,区间为空区间,因此要讨论返回 left 和 right;
while (left < right) :退出循环的时候,left 与 right 重合,区间里只有一个元素,这一点是咱们很喜欢的;
while (left + 1 < right) :退出循环的时候,left 在左,right 在右,区间里有 2 个元素,须要编写专门的逻辑。这种写法在设置 left 和 right 的时候不须要加 1 和减 1。
看似简化了思考的难度,但实际上屏蔽了咱们应该且彻底能够分析清楚的细节。退出循环之后必定要讨论返回哪个,也增长了编码的难度。
我我的的经验是:
while (left <= right) 用在要找的数的性质简单的时候,把区间分红三个部分,在循环体内就能够返回;
while (left < right) 用在要找的数的性质复杂的时候,把区间分红两个部分,在退出循环之后才能够返回;
彻底不用 while (left + 1 < right) ,理由是不会使得问题变得更简单,反而很累赘。
不少题目在二分法的基础上有变化,咱们要学会灵活变化。还要理解题意。
示例:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。若是目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5 输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2 输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7 输出: 4
示例 4:
输入: nums = [1,3,5,6], target = 0 输出: 0
示例 5:
输入: nums = [1], target = 0 输出: 0
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
为无重复元素的升序排列数组-104 <= target <= 104
class Solution { public int searchInsert(int[] nums, int target) { int left =0; int right = nums.length -1; while (left<=right) { int mid = left + (right-left)/2; if (nums[mid] == target) { return mid; } if (nums[mid]>target) { right = mid-1; }else { left = mid+1; } } // 没找到,那么 left 就是它所处的位置 return left; } }
注意一点:二分法只是用于有序数组,若是是无序的,此时是没法肯定边界的,这时候咱们就须要本身创造条件,找到数组的有序部分。
好比下面两道,你们能够本身找二分法题目去练习。
关于二分法的理论就讲到这里了,剩下的就是靠你们多多练习了。
参考文章
https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/