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

二分查找是在技术面试中经常出现的题目,首先这种题目考察思路,另外因为代码一般很短----不会超过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

     

 

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