zoukankan      html  css  js  c++  java
  • #转 二分查找

    二分查找

    二分查找算法基本思想

    二分查找算法的前置条件是,一个已经排序好的序列(在本篇文章中为了说明问题的方便,假设这个序列是升序排列的),这样在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止.

    用伪代码来表示, 二分查找算法大致是这个样子的:

     1 left = 0, right = n -1  
     2 
     3 while (left <= right)  
     4 
     5     mid = (left + right) / 2  
     6 
     7     case  
     8 
     9         x[mid] < t:    left = mid + 1;  
    10 
    11         x[mid] = t:    p = mid; break;  
    12 
    13         x[mid] > t:    right = mid -1;  
    14 
    15   
    16 
    17 return -1;

    第一个正确的程序

    根据前面给出的算法思想和伪代码, 我们给出第一个正确的程序,但是,它还有一些小的问题,后面会讲到:

     1 int search(int array[], int n, int v)  
     2 
     3 {  
     4 
     5     int left, right, middle;  
     6 
     7   
     8 
     9     left = 0, right = n - 1;  
    10 
    11   
    12 
    13     while (left <= right)  
    14 
    15     {  
    16 
    17         middle = (left + right) / 2;  
    18 
    19         if (array[middle] > v)  
    20 
    21         {  
    22 
    23             right = middle;  
    24 
    25         }  
    26 
    27         else if (array[middle] < v)  
    28 
    29         {  
    30 
    31             left = middle;  
    32 
    33         }  
    34 
    35         else  
    36 
    37         {  
    38 
    39             return middle;  
    40 
    41         }  
    42 
    43     }  
    44 
    45   
    46 
    47     return -1;  
    48 
    49 }  

    下面,讲讲在编写二分查找算法时可能出现的一些问题.

    边界错误造成的问题

    二分查找算法的边界,一般来说分两种情况,一种是左闭右开区间,类似于[left, right),一种是左闭右闭区间,类似于[left, right].需要注意的是, 循环体外的初始化条件,与循环体内的迭代步骤, 都必须遵守一致的区间规则,也就是说,如果循环体初始化时,是以左闭右开区间为边界的,那么循环体内部的迭代也应该如此.如果两者不一致,会造成程序的错误.比如下面就是错误的二分查找算法:

     1 int search_bad(int array[], int n, int v)  
     2 
     3 {  
     4 
     5     int left, right, middle;  
     6 
     7   
     8 
     9     left = 0, right = n;  
    10 
    11   
    12 
    13     while (left < right)  
    14 
    15     {  
    16 
    17         middle = (left + right) / 2;  
    18 
    19         if (array[middle] > v)  
    20 
    21         {  
    22 
    23             right = middle - 1;  
    24 
    25         }  
    26 
    27         else if (array[middle] < v)  
    28 
    29         {  
    30 
    31             left = middle + 1;  
    32 
    33         }  
    34 
    35         else  
    36 
    37         {  
    38 
    39             return middle;  
    40 
    41         }  
    42 
    43     }  
    44 
    45   
    46 
    47     return -1;  
    48 
    49 }

       

    这个算法的错误在于, 在循环初始化的时候,初始化right=n,也就是采用的是左闭右开区间,而当满足array[middle] > v的条件是, v如果存在的话应该在[left, middle)区间中,但是这里却把right赋值为middle - 1了,这样,如果恰巧middle-1就是查找的元素,那么就会找不到这个元素.

    下面给出两个算法, 分别是正确的左闭右闭和左闭右开区间算法,可以与上面的进行比较:

      1 int search2(int array[], int n, int v)  
      2 
      3 {  
      4 
      5     int left, right, middle;  
      6 
      7   
      8 
      9     left = 0, right = n - 1;  
     10 
     11   
     12 
     13     while (left <= right)  
     14 
     15     {  
     16 
     17         middle = (left + right) / 2;  
     18 
     19         if (array[middle] > v)  
     20 
     21         {  
     22 
     23             right = middle - 1;  
     24 
     25         }  
     26 
     27         else if (array[middle] < v)  
     28 
     29         {  
     30 
     31             left = middle + 1;  
     32 
     33         }  
     34 
     35         else  
     36 
     37         {  
     38 
     39             return middle;  
     40 
     41         }  
     42 
     43     }  
     44 
     45   
     46 
     47     return -1;  
     48 
     49 }  
     50 
     51 /*------------华丽的分割线----------------------------*/  
     52 
     53 int search3(int array[], int n, int v)  
     54 
     55 {  
     56 
     57     int left, right, middle;  
     58 
     59   
     60 
     61     left = 0, right = n;  
     62 
     63   
     64 
     65     while (left < right)  
     66 
     67     {  
     68 
     69         middle = (left + right) / 2;  
     70 
     71   
     72 
     73         if (array[middle] > v)  
     74 
     75         {  
     76 
     77             right = middle;  
     78 
     79         }  
     80 
     81         else if (array[middle] < v)  
     82 
     83         {  
     84 
     85             left = middle + 1;  
     86 
     87         }  
     88 
     89         else  
     90 
     91         {  
     92 
     93             return middle;  
     94 
     95         }  
     96 
     97     }  
     98 
     99   
    100 
    101     return -1;  
    102 
    103 }  

    死循环

    上面的情况还只是把边界的其中一个写错, 也就是右边的边界值写错, 如果两者同时都写错的话,可能会造成死循环,比如下面的这个程序:

     1 int search_bad2(int array[], int n, int v)  
     2 
     3 {  
     4 
     5     int left, right, middle;  
     6 
     7   
     8 
     9     left = 0, right = n - 1;  
    10 
    11   
    12 
    13     while (left <= right)  
    14 
    15     {  
    16 
    17         middle = (left + right) / 2;  
    18 
    19         if (array[middle] > v)  
    20 
    21         {  
    22 
    23             right = middle;  
    24 
    25         }  
    26 
    27         else if (array[middle] < v)  
    28 
    29         {  
    30 
    31             left = middle;  
    32 
    33         }  
    34 
    35         else  
    36 
    37         {  
    38 
    39             return middle;  
    40 
    41         }  
    42 
    43     }  
    44 
    45   
    46 
    47     return -1;  
    48 
    49 }  

    这个程序采用的是左闭右闭的区间.但是,当array[middle] > v的时候,那么下一次查找的区间应该为[middle + 1, right], 而这里变成了[middle, right];当array[middle] < v的时候,那么下一次查找的区间应该为[left, middle - 1], 而这里变成了[left, middle].两个边界的选择都出现了问题, 因此,有可能出现某次查找时始终在这两个范围中轮换,造成了程序的死循环.

    溢出

    前面解决了边界选择时可能出现的问题, 下面来解决另一个问题,其实这个问题严格的说不属于算法问题,不过我注意到很多地方都没有提到,我觉得还是提一下比较好.

    在循环体内,计算中间位置的时候,使用的是这个表达式:

    1 middle = (left + right) / 2;  

    假如,left与right之和超过了所在类型的表示范围的话,那么middle就不会得到正确的值.

    所以,更稳妥的做法应该是这样的。

    1 middle = left + (right - left) / 2;  

    更完善的算法

    前面我们说了,给出的第一个算法是一个"正确"的程序, 但是还有一些小的问题。

    首先, 如果序列中有多个相同的元素时,查找的时候不见得每次都会返回第一个元素的位置, 比如考虑一种极端情况:序列中都只有一个相同的元素,那么去查找这个元素时,显然返回的是中间元素的位置.

    其次, 前面给出的算法中,每次循环体中都有三次情况,两次比较,有没有办法减少比较的数量进一步的优化程序?

    <<编程珠玑>>中给出了解决这两个问题的算法,结合前面提到溢出问题我对middle的计算也做了修改:

     3 int search4(int array[], int n, int v)  
     4 
     5 {  
     6 
     7     int left, right, middle;  
     8 
     9   
    10 
    11     left = -1, right = n;  
    12 
    13   
    14 
    15     while (left + 1 != right)  
    16 
    17     {  
    18 
    19         middle = left + (right - left) / 2;  
    20 
    21   
    22 
    23         if (array[middle] < v)  
    24 
    25         {  
    26 
    27             left = middle;  
    28 
    29         }  
    30 
    31         else  
    32 
    33         {  
    34 
    35             right = middle;  
    36 
    37         }  
    38 
    39     }  
    40 
    41   
    42 
    43     if (right >= n || array[right] != v)  
    44 
    45     {  
    46 
    47         right = -1;  
    48 
    49     }  
    50 
    51   
    52 
    53     return right;  
    54 
    55 }  

    以上这个最精简

  • 相关阅读:
    [Bug] .NET 2.0 的Bug —— ComboBox中不能添加Component.
    [WPF]WPF中如何实现数据与表示分离。(一) —— XAML
    我有2个Windows Live Messenger的邀请。
    Avalon学习笔记 之 路由事件
    [FxCop.设计规则]10. 类型应该被声明在命名空间中
    Avalon学习笔记(二)——从属属性 和 附加属性
    Longhorn将集成RSS支持。
    [WinFX]WinFX 12月份CTP发布,其中包含了XAML设计器
    [FxCop.设计规则]9. 事件句柄声明不恰当
    对于最近一段时间热门的新技术的感想
  • 原文地址:https://www.cnblogs.com/awsent/p/4267202.html
Copyright © 2011-2022 走看看