最近在人人网上看到有同学分享各公司笔试面试题汇总,看到百度有道题,觉得比较有意思。因为题目看很简单,我当时就想百度为什么出这样的问题,因为在编程过程中根本不会遇到样的问题,并且当时给出了一种“打趣”的解法,即下面说到的SumGet方法。
【百度面试题】有101个数,为[1,100]之间的数,其中一个数是重复的,如何寻找这个重复的数,其时间复杂度和空间复杂度是多少?
看着就觉得好笑,出题人怎么会出这样的题?让人觉得出题人没有水平。我觉得像百度这样的大公司,至少这样出才比较合理:有2^31个数,都是在[1, 2^32-1]之间的整数,请写出一个算法,判断这些数中是否有重复的数,如果有,请都输出来;最后给出算法的时间空间复杂度。最近不是都流行大数据,大处理量吗?什么竞赛不也是这样训练的吗?
扯得有点远,还是回到这道题上来吧。我觉得学计算机的肯定对排序是特别熟悉,据说%之80到90的问题都可以用排序来解决,难怪有那么多排序方法。我想一般人遇到百度这道题,都会想到用排序方法(%之8910的成功率放在那不用,傻啊!)。排序方法有很多,如何选择那就看个人擅长了,反正我是比较晕。如果我记得不错的话,像这种初始顺序都没定的排序,时间复杂度至少是o(nlgn),空间复杂度至少是o(n)。诚然排序是万能的方法,但是万能的方法不一定是好方法。因为这样的方法往往会忽略问题的本质,而对问题来说,本质才是最重要的,才是解决问题的关键。
下面我将写写我对这道题的本质思考得到的三种方法(以下方法都对应函数的名字)。
一、SumGet方法
这个题说只有一个数是重复的,那撇开这个数,也就是说1到100的数都会出现,于是我把所有101个数相加起来,最后再减去1到100的和,不就出来了吗?
这种方法的时间和空间复杂度都是o(n)。这种方法也就是投机取巧而已,虽然解决了问题,而且似乎还不错,但是没有什么推广性,下次百度出:2^32-1个数,为[1,2^32-1]之间的数,其中一个数是重复的,……你就傻眼了,因为要溢出了。虽然对溢出做一些处理也是可以用的。
二、FlagGet方法
做标记是一个好方法。可以创建一个数组flags[100],初始化各元素都为0,然后对要求数组遍历,将flags数组对应的元素置为1,如遍历到50这个数了,那么就令flags[50-1] = 1。于是在遍历过程中看flags对应元素是否为1,如果为1,说明已经找到,可以结束了。
这种方法的时间和空间复杂度也是o(n)。不足之处就是要申请一个数组,如果数组比较大的话就完了。
三、JumpGet方法
这种方法是对FlagGet方法的改进,不用创建数组,唯一的区别在于它将原数组改变了。
有点不太好说,举例子比较形象化。比如初始数组为numbers,numbers[23]值为71,那么我就jump到numbers[71-1]处,得知它的值为15,那么将numbers[71-1]令为0,再jump到numbers[15-1]处,如此反复,直到jump到一个为0的位置,说明之前已经jump来了,在jump到这里,就说明我这次jump的数字就是那个重复的数字。等等,有个问题。如何选择jump的第一个位置呢?如果选择的numbers[23]值为24,那不是原地jump吗?而且有可能出现这样的循环,如:numbers[23]=71,numbers[70]=15,numbers[14]=24。其实,有一个位置是其他位置都jump不到的,那就是numbers[100],因为没有数会等于101。所以以这个位置为初始点是最好的,它不会回来,那种循环的点它又进不去,那就只有一种可能,通过不同的位置jump到同一位置。于是找到答案了。
这种方法的时间和空间复杂度也是o(n)。
附上源代码吧。
#include <iostream>
#include <algorithm>
#include <ctime>
#include <cstdlib>
#define NUM 100
using namespace std;
int SumGet(int numbers[NUM+1])
{
int sum = 0;
for (int i = 0; i <= NUM; i++)
sum += numbers[i];
return (sum-(NUM+1)*NUM/2);
}
int FlagGet(int numbers[NUM+1])
{
int flags[NUM+1];
int num;
int searchNums = 0;
for (int i = 0; i <= NUM; i++)
flags[i] = 0;
for (int i = 0; i <= NUM; i++) {
if (flags[numbers[i]-1] == 1) {
num = numbers[i];
break;
}
searchNums++;
flags[numbers[i]-1] = 1;
}
cout << "searchNums = " << searchNums << endl;
return num;
}
int JumpGet(int numbers[NUM+1])
{
int jumpto = numbers[NUM];
int lastjump = 0;
int jumpNums = 0;
while (jumpto != 0) {
lastjump = jumpto;
jumpto = numbers[lastjump-1];
numbers[lastjump-1] = 0;
jumpNums++;
cout << "-->" << lastjump;
}
cout << endl << "jumpNums = " << jumpNums << endl;
return lastjump;
}
int main()
{
int numbers[NUM+1];
int num = 0;
for (int i = 0; i <= NUM; i++)
numbers[i] = i+1;
while ( num <= 0 || num > NUM) {
cout << "input a number between 1 to 100 :";
cin >> num;
}
numbers[NUM] = num;
srand(unsigned(time(NULL)));
random_shuffle(numbers, numbers+NUM+1);
for (int i = 0; i <= NUM; i++)
cout << numbers[i] << " ";
cout << endl;
cout << "SumGet get :" << SumGet(numbers) << endl;
cout << "FlagGet get :" << FlagGet(numbers) << endl;
cout << "JumpGet get :" << JumpGet(numbers) << endl;
return 0;
}
最后,我想说:百度这道题出得还是蛮好的。不知道有没有这样的寓意,就是要从百个千个的应聘者中找出这么一个出来。我还是想抱怨一下,我们做的难题还不够吗,我们不就是从难题中爬出来,进了大学,NM出来了还让我们做难题,让不让人活啊!
Linux之父Linus说的好,“做简单的事,做到完美”。也不太确定是不是他说的,确实很有道理。