二分查找是在技术面试中经常出现的题目,首先这种题目考察思路,另外因为代码一般很短----不会超过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)//算法需要注意的是 选择的 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;
}
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') //这个位置稍微不小心就留下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;
}
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]) //包括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;
}
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() //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;
}
5. 需找第一个大于(或小于) 指定数的数
说起来有点不好懂,给个例子。
例如升序数组 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;
}
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];//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