(转)二分讲解(最全最通俗易懂的讲解)

本文深入探讨二分查找的原理及常见错误,分析不同区间表示法下的边界更新规则,避免查找错误与死循环,同时提供一个健壮的二分查找模板。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:博主在学习二分查找时,就对众多不同形式的二分查找写法感到疑惑,而且在OJ上,同一道题目,不同写法的二分,可能会让你 Wrong Answer 或者 TLE,,,,学校集训队有一个15学长,貌似队员都是用它的二分模板,而且基本没出过错,当时"太年轻”,总是觉得,为什么这学长写的二分模板这么健壮,为啥这么写才是对的呢, 当然当时也是没怎么想清楚,这几天,在图书馆,偶然翻到 <<编程珠玑>>,     发现里面对二分的探究,于是在网上学习了相关资料,但是感觉很多博文纯粹是为了写博文而写博文,写得很粗糙,于是趁机写篇关于二分博文,,希望可以帮助读者更好的理解二分查找。

第一个二分是在20世纪40年代出来的,但是第一个完全正确的二分直到20世纪60年代才出现,二分虽然简单,但是细节需要考虑很多,因此很多人写的二分都是有bug的

 

首先二分查找的适用条件是什么?我们为什么要用二分查找?

There is  no doubt  that  当然是为了效率啊

我举个例子,在含有10^9个元素的非下降序列的数组(当然了,开这么大,会爆内存的)中查找某个特定值,并且我们要查找的元素刚好是第10^9个元素,且该值只存在一次。用从头到尾遍历的方法,我需要for循环10^9次才能查找到其下标。如果用

二分查找,每次循环将key值与区间的mid点的值比较,mid点的值刚好等于key值,就返回下标,否则更新左边界或者是右边界来使区间减半,这样每次待查区间就会不断减半,最终在某一次,区间会缩小到刚好mid点的值等于key值,或者区间的left端,或者区间的right端点就刚好指向key值(不同的二分写法 不一样,因此最后key值的下标如果存在的话,可能是通过mid ,    right,left端来指向的),这边二分的次数就是需要循环判断的次数,最多判断次数,也就是最坏复杂度, 也就是O(log2n);

 

备注:下面代码均是针对:  一个非递减数组,在其中查找key值,存在的话返回下标,不存在的话返回-1;

三个步骤写出正确的二分查找(先给出正确写法,,后面会分析错误代码):

第一个步骤: 确定初始区间表示法。

我们都知道,一段有限区间的表示法,有4种: 

左闭右闭:  [0,  5]

左开右开 (-1,  6),

左闭右开 [0 ,  6)  

左开右闭 (-1,  5], 

这四个区间都表示0 ,1 , 2 ,3 ,4,5这6个数的集合,同理,假如我们要表示一段下标为0........5的数组,我们也可以用以上任意一种来表示。比较常用的是左闭右闭表示法,左闭右开表示法, 左开右开表示法。

如果选用的是左闭右开表示法来表示初始化区间:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.  
  6.     left = 0, right = n;

  7.  
  8.  
  9.     while ()

  10.     {

  11.  
  12.  
  13.         cout << "循环" << endl;

  14.     }

  15.  
  16.  
  17.     return -1;

  18. }

如果选用的是左闭右闭表示法来表示初始化区间:

 
  1. int binary(int array[], int n, int v)

  2. {

  3. int left, right, middle;

  4.  
  5. left = 0, right = n - 1;

  6.  
  7. while ()

  8. {

  9.  
  10. cout << "循环" << endl;

  11. }

  12.  
  13.  
  14. return -1;

  15. }

 

如果选用的是左开右开表示法:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.     left = -1, right = n;

  6.  
  7.     while ()

  8.     {

  9.  
  10.         cout << "循环" << endl;

  11.     }

  12.  
  13.  
  14.     return -1;

  15. }

第二个步骤:   根据第一步所选的区间表示法,写出对应的循环条件:

