二分查找是在技术面试中经常出现的题目,首先这种题目考察思路,另外因为代码一般很短----不会超过50行。所以很适合做技术笔试,或者面试之类的题目出现。
之前做过一些题目,很多是BS算法的变种,我这里给出几个例子,算是做一个总结吧。
1. 传统的Binary Search
1.1. 最普通的BS算法就是给定一个排好序的数组,然后查找一个数是否在数组内,如果在给出下标,如果不在则返回-1.
- template<typename T>
- int binarySearch(const T vData[], int nSize, const T &query)
- {
- int l, u, m;
-
- l = 0;
- u = nSize - 1;
-
- while(l <= u)
- {
- m = (l + u)/2;
-
- if(vData[m] == query)
- {
- return m;
- }
- else if(vData[m] > query)
- {
- u = m - 1;
- }
- else
- {
- l = m + 1;
- }
- }
- return -1;
- }
1. 2 比普通的BS算法稍微复杂一点的应该是在字典中查找一个指定的字符串了。
对于输入的所有单词,我们可以使用排序算法使得所有单词按照字典序排列,然后用BS算法找到给定的元素的下标。
代码如下:
- #define MAXCHARLEN 50
- int binarySearch(const char directionary[][MAXCHARLEN], int nSize, const char* pQuery)
- {
- int l, u;
-
- l = 0;
- u = nSize - 1;
- int m;
- int cmpResult;
- while(l <= u)
- {
- m = (l + u)/2;
- cmpResult = strcmp(directionary[m], pQuery);
-
- if(cmpResult == 0)
- {
- return m;
- }
- else if(cmpResult == 1)
- {
- u = m - 1;
- }
- else
- {
- l = m + 1;
- }
-
- }
- return -1;
- }
2. 含有空的Bianry Search
这个问题的一种描述形式可能如下:
在给定的字符串序列中(按照字典序排列好的), 存在一些空串,请你找出给定字符串的位置,不在里面返回 -1.
例如 char directionary[] = {"", "a", "ab", "", "", "", "bb", "cc", "zz", "" ""} 中需找 "ab";
这里需要添加一些处理,在找到binary search的中心点以后,如果是空,我们要移动当前的m
- #include<stdio.h>
- #include <string.h>
- #include<memory.h>
-
- #define MAXCHARLEN 50
- int binarySearch(const char directionary[][MAXCHARLEN], int nSize, const char* pQuery)
- {
- int l, u;
-
- l = 0;
- u = nSize - 1;
- int m;
- int cmpResult;
- while(l <= u)
- {
- m = (l + u)/2;
- while(m > l && directionary[m][0] == '\0')
- {
- m--;
- }
- cmpResult = strcmp(directionary[m], pQuery);
-
- if(cmpResult == 0)
- {
- return m;
- }
- else if(cmpResult == 1)
- {
- u = m - 1;
- }
- else
- {
- l = m + 1;
- }
-
- }
- return -1;
- }
- int main()
- {
-
- const char directionary[][MAXCHARLEN] =
- {
- "", "aa", "", "bb", "", "cc", "dd", "e", "", "", "ff"
- };
-
- const char query[][MAXCHARLEN] =
- {
- "aa", "bb", "cc", "dd", "kk", "e"
- };
-
- for( int i = 0; i < 6; i++)
- {
- printf("%d ", binarySearch(directionary, 11, query[i]));
- }
- return 0;
- }
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,虽然看起来很简单。
- #include<stdio.h>
- #include <string.h>
- #include<memory.h>
-
- bool InRange(int l, int u, int query)
- {
- return query >= l && query <= u;
- }
- int binarySearch(int rotatedArray[], int nSize, int query)
- {
- int lower, upper;
-
- lower = 0, upper = nSize - 1;
- int m;
-
- while(lower <= upper)
- {
- m = (lower + upper)/2;
-
- if(rotatedArray[m] == query)
- {
- return m;
- }
-
- if(rotatedArray[m] >= rotatedArray[lower])
- {
- if(InRange(rotatedArray[lower], rotatedArray[m], query))
- {
- upper = m - 1;
- }
- else
- {
- lower = m + 1;
- }
- }
- else
- {
- if(InRange(rotatedArray[m], rotatedArray[upper], query))
- {
- lower = m + 1;
-
- }
- else
- {
- upper = m - 1;
- }
- }
-
- }
-
- return -1;
-
- }
- int main()
- {
-
- int a[] = {3, 5, 7, 9, 0, 2};
-
- for(int i = 0; i < 10; i++)
- {
- printf("i: %d is at %d\n",i, binarySearch(a, 6, i));
- }
- return 0;
- }
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].
- int findMin(int a[], int lower, int upper)
- {
- if(a[lower] <= a[upper])
- {
- return lower;
- }
- int m = (lower + upper)/2;
-
- if(a[m] >= a[lower] )
- {
- return findMin(a, m+1, upper);
- }
- else
- {
- return findMin(a, lower, m);
- }
- }
- int main()
- {
- int a[10];
- for( int i = 0; i < 6; i++)
- {
- for( int j = 0; j < 6; j++)
- {
- a[ (i + j)%6] = j;
- }
- for( int j = 0; j < 6; j++)
- {
- printf("%d ", a[j]);
- }
- printf("\nMin at %d\n", findMin(a, 0, 5));
- }
- return 0;
- }
4. 统计出现的次数
这个题目的描述可以是这样:
在一个排序好的数组中,有一些元素是重复的。 我们写一个函数,对给定的数,我们返回这个数出现的次数。
例如输入数据 1 2 2 3 3 3 5 5,如果输入2, 返回2,因为2在数组中出现2次。
这个问题可以引入两个子问题,寻找到 Query(Q)第一次出现的位置,和Q最后一次出现的位置。
我们可以称之为 lower_bound, 和 upper_bound
- int findLowerBound(int a[], int nSize, int query)
- {
- int lower, upper;
- int m;
- lower = 0, upper = nSize - 1;
-
- while(lower <= upper)
- {
- m = (lower + upper)>>1;
-
- if(a[m] == query)
- {
- while(a[--m] == query);
- return m+1;
- }
- else if(a[m] > query)
- {
- upper = m - 1;
- }
- else
- {
- lower = m + 1;
- }
- }
-
- return -1;
- }
-
- int findUpperBound(int a[], int nSize, int query)
- {
- int lower, upper, m;
-
- lower = 0, upper = nSize - 1;
-
- while(lower <= upper)
- {
- m = (lower + upper) >> 1;
-
- if(a[m] == query)
- {
- while(a[++m] == query);
- return m-1;
-
- }
- else if(a[m] > query)
- {
- upper = m - 1;
-
- }
- else
- {
- lower = m + 1;
- }
- }
- return -1;
- }
- int main()
- {
- int a[] = {1, 2, 2, 3, 3, 3, 5, 5, 5};
- int l, u;
-
- for( int i = 0; i < 7; i++)
- {
- l = findLowerBound(a, 9, i);
- u = findUpperBound(a, 9, i);
- if( l != -1)
- {
- printf("l:%d u:%d (u-l):%d\n", l, u, u - l + 1);
- }
- else
- {
- printf("Can't find\n");
- }
- }
- return 0;
-
- }
补充:(感谢NickMouse)
不过这个算法在极端的情况下,复杂度会降低到 O(n), 给出一些改进的代码
by NickMouse:
- #include <stdio.h>
-
- int bsearch_lowerbound(int a[],int n,int x)
- {
- int l=0,r=n-1;
- while(l+1<r){
- int m=(l+r)/2;
- if(a[m]>=x)
- r=m;
- else
- l=m+1;
- }
- if(a[l]==x)
- return l;
- else if(a[r]==x)
- return r;
- else
- return -1;
- }
-
- int bsearch_upperbound(int a[],int n,int x)
- {
- int l=0,r=n-1;
- while(l+1<r){
- int m=(l+r)/2;
- if(a[m]<=x)
- l=m;
- else
- r=m-1;
- }
- if(a[r]==x)
- return r;
- else if(a[l]==x)
- return l;
- else
- return -1;
- }
-
- int main()
- {
- int a[] = {1,1, 2, 3, 3,3, 5};
-
-
-
-
- for(int i = 0; i < 6; i++)
- {
- printf("%d\n", bsearch_lowerbound(a, 7, i));
- }
-
- return 0;
- }
5. 需找第一个大于(或小于) 指定数的数
说起来有点不好懂,给个例子。
例如升序数组 1 4 5 8, 如果输入的是 7, 我们应该返回8, 因为8 是第一个大于输入:7的数。
其实这个算法有一个很重要的作用是在O(Nlog(N))的最长递增子序列里面,每次扫描到一个数,我们要知道这个数,可以作为长度是几的递增子序列的最后元素。
-
- int FindFirstLarger(int a[], int nSize, int query)
- {
- int lower = 0;
- int upper = nSize - 1;
- int m ;
- while(lower <= upper)
- {
- m = (lower + upper) / 2;
-
- if(a[m] < query)
- {
- lower = m + 1;
- }
- else
- {
- upper = m - 1;
- }
- }
- return lower;
- }
- int main()
- {
- int a[] = {1, 4, 5, 8};
-
- for(int i = 0; i < 10; i++)
- {
- printf("i: %d at: %d\n", i, FindFirstLarger(a, 4, i));
- }
-
- return 0;
- }
6. 在 行列 排序的矩阵中里面需找某个元素
例如如下输入:
1 5 7 10
2 6 8 15
4 9 11 16
12 13 19 21
输入满足按行来看,是递增排序,按列也是递增排序。现在要找到某个元素,如果存在,则输出 -1
因为按行和按列是排好序的,所以对于任意一个元素来说,它的左边所有元素比它,它下面的元素比它大,利用这个性质,我们可以设计出一个 二分查找。这个查找从 第0行的最后一个元素开始。按照比较结果,决定向下,还是向左覆盖整个区间。
代码如下:
- bool SearchInRowColSortedMatrix(const int data[], int nRow, int nCol, int query, int &tRow, int &tCol)
- {
- int iRow, iCol;
-
- iRow = 0;
- iCol = nCol - 1;
- int t;
- while(iRow < nRow && iCol >= 0)
- {
- t = data[iRow * nCol + iCol];
-
- if( t == query)
- {
- tRow = iRow;
- tCol = iCol;
- return true;
- }
- else if( t < query)
- {
- iRow++;
- }
- else
- {
- iCol--;
- }
- }
- return false;
- }
写在最后:
上面的代码可能有一些地方有bug, 虽然我做了一些测试,包括穷举所有内部元素的 正测试,还有不在查找数组中的反测试。 这些代码确实很容易出bug,对于一些大公司如MS等比较看重代码,要求bug-free的公司可能经常作为考题,来考察现场编程能力。 不过自己依然很水,还需要努力,努力写出bug-free的code