经典面试题总结 —— Binary Search 及其变种

二分查找是在技术面试中经常出现的题目,首先这种题目考察思路,另外因为代码一般很短----不会超过50行。所以很适合做技术笔试,或者面试之类的题目出现。

之前做过一些题目,很多是BS算法的变种,我这里给出几个例子,算是做一个总结吧。


1. 传统的Binary Search

     1.1. 最普通的BS算法就是给定一个排好序的数组,然后查找一个数是否在数组内,如果在给出下标,如果不在则返回-1. 

     

[cpp]  view plain copy
  1. template<typename T>  
  2. int binarySearch(const T vData[], int nSize, const T &query)  
  3. {  
  4.     int l, u, m;  
  5.   
  6.     l = 0;  
  7.     u = nSize - 1;  
  8.   
  9.     while(l <= u)//算法需要注意的是 选择的 l 和u, 如果u 选择的是size的话,这个条件会不一样  
  10.     {  
  11.         m = (l + u)/2;  
  12.   
  13.         if(vData[m] == query) //找到返回  
  14.         {  
  15.             return m;  
  16.         }  
  17.         else if(vData[m] > query) //当前元素大于要找的元素  
  18.         {  
  19.             u = m - 1;  
  20.         }  
  21.         else  
  22.         {  
  23.             l = m + 1;  
  24.         }  
  25.     }  
  26.     return -1;  
  27. }  

    1. 2 比普通的BS算法稍微复杂一点的应该是在字典中查找一个指定的字符串了。

     对于输入的所有单词,我们可以使用排序算法使得所有单词按照字典序排列,然后用BS算法找到给定的元素的下标。

     代码如下:

 

[cpp]  view plain copy
  1. #define MAXCHARLEN 50  
  2. int binarySearch(const char directionary[][MAXCHARLEN], int nSize, const char* pQuery)  
  3. {  
  4.     int l, u;  
  5.   
  6.     l = 0;  
  7.     u = nSize - 1;  
  8.     int m;  
  9.     int cmpResult;  
  10.     while(l <= u)  
  11.     {  
  12.         m = (l + u)/2;  
  13.         cmpResult = strcmp(directionary[m], pQuery);//先保存结果,以免被计算好几次  
  14.   
  15.         if(cmpResult == 0)  
  16.         {  
  17.             return m;  
  18.         }  
  19.         else if(cmpResult == 1)  
  20.         {  
  21.             u = m - 1;  
  22.         }  
  23.         else  
  24.         {  
  25.             l = m + 1;  
  26.         }  
  27.   
  28.     }  
  29.     return -1;  
  30. }   


    