如果选用的是左闭右开表示法来表示初始化区间:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.     left = 0, right = n ;

  6.  
  7.     while ( left < right ) 

  8.     { 

  9.  
  10.          cout << "循环" << endl; 

  11.     

  12.      }

  13.      return -1;

  14. }

为什么对应的循环条件是这样呢? :其实很好理解的, 什么时候循环应该终止呢?当区间的实际长度已经<=0的时候。

 上面这样写,意味着循环终止的条件是left == right ,由于我们采用的是左闭右开表示法,也就是 区间为 [left  ,right) 且left == right 循环停止,,如果用个例子来解释,大家就会豁然开朗了, 假如[ left=4,  right=4),此时用左闭右闭来解释的话,区间实际上是出现了右边界=3 , 左边界=4 ,   这种情形,右边界 < 左边界,这意味着区间长度<=0 了,无法继续二分了。

 

如果选用的是左闭右闭表示法来表示初始化区间:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.     left = 0, right = n - 1;

  6.  
  7.     while ( left <= right )

  8.     {

  9.  
  10.         cout << "循环" << endl;

  11.     }

  12.  
  13.  
  14.     return -1;

  15. }

 上面这样写,意味着循环终止的条件是left > right ,由于我们采用的是左闭右闭表示法,也就是 区间为 [left  ,right],如果用个例子来解释,大家就会豁然开朗了, 假如[ left=4,  right=3],区间实际上是出现了 右边界=3 ,左边界=4这种情形,也就是右边界 <  左边界,这意味着区间长度<=0 了,无法继续二分了。

 

如果选用的是左开右开表示法:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.  
  6.     left = -1, right = n;

  7.  
  8.     while ( left + 1 != right )

  9.     {

  10.  
  11.         cout << "循环" << endl;

  12.     }

  13.  
  14.  
  15.     return -1;

  16. }

 上面这样写,意味着循环终止的条件是left +1 == right ,由于我们采用的是左开右开表示法,也就是 区间为 (left  ,right),如果用个例子来解释,大家就会豁然开朗了, 假如( left=3,  right=4),区间实际上是出现了 右边界=3 ,左边界=4这种情形,也就是右边界 <  左边界,这意味着区间长度<=0 了,无法继续二分了。

 

第三步:    在第一,二步正确的情况下,写对应的边界更新语句

如果选用的是左闭右开表示法来表示初始化区间:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.  
  6.     left = 0, right = n ;

  7.  
  8.     while (  left < right )

  9.     {

  10.         middle = (left + right) / 2;

  11.        

  12.         if (array[middle] > v)

  13.         {

  14.             right = middle ;

  15.         }

  16.         else if (array[middle] < v)

  17.         {

  18.             left = middle + 1 ;

  19.         }

  20.         else

  21.         {

  22.             return middle;

  23.         }

  24.  
  25.  
  26.         cout << "循环" << endl;

  27.     }

  28.  
  29.  
  30.     return -1;

  31. }

为什么left 和right 的更新条件要这样写呢?  

 
  1. if (array[middle] > v), 那么说明key 值肯定在[left , mid-1]中, 但是由于初始区间用了左闭右开表示法,我们应该表示为[left ,mid)

  2. 所以right = mid  而不是right = mid -1 ,右开右开!

  3.  
  4. if (array[middle] < v),那么说明key值肯定在[mid+1,right)中,因为左闭右开表示法,我们应该表示为[mid+1 ,right),所以left = mid +1,

  5. 而不是left = mid; 左闭左闭

 

如果选用的是左闭右闭表示法来表示初始化区间:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.  
  6.     left = 0, right = n-1 ;

  7.  
  8.     while (  left <= right )

  9.     {

  10.         middle = (left + right) / 2;

  11.        

  12.         if (array[middle] > v)

  13.         {

  14.             right = middle - 1;

  15.         }

  16.         else if (array[middle] < v)

  17.         {

  18.             left = middle + 1 ;

  19.         }

  20.         else

  21.         {

  22.             return middle;

  23.         }

  24.  
  25.  
  26.         cout << "循环" << endl;

  27.     }

  28.  
  29.  
  30.     return -1;

  31. }

