一文带你攻克二分查找

二分查找是一种高效的查找算法,通常普通的循环遍历查找须要O(n)的时间复杂度,而二分查找时间复杂度则为O(logN),由于每一次都将查找范围减半。java

看看百度百科以及LeetCode官方给出的二分查找算法的解释:面试

(百度百科)算法

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。可是,折半查找要求线性表必须采用顺序存储结构,并且表中元素按关键字有序排数组

列。数据结构

(LeetCode)优化

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,能够在数据规模的对数时间复杂度内完成查找。但spa

是,二分查找要求线性表具备有随机访问的特色(例如数组),也要求线性表可以根据中间元素的特色推测它两侧元素的性质,以达到缩减问题规模的效果。指针

二分查找问题也是面试中常常考到的问题,虽然它的思想很简单,但写好二分查找算法并非一件容易的事情。code

可以使用二分查找有两个前提:一是要求数据结构必须先排好序或者说是有序的(这样能够推断出一个元素左边以及右边的性质) 二是还能够随机访问(顺序存blog

储结构)。

为何须要者两个前提呢?咱们经过一个案例来看看

 


 

二分查找最经典的案例 就是给出一个升序的无重复元素的数组,再给出一个目标值,返回数组中与目标值相等的

元素的索引(下标)找不到则返回-1;看下方核心代码:

int left = 0,right = array.length;
while
(left<right){ //left 为左边界 //right 为右边界 mid = (left+right)/2; if(array[mid]==target) return mid; //找到目标索引 直接返回 else if(array[mid]>target) { right = mid; //大于目标值 则搜索区间改成左半区间 }else if(array[mid]<target) { left = mid+1; 0 //小于目标值 则搜索区间改成右半区间 } return -1; }

可能不少小伙伴看到left<right会疑惑由于还见过left<=right的,这个咱们留待后面讨论。

这题其实就是在查找索引。假定数组为【1,2,3,4,5,6,7】 target值为6 咱们看看是如何查找的。

其实二分查找的核心就是一步一步的把查找(搜索)范围减半。

 

 

left = 0 ,right = array.length为初始化边界,即将查找范围锁定为[0,array.length)注意是左闭右开;

接着咱们将数组分为两部分,准确的说是3部分,一是 该查找范围的中间值mid  二是mid左边部分(姑且叫左半区间) 三是mid右边部分(姑且叫右半区间)

而后咱们来看看 mid 这个索引 对于的数组元素与目标值的大小比较。若是相等则表明mid 就是须要查找的索引直接返回,若是mid对应元素比目标值大,值大了

就应该缩小,因此咱们将查找范围改成mid 的左边部分

即right = mid。若是mid对应元素比目标值小,值小了就应该放大,因此咱们将查找范围改成mid右边部分即left = mid +1.可能有人好奇为何left = mid+1,而right 却直接变为mid。这个咱们也留在后面讨论。

一开始咱们left = 0 ,right = 7.那么第一步一开始查找的值即是(0+7)/2=3。便是4 比6要小,说明目标值更大因此将查找范围减半至更大的一部分(右边部分),咱们经过将左边界右移至mid+1的位置来改变

边界,此时left = 4,right是7,接着咱们查找(4+7)/2 = 5 索引5 对应的是6 正好是目标值因此直接返回了5。

为何咱们经过mid与目标值关系就能够知道查找范围应该落在哪里呢?

就是由于数组是已经排好序的(知足了一个条件),那为何咱们能够知道左右区间又或者说知道了范围能够直接访问中间的元素呢?由于数组是顺序存储结构可

以随机访问(经过下标),因此必须是

顺序存储结构。若是是链表没有下标访问是没法直接判断中间元素与目标值关系的。这就是为何想要二分查找必须知足这两个条件了。

接着咱们来讨论下 何时循环条件是left<=right,而何时又是left<right。

这一个其实取决与咱们定义查找范围区间时是如何定义的,因为咱们初始化left = 0,right = array.length。因此咱们能够肯定初始状态的左边界是0而右边界是数组长度。因为是查找索引,因此一开始的查找区间是[left,right)为左闭右开的,此时咱们的循环执行条件是left<right,循环的终止条件是left = right  ,此时

对应的区间是[left,left),该区间内已经没法再查询了因此该循环条件是可行的。

但当咱们将right 初始化为array.length-1时,此时的查找范围区间即是[left,right],左闭右闭,此时若是咱们的循环条件仍然是left<right,那么表明终止时left=right,对应的搜索区间是【left,left】此时咱们带入一个具体数字如是2,则区间则为[2,2],此时区间内还有一个数2没有查询而循环却终止了(漏查,所

以是不可取的。故循环条件的中的这个=号取决于查找区间的定义是左闭右开仍是左闭右闭。

还有一个问题是当减半查找范围时,left = mid +1,为何right = mid又或者right = mid-1。这其实也跟查找区间有关,上面讲到其实二分查找时将数组分为三

部分,mid,mid左半区间,mid右半区间。

若是咱们定义的right 为array.length,那么搜索区间是左闭右开的。咱们每查找一次mid 的值后,就要对范围作改变,由于mid已经查找过了因此咱们的选择要么左边要么右边。若是咱们去右边,则left 指针右移, 即left = mid +1。而要去左边时,须要的是right指针左移,因为区间是左闭右开的,因此右边界直接等于mid。咱们也能够将三部分两个区间直接用区间表示出来[left,mid) , [mid+1,right)这就是为何right 直接等于mid了。

固然咱们也能够将right 初始化为array.length-1,那么查找区间就是左闭右闭的 ,那三部分中里的两个区间分别为[left,mid-1],[mid+1,right]。这时咱们改变查

找范围时left = mid+1,而right = mid-1。

因此循环条件的等号和left,right的相应变化都取决于左右边界时的定义(取决因而左闭右闭仍是左闭右开)。

另外,对于 mid = (left+right)/2。 这一式子咱们能够变形优化, mid = left + (right-left)/2  防止溢出,也能够将整除2换成位运算右移一位即 mid = left = ((right-left)>>1)。

这是二分查找的一种经典题型——精确查找,找到某值就直接返回。关于精确查找的例题能够直接在leetcode里找到——704.二分查找

二分查找的应用除了精确查找还有查找左右边界

关于查找左右边界的定义我的看法为:

(查找左边界)从后往前有一部分知足条件,则前面必有一个临界点属于边界,比边界大则知足,比边界小则不符合。

(查找右边界)从前日后有一部分知足条件,则后面必有一个临界点属于边界,比边界小则知足,比边界大则不符合。

对于查找两个边界我也找了两个题目来帮助理解

先看看查找左边界(原题 leetcode278.第一个错误的版本):

 

 

 刚看题目,首先暴力确定能够,直接从头至尾扫一次,发现是错误的版本直接return出来就好。

虽然不是考精确查值,可是其实这个是考二分查找的。并且极其明显的查值左边界的特征:从后往前有一部分知足条件,则前面必有一个临界点属于边界,比边界大则知足,比边界小

则不符合。从最后往前到第一个错误的版本都是知足条件的都是错误的版本,可是第一个错误的版本就是临界点就是边界。边界前的版本都是正确的版本。

首先咱们想一想

咱们想一想若是第k个版本是错误的版本 那么很天然  它后面的全部版本 (k+1~n) 也都是错误的版本,可是第一个错误的版本倒是落在k的左边的或者就是k。

那若是第k个版本并不是错误的版本,那错误的版本会落在哪里呢?很明显确定是落在k的右手边,而且第一个错误的版本也是在右边。

直接看代码更便于理解:

 

 1 /* The isBadVersion API is defined in the parent class VersionControl.
 2       boolean isBadVersion(int version); */
 3 public class Solution extends VersionControl {
 4     public int firstBadVersion(int n) {
 5     int left = 1;
 6     int right = n;
 7     //【left,right】 左闭右闭搜索区间
 8     while (left <= right) {   
 9         int mid = left + ((right-left)>>1);
10         if (isBadVersion(mid)) {    //当前版本是错误的版本   要找第一个错误的版本   搜索区间改成左半部分
11             right = mid-1;
12         } else {
13             left = mid + 1;    //不是错误版本直接去右方找
14         }
15     }
16     return right+1;    //why?
17 }
18 }

 

对于区间的减半方向咱们已经了解了。那这题的返回值该是什么呢?咱们这样循环结束后,第一个错误的版本号是什么呢?

咱们能够根据循环内部的代码得出咱们想要的答案,由于是要第一个错误的版本,那么版本首先确定是错误的那咱们直接就将目光锁定在

 

 咱们看到 ,mid 版本是确定错误的,那咱们循环最后终止时的mid 应该就是咱们要找的第一个错误版本号。但是咱们定义的变量只有left,right两个指针。

那咱们该如何返回mid呢?咱们看到知足条件执行的语句是 right = mid -1;那mid 等于什么呢?很明显 mid = right +1; 因此咱们能够直接返回right+1;

不少人可能会疑惑,不是查询左边界嘛,怎么返回是指针是right 再+1?想返回左指针便于理解怎么办?其实也能够,咱们知道正确的返回值是right+1,那循环

条件是什么?是left<=right,那循环终止时left 的值就是right+1.因此能够返回right+1,也可直接返回left。

 

看完左边界咱们继续看看查找右边界(特殊缘由不放连接直接上图):

 

首先单看题目,其实就是问你,给你n块巧克力让你切成k份而且要保证切出来的是正方形。让咱们求知足条件下最大这个切出来的正方形的边长是多少。

可能会有些小伙伴看不出这是个二分查找的题目。让咱们分析一下。   咱们求的是知足条件下切出来的正方形最大的边长为多少。咱们知足的条件是什么?要切出

k块。那其实咱们想象,若是切出来的正方形的边长越小,那是否是获得的块数就越多呢?假设一下,   若是最大边长是  x 那,边长为1的正方形去切也确定知足吧?

那就是说咱们须要查的最大边长,其实就是知足条件下边长是右边界。肯定了是二分查找的题目,那咱们怎么肯定初始的搜索区间呢?对于左边,题目提示了最小

能够为1,那咱们就

为1,对于右边没有规定,那咱们就直接初始化为所给出的巧克力的最大边长就好。那如今问题就转变到了,对于如何肯定边长x是否知足条件咱们能够经过计算最

终切出来的块数而判

断出来。那对于一块巧克力,若是按照x的边长是正方形去切,咱们能获得几块巧克力呢?假设为6*5的巧克力去切边长为2的正方形。正方形是特殊的长方形,他

们的面积其实都是长*宽,那咱们其实能够直接经过巧克力的长宽分别除于边长再乘起来就能够了,注意是整出。如这个例子,能切出来的巧克力块数就是(6/2)*(5/2)=6块。

全部的问题都解决了咱们就直接上代码吧

 1 import java.util.Scanner;
 2 
 3 public class Main {
 4 
 5     public static void main(String[] args) {
 6         Scanner in = new Scanner(System.in);
 7         int left = 1;
 8         int right = -1;
 9         int n = in.nextInt();
10         int k = in.nextInt();
11         int[] h = new int[n];
12         int[] w = new int[n];
13         for (int i = 0; i < n; i++) {
14             h[i] = in.nextInt();
15             w[i] = in.nextInt();
16             right = Math.max(Math.max(h[i], w[i]), right);
17         }
18         while (left <= right) {
19             int mid = left + ((right - left) >> 1);
20             if (check(mid, h, w, k)) {
21                 left = mid + 1;
22             } else
23                 right = mid - 1;
24         }
25         System.out.println(right);
26     }
27 
28     public static boolean check(int mid, int[] h, int[] w, int k) {
29         int cnt = 0;
30         for (int i = 0; i < h.length; i++) {
31             cnt += ((h[i] / mid) * (w[i] / mid));
32             if (cnt >= k)
33                 return true;
34         }
35         return false;
36     }
37 }

好了本篇文章到此结束了,可能并不能真的让你攻克二分查找,但确定会让你更好的理解二分查找。感谢浏览!

相关文章
相关标签/搜索