二分查找是在技术面试中经常出现的题目,首先这种题目考察思路,另外因为代码一般很短----不会超过50行。所以很适合做技术笔试,或者面试之类的题目出现。
之前做过一些题目,很多是BS算法的变种,我这里给出几个例子,算是做一个总结吧。
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)//算法需要注意的是 选择的 l 和u, 如果u 选择的是size的话,这个条件会不一样 { 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; }
这个问题的一种描述形式可能如下:
在给定的字符串序列中(按照字典序排列好的), 存在一些空串,请你找出给定字符串的位置,不在里面返回 -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') //这个位置稍微不小心就留下bug了,或许我这里依然有bug存在。 { 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()//test cases. { 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; }
这种问题有一个描述是说,已知我们有一个数组,它是在一个排序数组的基础上,用rotate的方式生成出来的。
例如: 3 4 5 1 2 就是一个符合上面说法的数组。
现在有如下两个任务:
此类问题的分析的时候,一个核心的思路应该是说,无论数组[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; }
如果我们查找的区间中 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]) //包括2个case, 1:搜索到最小区间 2: 全区间已经是排序区间,则直接返回最小那个数 { return lower; } int m = (lower + upper)/2; if(a[m] >= a[lower] )//如果 lower ... m 是排序区间,并且整个大的区间不是排序区间,那么下一步搜索转到 m + 1, upper. { //等于号为了防止 例如输入是 6 1这样的情况,在这个情况下 m == lower return findMin(a, m+1, upper); } else { return findMin(a, lower, m); //否则搜索当前区间 } } int main() // generate test case { 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; }
这个题目的描述可以是这样:
在一个排序好的数组中,有一些元素是重复的。 我们写一个函数,对给定的数,我们返回这个数出现的次数。
例如输入数据 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() //generate test case { int a[] = {1, 2, 2, 3, 3, 3, 5, 5, 5}; //size is 9 int l, u; //int *p = lower_bound(a, a + 9, 0); 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; }
说起来有点不好懂,给个例子。
例如升序数组 1 4 5 8, 如果输入的是 7, 我们应该返回8, 因为8 是第一个大于输入:7的数。
其实这个算法有一个很重要的作用是在O(Nlog(N))的最长递增子序列里面,每次扫描到一个数,我们要知道这个数,可以作为长度是几的递增子序列的最后元素。
//1 4 5 8 ---> 7 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}; //size is 9 for(int i = 0; i < 10; i++) { printf("i: %d at: %d\n", i, FindFirstLarger(a, 4, i)); } return 0; }
例如如下输入:
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];//get the current data. if( t == query) //find it { tRow = iRow; tCol = iCol; return true; } else if( t < query) // eleminate the rest of the elements in the current row, who are less than t. { iRow++; } else //eleminate all the rest elements in current col, who are greater than t. { iCol--; } } return false; }
上面的代码可能有一些地方有bug, 虽然我做了一些测试,包括穷举所有内部元素的 正测试,还有不在查找数组中的反测试。 这些代码确实很容易出bug,对于一些大公司如MS等比较看重代码,要求bug-free的公司可能经常作为考题,来考察现场编程能力。 不过自己依然很水,还需要努力,努力写出bug-free的code