为什么left 和right 的更新条件要这样写呢?  

 
  1. if (array[middle] > v), 那么说明key 值肯定在[left , mid-1]中,又由于初始区间用了左闭右闭表示法,我们应该表示为[left ,mid-1]

  2. 所以right = mid-1  而不是right = mid ,右闭右闭!

  3.  
  4.  
  5. if (array[middle] < v),那么说明key值肯定在[mid+1,right]中,因为左闭右开表示法,我们应该表示为[mid+1 ,right],所以left = mid +1,

  6. 而不是left = mid; 左闭左闭

如果选用的是左开右开表示法:

 
  1. int binary(int array[], int n, int v)

  2. {

  3.     int left, right, middle;

  4.  
  5.  
  6.     left = -1, right = n ;

  7.  
  8.     while (  left + 1 != right )

  9.     {

  10.         middle = (left + right) / 2;

  11.        

  12.         if (array[middle] > v)

  13.         {

  14.             right = middle ;

  15.         }

  16.         else if (array[middle] < v)

  17.         {

  18.             left = middle  ;

  19.         }

  20.         else

  21.         {

  22.             return middle;

  23.         }

  24.  
  25.  
  26.         cout << "循环" << endl;

  27.     }

  28.  
  29.  
  30.     return -1;

  31. }

 

为什么left 和right 的更新条件要这样写呢?  

 
  1. if (array[middle] > v), 那么说明key 值肯定在(left , mid-1]中,又由于初始区间用了左开右开表示法,我们应该表示为(left ,mid)

  2. 所以right = mid  而不是right = mid -1 ,右开右开!

  3.  
  4. if (array[middle] < v),那么说明key值肯定在[mid+1,right)中,因为左开右开表示法,我们应该表示为(mid ,right), 所以left = mid ,

  5. 而不是left = mid + 1; 左开左开!

若对上面三个步骤还有疑问或者不太理解,没关系, 继续往下看,下面的分析能让你更好的理解

下面讲讲,二分中经常出现的错误

1.【两个边界写错了一个,造成查找错误】先上段有bug的代码,是不是你常用的呢?

 
  1. #include<cstdio>

  2. #include<algorithm>

  3. #include<iostream>

  4. using namespace std;

  5. int s[1000];

  6. int binary(int array[], int n, int v)

  7. {

  8.     int left, right, middle;

  9.  
  10.  
  11.     left = 0, right = n;

  12.  
  13.     while (left < right)

  14.     {

  15.         middle = (left + right) / 2;

  16.         if (array[middle] > v)

  17.         {

  18.             right = middle - 1;

  19.         }

  20.         else if (array[middle] < v)

  21.         {

  22.             left = middle + 1;

  23.         }

  24.         else

  25.         {

  26.             return middle;

  27.         }

  28.  
  29.         cout << "循环" << endl;

  30.     }

  31.  
  32.  
  33.     return -1;

  34. }

  35.  
  36. int main()

  37. {

  38.     int n  ;

  39.     cin >> n;

  40.  
  41.     for(int i = 0; i <  n ; i++)

  42.         cin >> s[i];

  43.  
  44.     int Index = binary(s,n,6);//查找6

  45.     cout << "key值的下标是:" << Index << endl;

  46.     return 0;

  47.  
  48.  
  49.  
  50.  
  51. }

  52. /*

  53. 6

  54. 2 3 4 5 6 7

  55. */

大家将我注释里面的样例拿去跑,会发现查找key = 6,时居然返回-1,。

我们来手动模拟一下上面这段错误代码的二分过程:

第一次循环时:

left = 0,   right = 6, 既[0, 6)

由于s[3] <6,,,,更新下次循环的待查区间为 [4, 6);

第二次循环时:

left = 4 , right = 6, 既[4, 6)

由于 s[5] > 6...........更新下次循环的待查区间为[4,4);(其实[4, 4)对应着区间[4,3],已经不是一个合法区间了,说明区间长度<=0l了,不能再二分减半了,因此应该退出循环)