2. 含有空的Bianry Search

     这个问题的一种描述形式可能如下:

     在给定的字符串序列中(按照字典序排列好的), 存在一些空串,请你找出给定字符串的位置,不在里面返回 -1.

     例如  char directionary[] = {"", "a", "ab", "", "", "", "bb", "cc", "zz", "" ""} 中需找 "ab"; 

     这里需要添加一些处理,在找到binary search的中心点以后,如果是空,我们要移动当前的m

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include <string.h>  
  3. #include<memory.h>  
  4.   
  5. #define MAXCHARLEN 50  
  6. int binarySearch(const char directionary[][MAXCHARLEN], int nSize, const char* pQuery)  
  7. {  
  8.     int l, u;  
  9.   
  10.     l = 0;  
  11.     u = nSize - 1;  
  12.     int m;  
  13.     int cmpResult;  
  14.     while(l <= u)  
  15.     {  
  16.         m = (l + u)/2;  
  17.         while(m > l && directionary[m][0] == '\0'//这个位置稍微不小心就留下bug了,或许我这里依然有bug存在。  
  18.         {  
  19.             m--;  
  20.         }  
  21.         cmpResult = strcmp(directionary[m], pQuery);  
  22.   
  23.         if(cmpResult == 0)  
  24.         {  
  25.             return m;  
  26.         }  
  27.         else if(cmpResult == 1)  
  28.         {  
  29.             u = m - 1;  
  30.         }  
  31.         else  
  32.         {  
  33.             l = m + 1;  
  34.         }  
  35.   
  36.     }  
  37.     return -1;  
  38. }    
  39. int main()//test cases.  
  40. {  
  41.   
  42.     const char directionary[][MAXCHARLEN] =   
  43.     {  
  44.         """aa""""bb""""cc""dd""e""""""ff"  
  45.     };  
  46.   
  47.     const char query[][MAXCHARLEN] =   
  48.     {  
  49.         "aa""bb""cc""dd""kk""e"  
  50.     };  
  51.   
  52.     forint i = 0; i < 6; i++)  
  53.     {  
  54.         printf("%d ", binarySearch(directionary, 11,  query[i]));  
  55.     }  
  56.     return 0;  
  57. }  

 

3. Rotated Sorted Array

    这种问题有一个描述是说,已知我们有一个数组,它是在一个排序数组的基础上,用rotate的方式生成出来的。

    例如: 3 4 5 1 2 就是一个符合上面说法的数组。 

    现在有如下两个任务:

    3.1 查找到某个元素(Search for a specific element).

    此类问题的分析的时候,一个核心的思路应该是说,无论数组[l, u]被分开后,[l, m-1],  [m+1, u]其中一定至少有一个是已经排好序的了,并且这个排序的区间内的所有元素,和另一个区间 是不会相互覆盖的。

    例如 3 4 5 1 2, 如果分成  3 4, 5 1 2, 不会有交叠的情况,就是说 各自依然满足原始 ratated sorted array的定义。

    所以可以用binary search 的方法来做搜索。

    不过这些代码特别容易写出bug,虽然看起来很简单。 

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include <string.h>  
  3. #include<memory.h>  
  4.   
  5. bool InRange(int l, int u, int query)  
  6. {  
  7.     return query >= l && query <= u;  
  8. }  
  9. int binarySearch(int rotatedArray[], int nSize, int query)  
  10. {  
  11.     int lower, upper;  
  12.   
  13.     lower = 0, upper = nSize - 1;  
  14.     int m;  
  15.   
  16.     while(lower <= upper)  
  17.     {  
  18.         m = (lower + upper)/2;  
  19.   
  20.         if(rotatedArray[m] == query)  
  21.         {  
  22.             return m;  
  23.         }  
  24.   
  25.         if(rotatedArray[m] >= rotatedArray[lower]) //如果第一个区间是排序区间  
  26.         {  
  27.             if(InRange(rotatedArray[lower], rotatedArray[m], query))//如果当前搜索在排序区间内  
  28.             {  
  29.                 upper = m - 1;  
  30.             }  
  31.             else//检索元素不在排序区间内  
  32.             {  
  33.                 lower = m + 1;  
  34.             }  
  35.         }  
  36.         else//另一半是排序区间  
  37.         {  
  38.             if(InRange(rotatedArray[m], rotatedArray[upper], query))//在排序区间内  
  39.             {  
  40.                 lower = m + 1;  
  41.   
  42.             }  
  43.             else//不在排序区间内  
  44.             {  
  45.                 upper = m - 1;  
  46.             }  
  47.         }  
  48.   
  49.     }  
  50.   
  51.     return -1;  
  52.   
  53. }  
  54. int main()  
  55. {  
  56.   
  57.     int a[] = {3, 5, 7, 9, 0, 2};  
  58.   
  59.     for(int i = 0; i < 10; i++)  
  60.     {  
  61.         printf("i: %d is at %d\n",i, binarySearch(a, 6, i));  
  62.     }  
  63.     return 0;  
  64. }  

    


    3.2 找到最小元素(Search for the minumum or maximum )

    如果我们查找的区间中 a[lower] < a[upper] 那么我们可以断定,我们当前搜索的区间是完全排序的,没有rotated,可以直接返回 a[lower].

    如果 lower == upper 我们只有一个元素, 也可以直接返回 a[lower];

    

    在其他情况,我们可以可以用binary search的思想。

    如果 a[m] >= a[lower], 说明从a[lower] -> a[m]是排序好的,a[lower] < a[upper]的条件又不满足,所以搜索区间一定再另一侧。

    否则搜索区间在 a[m], a[lower]. 

  

[cpp]  view plain copy
  1. int findMin(int a[], int lower, int upper)  
  2. {  
  3.     if(a[lower] <= a[upper]) //包括2个case, 1:搜索到最小区间 2: 全区间已经是排序区间,则直接返回最小那个数  
  4.     {  
  5.         return lower;  
  6.     }  
  7.     int m = (lower + upper)/2;  
  8.   
  9.     if(a[m] >= a[lower] )//如果 lower ... m 是排序区间,并且整个大的区间不是排序区间,那么下一步搜索转到 m + 1, upper.   
  10.     {                    //等于号为了防止  例如输入是 6 1这样的情况,在这个情况下 m == lower  
  11.         return findMin(a, m+1, upper);  
  12.     }  
  13.     else  
  14.     {  
  15.         return findMin(a, lower, m); //否则搜索当前区间  
  16.     }  
  17. }  
  18. int main() // generate test case  
  19. {  
  20.     int a[10];  
  21.     forint i = 0; i < 6; i++)  
  22.     {  
  23.         forint j = 0; j < 6; j++)  
  24.         {  
  25.             a[ (i + j)%6] = j;  
  26.         }  
  27.         forint j = 0; j < 6; j++)  
  28.         {  
  29.             printf("%d ", a[j]);  
  30.         }  
  31.         printf("\nMin at %d\n", findMin(a, 0, 5));  
  32.     }  
  33.     return 0;  
  34. }  

    

4. 统计出现的次数

    这个题目的描述可以是这样: 

    在一个排序好的数组中,有一些元素是重复的。 我们写一个函数,对给定的数,我们返回这个数出现的次数。

    例如输入数据  1 2 2 3 3 3 5 5,如果输入2, 返回2,因为2在数组中出现2次。

    

    这个问题可以引入两个子问题,寻找到 Query(Q)第一次出现的位置,和Q最后一次出现的位置。

    我们可以称之为 lower_bound, 和 upper_bound

    

[cpp]  view plain copy
  1. int findLowerBound(int a[], int nSize, int query)  
  2. {  
  3.     int lower, upper;  
  4.     int m;  
  5.     lower = 0, upper = nSize - 1;  
  6.   
  7.     while(lower <= upper)  
  8.     {  
  9.         m = (lower + upper)>>1;  
  10.   
  11.         if(a[m] == query)  
  12.         {  
  13.             while(a[--m] == query);  
  14.             return m+1;  
  15.         }  
  16.         else if(a[m] > query)  
  17.         {  
  18.             upper = m - 1;  
  19.         }  
  20.         else  
  21.         {  
  22.             lower = m + 1;  
  23.         }  
  24.     }  
  25.   
  26.     return -1;  
  27. }  
  28.   
  29. int findUpperBound(int a[], int nSize, int query)  
  30. {  
  31.     int lower, upper, m;  
  32.   
  33.     lower = 0, upper = nSize - 1;  
  34.   
  35.     while(lower <= upper)  
  36.     {  
  37.         m = (lower + upper) >> 1;  
  38.   
  39.         if(a[m] == query)  
  40.         {  
  41.             while(a[++m] == query);  
  42.             return m-1;  
  43.               
  44.         }  
  45.         else if(a[m] > query)  
  46.         {  
  47.             upper = m - 1;  
  48.   
  49.         }  
  50.         else  
  51.         {  
  52.             lower = m + 1;  
  53.         }  
  54.     }  
  55.     return -1;  
  56. }  
  57. int main() //generate test case  
  58. {  
  59.     int a[] = {1, 2, 2, 3, 3, 3, 5, 5, 5}; //size is 9  
  60.     int l, u;  
  61.     //int *p =  lower_bound(a, a + 9, 0);  
  62.     forint i = 0; i < 7; i++)  
  63.     {  
  64.         l = findLowerBound(a, 9, i);  
  65.         u = findUpperBound(a, 9, i);  
  66.         if( l != -1)  
  67.         {  
  68.             printf("l:%d u:%d  (u-l):%d\n", l, u, u - l + 1);   
  69.         }  
  70.         else  
  71.         {  
  72.             printf("Can't find\n");  
  73.         }  
  74.     }  
  75.     return 0;  
  76.   
  77. }  

补充:(感谢NickMouse)

不过这个算法在极端的情况下,复杂度会降低到 O(n), 给出一些改进的代码

by NickMouse:

[cpp]  view plain copy
  1. #include <stdio.h>  
  2.   
  3. int bsearch_lowerbound(int a[],int n,int x)  
  4. {  
  5.     int l=0,r=n-1;  
  6.     while(l+1<r){  
  7.         int m=(l+r)/2;  
  8.         if(a[m]>=x)  
  9.             r=m;  
  10.         else  
  11.             l=m+1;  
  12.     }  
  13.     if(a[l]==x)  
  14.         return l;  
  15.     else if(a[r]==x)  
  16.         return r;  
  17.     else  
  18.         return -1;  
  19. }  
  20.   
  21. int bsearch_upperbound(int a[],int n,int x)  
  22. {  
  23.     int l=0,r=n-1;  
  24.     while(l+1<r){  
  25.         int m=(l+r)/2;  
  26.         if(a[m]<=x)  
  27.             l=m;  
  28.         else  
  29.             r=m-1;  
  30.     }  
  31.     if(a[r]==x)  
  32.         return r;  
  33.     else if(a[l]==x)  
  34.         return l;  
  35.     else  
  36.         return -1;  
  37. }  
  38.   
  39. int main()  
  40. {  
  41.     int a[] = {1,1, 2, 3, 3,3, 5};  
  42.   
  43.       
  44.   
  45.       
  46.     for(int i = 0; i < 6; i++)  
  47.     {  
  48.         printf("%d\n", bsearch_lowerbound(a, 7, i));  
  49.     }  
  50.   
  51.     return 0;  
  52. }  



5. 需找第一个大于(或小于) 指定数的数

     说起来有点不好懂,给个例子。

     例如升序数组  1 4 5 8, 如果输入的是 7, 我们应该返回8, 因为8 是第一个大于输入:7的数。

     其实这个算法有一个很重要的作用是在O(Nlog(N))的最长递增子序列里面,每次扫描到一个数,我们要知道这个数,可以作为长度是几的递增子序列的最后元素。

    

[cpp]  view plain copy
  1. //1 4 5 8 ---> 7  
  2. int FindFirstLarger(int a[], int nSize, int query)  
  3. {  
  4.     int lower = 0;   
  5.     int upper = nSize - 1;  
  6.     int m ;  
  7.     while(lower <= upper)  
  8.     {  
  9.         m = (lower + upper) / 2;  
  10.           
  11.         if(a[m] < query)  
  12.         {  
  13.             lower = m + 1;  
  14.         }  
  15.         else  
  16.         {  
  17.             upper = m - 1;  
  18.         }  
  19.     }  
  20.     return lower;  
  21. }  
  22. int main()  
  23. {  
  24.     int a[] = {1, 4, 5, 8}; //size is 9  
  25.       
  26.     for(int i = 0; i < 10; i++)  
  27.     {  
  28.         printf("i: %d  at: %d\n", i, FindFirstLarger(a, 4, i));  
  29.     }  
  30.   
  31.     return 0;  
  32. }  

6. 在 行列 排序的矩阵中里面需找某个元素


例如如下输入:


  1   5   7    10 

  2   6   8   15

  4   9  11  16

12 13 19  21

输入满足按行来看,是递增排序,按列也是递增排序。现在要找到某个元素,如果存在,则输出 -1

因为按行和按列是排好序的,所以对于任意一个元素来说,它的左边所有元素比它,它下面的元素比它大,利用这个性质,我们可以设计出一个 二分查找。这个查找从 第0行的最后一个元素开始。按照比较结果,决定向下,还是向左覆盖整个区间。


代码如下:

[cpp]  view plain copy
  1. bool SearchInRowColSortedMatrix(const int data[], int nRow, int nCol, int query, int &tRow, int &tCol)  
  2. {  
  3.      int iRow, iCol;  
  4.        
  5.      iRow = 0;  
  6.      iCol = nCol - 1;  
  7.      int t;   
  8.      while(iRow < nRow && iCol >= 0)  
  9.      {  
  10.          t = data[iRow * nCol + iCol];//get the current data.   
  11.            
  12.          if( t == query) //find it  
  13.          {  
  14.              tRow = iRow;  
  15.              tCol = iCol;  
  16.              return true;  
  17.          }  
  18.          else if( t < query) // eleminate the rest of the elements in the current row, who are less than t.   
  19.          {  
  20.              iRow++;  
  21.          }  
  22.          else //eleminate all the rest elements in current col, who are greater than t.  
  23.          {  
  24.              iCol--;  
  25.          }  
  26.      }  
  27.      return false;  
  28. }  

写在最后:

上面的代码可能有一些地方有bug, 虽然我做了一些测试,包括穷举所有内部元素的 正测试,还有不在查找数组中的反测试。 这些代码确实很容易出bug,对于一些大公司如MS等比较看重代码,要求bug-free的公司可能经常作为考题,来考察现场编程能力。 不过自己依然很水,还需要努力,努力写出bug-free的code

     

你可能感兴趣的:(算法,面试,测试,search,query,任务)