然后left == right ==4就退出循环了,返回-1.

我们来分析为什么会有bug:

初始化区间的表示法就打算用左闭右开,既[0 , 6),  那么我们的边界更新语句就应该与之对应,而当满足array[middle] > v的条件时, v如果存在的话应该在[left, mid)区间中,但是上诉循环内更新右端点的语句,居然是用

 
  1. if (array[middle] > v)

  2. {

  3. right = middle - 1;

  4. }

这样的话区间被更新为[left,   mid-1),  那么 mid-1这个位置的元素就会被忽略了,万一,mid -1这个点的值就是key值呢,所以啊就像上面分析过程的第二次循环,其实区间应该更新为【4,  5)    ,,不然你更新后的区间就不包含样例中的6了,

博主是勤劳的小蜜蜂,顺手也把right = mid时 正确的二分过程也手动模拟吧(正确的程序,在上面就给出了):

第一次循环时:

left = 0,   right = 6, 既[0  6)

由于s[3] <6,,,,更新下次循环的待查区间为 [4, 6);

第二次循环时:

left = 4 , right = 6, 既[4, 6)

由于 s[5] > 6...........更新下次循环的待查区间为[4,5);

第三次循环时:

left = 4 ,right = 5,既 [4, 5)

由于s [4] == 6 ,return 4;

成功查到!

敲黑板!敲黑板:初始区间的表示法在选定后, 边界更新语句应该与之对应,避免漏掉key值,我这边只是举例左闭右开表示法的错误例子,其他区间表示法,若边界写错了一个,也会出错!!!

 

2:【两个边界写错了2个,造成死循环】

为什么会死循环,死循环说明了区间长度一直没有减少。例如在某次循环后待查询的区间始终是[0 ,1],但是在该次循环后

区间始终无法减小,始终是保持着是[0  ,   1] ;

其实死循环的导致,是因为区间的左右边界条件全部都写错了(也可以说是前面第一种错误情况的一种特例,第一种错误情况是只有   左边界或者右边界写错)

给出一个错误的代码,估计读者也会觉得好熟悉啊:

 
  1. #include<cstdio>

  2. #include<algorithm>

  3. #include<iostream>

  4. using namespace std;

  5. int s[1000];

  6. int binary(int array[], int n, int v)

  7. {

  8.     int left, right, middle;

  9.  
  10.     left = 0, right = n - 1;

  11.  
  12.     while (left <= right)

  13.     {

  14.         middle = (left + right) / 2;

  15.  

  16.         if (array[middle] > v)

  17.         {

  18.             right = middle ;

  19.         }

  20.         else if (array[middle] < v)

  21.         {

  22.             left = middle ;

  23.         }

  24.         else

  25.         {

  26.             return middle;

  27.         }

  28.  
  29.         cout << "循环" << endl;

  30.     }

  31.  
  32.  
  33.     return -1;

  34. }

  35.  
  36. int main()

  37. {

  38.     int n  ;

  39.     cin >> n;

  40.  
  41.     for(int i = 0; i <  n ; i++)

  42.         cin >> s[i];

  43.  
  44.     int Index = binary(s,n,1); //查找1

  45.  
  46.     cout << "key值的下标是:" << Index << endl;

  47.  
  48.     return 0;

  49.  
  50. }

  51. /*

  52. 2

  53. 0 1

  54. */

大家将我注释里面的样例拿去跑,会发现查找1时,出现了死循环,。

我们来手动模拟一下上面这段错误代码的二分过程:

第一次循环时:

left = 0 , right = 1 ,既 [ 0 , 1]

s[ 0]  <  1 , 区间更新为 [0 , 1];

第二次循环时:

left = 0, right =1 ,既  [0   ,   1]

s[ 0]  <  1 , 区间还是更新为 [0 , 1];

接下来,,区间更新都是为[0,   1],长度无法减少,所以出现了死循环。

出现bug原因分析:

初始区间用了左闭右闭,那么边界更新语句应该是left = mid +1 ,right = mid -1,而不是 left = mid ,rigth = mid,,,两个

边界语句都写错了。

订正后的二分过程模拟(正确代码开头有):

 

第一次循环时:

left = 0 ,   right = 1 ,既 [ 0 , 1]

s[ 0]  <  1 , 区间更新为 [1 , 1];

第二次循环时:

left = 1 ,  right =1 ,  既 [1,  1]

s[1] == 1,   return 1

查找成功!

 

敲黑板!敲黑板:再说一遍!!初始区间的表示法在选定后, 两条边界更新语句应该与之对应,否则可能会出现死循环.

 

规避边界条件的错误后,我们还要注意一个地方可能会溢出,这也是不能忽略的:

 
  1. middle = (left + right) / 2; 容易溢出

  2. middle = left + ( right - left ) /2 这样写不容易溢出

  3. 假如left 很大,right 很大,此时第一个语句就会溢出,爆int,因为left + right 的结果可能会大于int 能表示的最大值

完善的二分写法:

下面这个完善的二分写法,是<<编程珠玑>>中的代码,第一次看到后,感觉特别熟悉,校队的旺神学长写的二分模板基本跟里面一模一样(旺神学长貌似是自己调试出来的,此处再次膜拜旺神学长  ,,ORZ , ORZ)

 

其实这个完善的二分写法就是开头提到的左开右开写法,但是做了一些小修改。

考虑一种特殊情况,如果数组中的元素都相同 ,  那么查找的时候不一定每次都会返回第一个元素的位置, 用开头的3种正确代码去查找,肯定返回的是正中间元素的位置. ,,因此进行了以下修改,修改后的代码,可以在非递减序列中,查找某个元素第一次出现的位置,不存在返回-1

 

 ​​​​​
int binary(int array[], int n, int v)

{

    int left, right, middle;



    left = -1, right = n ;



    while (  left + 1 != right )

    {

        middle = (left + right) / 2;



        if (array[middle] >= v)

        {

            right = middle ;

        }

        else

        {

            left = middle  ;

        }




    }



    if( right == n || array[right]!= v)

        return -1;


    return right;

}

 

<think>好的,用户希望获得关于CNN中卷积层、池化层和全连接层作用的通俗易懂且详细的讲解。首先,我需要回顾之前的对话和提供的引用内容,确保回答准确且符合用户需求。 用户之前已经得到了较为技术性的解释,现在需要更通俗的版本。可能需要用类比或日常例子来帮助理解。比如,把卷积层比作放大镜找细节,池化层类似压缩图片,全连接层像总结报告。 检查引用内容,特别是引用[1]和[2],确认全连接层作为分类器的作用,以及卷积层和池化层的功能。需要确保解释与这些引用一致,同时避免使用过多术语。 用户提到“通俗易懂”,所以需要简化数学公式或避免复杂的公式。比如,之前的回答中有公式,但用户现在可能需要更少的数学,更多的比喻。 另外,用户可能对CNN的工作流程感兴趣,所以用步骤化的例子来说明各层如何协作,比如从输入图片到终分类的过程。 需要确认是否覆盖了各层的作用、为什么重要、如何协作,以及可能的常见问题,比如参数共享、平移不变性等,但用简单的话解释。 还要注意用户可能存在的误解,比如全连接层为何参数多,是否有替代方法,如全局平均池化,但引用[1]提到可以用卷积操作实现,这点可能需要提及但不过于深入。 后,确保回答结构清晰,分点讲解,每个层的作用、通俗解释、实际例子,以及协作流程。可能还需要总结和问题部分,但根据用户要求,问题已经在前面的回答中生成了,所以这里可能不需要。 总之,需要将技术内容化为日常语言,使用比喻和例子,确保每个层的功能明确,并说明它们如何共同工作,帮助用户直观理解CNN的工作原理。</think>### 通俗版讲解:CNN三大核心层的作用 (用日常场景比喻+关键原理说明,保证零基础也能理解) --- #### **1. 卷积层——「细节放大镜」** **作用一句话**:像用放大镜扫描图片,专门找小图案(比如线条、斑点、颜色变化)。 **通俗解释**: - **举个栗子**:假设你要找一张猫图里的“猫耳朵”,不需要看整张图,只需要用小窗口(比如3×3像素)在图上滑动,每次检查这个小窗口里有没有尖尖的形状——这就是卷积核在检测特征。 - **关键设计**: - **局部观察**:每个卷积核只关注图片的一小块区域(局部感受野),避免一次性处理全图信息[^2]。 - **共享参数**:同一个“放大镜”(卷积核)扫过全图,比如检测“竖线”的核无论扫到左上角还是右下角,都用同一组参数,减少计算量[^2]。 - **多层叠加**:第一层找简单线条,第二层把线条组合成耳朵形状,第三层再拼成整只猫头——类似乐高积木层层组装[^3]。 --- #### **2. 池化层——「智能压缩器」** **作用一句话**:把图片信息压缩,只保留关键特征,同时让模型不纠结于细节位置。 **通俗解释**: - **举个栗子**:假设你拍了一张猫的照片,稍微向左平移了一点,池化层会让电脑认为这两张图都是“猫”,而不因为耳朵位置偏移就认不出来。 - **关键设计**: - **降维防过拟合**:比如用“大池化”在2×2区域内只保留明显的数值,相当于把图片分辨率降低(类似手机截图缩小),减少计算负担[^2]。 - **平移不变性**:无论猫耳朵在图片左侧还是右侧,池化后特征仍能被识别,增强模型鲁棒性[^3]。 - **常用操作**: - **大池化**:取区域大值(保留显著特征,如“这里有个明显的边缘”) - **平均池化**:取区域平均值(柔和处理,适合平滑背景) --- #### **3. 全连接层——「终极裁判员」** **作用一句话**:把前面找到的所有特征汇总,判断到底属于哪一类(比如“猫”还是“狗”)。 **通俗解释**: - **举个栗子**:假设卷积层发现了“尖耳朵”“胡须”“圆眼睛”,池化层确认这些特征的位置不重要,全连接层就会像老师改卷一样,根据这些特征打分数:猫90分,狗10分,输出终答案。 - **关键设计**: - **特征整合**:将多维特征图“拍扁”成一维向量(比如把20×20的图变成400个数字),方便计算[^1]。 - **分类决策**:通过权重计算(类似$\text{猫得分} = 0.8\times\text{耳朵特征} + 0.6\times\text{胡须特征} + ...$)输出概率,常用softmax函数归一化为概率分布[^2]。 - **参数问题**:传统全连接层参数极多(比如1000个神经元连接前一层的10万参数),因此现代网络常用全局平均池化(GAP)替代——直接对每个特征图取平均值,大幅减少参数[^1]。 --- ### **三兄弟协作流程图(以猫狗分类为例)** 1. **输入图片** → 2. **卷积层1**:发现边缘、色块 → 3. **池化层1**:压缩图片,忽略边缘位置变化 → 4. **卷积层2**:组合边缘成耳朵、眼睛等部件 → 5. **池化层2**:再次压缩,确认部件存在即可 → 6. **全连接层**:综合所有部件特征,判断“猫”概率70%,“狗”概率30% → 7. **输出结果**:喵星人! --- ### **你可能还想问** 1. **为什么不用全连接层直接处理图片?** → 一张1000×1000的图片,全连接层需要10^6×10^3=10^9个参数,而卷积层用10个3×3核只需90个参数,效率天壤之别[^2]。 2. **池化层会丢失重要信息吗?** → 会,但这是有意为之!就像人看模糊的马赛克图也能认出物体,关键特征保留即可,噪声过滤反而提升泛化能力[^3]。 3. **全连接层必须放在后吗?** → 不一定!有些网络(如GoogleNet)中间也插入全连接层辅助训练,但终分类仍需汇总层[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值