手机分配短讯id的面试题目.docx
《手机分配短讯id的面试题目.docx》由会员分享,可在线阅读,更多相关《手机分配短讯id的面试题目.docx(25页珍藏版)》请在冰豆网上搜索。
手机分配短讯id的面试题目
问题分析
原来的问题是要从一个无序ids数组里分配一个id。
我们可以用数学方式去更清楚地说明这个问题。
设m=256为所有id的个数,集合
为所有id的集合。
那么,给定一个已分配id的集合
,
(即参数ids),本题目可表示为,求一个
(即传回的id),符合条件:
减号是补集的意思,即x属于U但不属于A。
上回的对答已确定
,即
必然存在。
此外,这个条件又可以写成:
以上两种表达式可说明此问题的两种解法,一种编程方向是查找U集里有没有不属于A的id,而另种是计算A的补集再取出其中一个id。
纯函数API的解
实现程序之前,如果可以,应先写测试函数。
笔者认为,若面试者在情况容许下,也可在解答题目之前,写下测试程序。
如果有多个面试者能同样解题,或许同时写下测试程序的面试者能脱颖而出。
测试函数
为了简单起见,笔者使用了assert()来检测正确性,只于Debug版本有效。
而Release版本则用来测试效能。
由于U集合的子集合很多,
,不可能穷举所有可能集合。
所以,只能够举出随机的集合以作测试。
以下是一些常数(宏)及类型声明,TEST_COUNT是测试次数,而TEST_REPEATCOUNT是为了测试效能时,重覆测试的次数(即Release版本会调用测试函数一百万次):
1
2
3
4
5
6
7
8
9
10
11
12
13
#defineM256//ID的数目,且所有ID在[0,M)的区间内
#defineTEST_COUNT10000
#ifdefNDEBUG
#defineTEST_REPEATCOUNT100
#else
#defineTEST_REPEATCOUNT1
#endif
typedef unsignedchar byte;
typedef unsignedlong dword;
typedef byte(*idalloc_func)(byte*,size_t);
首先,写一个帮助函数测试某id是否在ids集合之内(不熟C++的读者可参考C版本):
1
2
3
4
5
6
//检测ids里是否含id(C++版本)
inline bool contain(byte*ids,size_t n,byteid){
assert(ids!
=NULL);
return find(ids,ids+n,id)!
=ids+n;
}
1
2
3
4
5
6
7
8
9
//检测ids里是否含id(C版本)
inline bool contain(byte*ids,size_t n,byteid){
assert(ids!
=NULL);
for (size_t i=0;i if (ids[i]==id)
return true;
return false;
}
笔者首先写了一个测试平均情况的测试平台函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//测试平均情况
void test_average(idalloc_funcidalloc){
assert(idalloc!
=NULL);
byteids[M];
for (size_t i=0;i ids[i]=(byte)i;
srand(0);//使每次测试的伪随机数相同
size_t n=0;
for (int test=0;test random_shuffle(ids,ids+M);//把整个数组洗牌
for (int repeat=0;repeat byteid=idalloc(ids,n);
(void)id;
assert(!
contain(ids,n,id));
//测试是否最小的id
for (size_t i=0;i assert(contain(ids,n,(byte)i));
}
n=(n+1)%M;
}
}
简单解释。
首先,把ids数组填入所有id值。
利用random_shuffle()把把整个ids数组洗牌,而n则是在[0,M)区间里循环递增。
由于笔者给出的解,都能传回最小的id,所以也会测试这条件。
而最坏情况,就是ids含无序的{0,1,...M-2},分配到的id为M-1,笔者也为此编了一个最坏情况的效能测试函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//测试最坏情况(ids为无序的[0,M-2],结果必然是id=M-1)
void test_worst(idalloc_funcidalloc){
assert(idalloc!
=NULL);
const size_t n=M-1;
byteids[n];
srand(0);//使每次测试的伪随机数相同
for (size_t i=0;i ids[i]=(byte)i;
for (int test=0;test random_shuffle(ids,ids+n);
for (int repeat=0;repeat byteid=idalloc(ids,n);
(void)id;
assert(id==M-1);
}
}
}
线性查找
最简单的想法,可能是遍历所整个U集合(即0至M-1),并使用contain()函数检测该id是否不包含在ids数组里。
1
2
3
4
5
6
7
8
9
10
11
12
13
//线性查找(总是传回最小id)
//时间复杂度:
O(n^2)
//临时内存大小:
0字节
//注:
因为nbytelinear_search(byte*ids,size_t n){
assert(ids!
=NULL);
assert(n
//逐个id检查是否存在于[ids,ids+n)
for (byteid=0;;id++)
if (!
contain(ids,n,id))
return id;
}
二分查找
网友Doyle在TL里提出了用二分查找的主意。
笔者实现了两种形式,以下这个是不需额外内存。
原理是把U集合分割为两个各占一半的区间,分别数算两个区间内的已分配元素数目,若元素数目少于区间大小,即代表该区间内有未分配的id。
再继续分割该区间,直至区间内都是可分配的id(即找到的元素是零)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//数ids内有多少个id在[min,max)的区间内
inline size_t count_interval(byte*ids,size_t n,size_t min,size_t max){
size_t count=0;
for (size_t i=0;i if (ids[i]>=min&&ids[i] count++;
return count;
}
//二分查找(总是传回最小id)
//时间复杂度:
O(nlgn)
//临时内存大小:
0字节
bytebinary_search(byte*ids,size_t n){
assert(ids!
=NULL);
assert(n
size_t l=0,r=M;
for(;;){
size_t c=(l+r)/2;//把id范围从[l,r)分割为[l,c),[c,r)两个区间
size_t count;
//以下的条件测试次序保证了传回最小id
if ((count=count_interval(ids,n,l,c)) if (count==0)
return (byte)l;
r=c;
}
else if ((count=count_interval(ids,n,c,r)) if (count==0)
return (byte)c;
l=c;
}
else
assert(false);//因为n }
}
这算法在最坏情况比线性查找快,但平均情况下却不一定。
排序
以上两个解,都是查找的方式,毋需改动数据。
相反,另一类解用的算法需改动ids数组内的元素,或是把ids复制到另一个临时数组里进行更改型的算法。
最简单的算法,是把无序的ids排序。
之后就可以从头开始扫描未分配的id。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//排序(总是传回最小id)
//时间复杂度:
O(nlgn)
//临时内存大小:
M字节(如果可改变ids则是0)
bytesort_stl(byte*ids,size_t n){
assert(ids!
=NULL);
assert(n
bytebuffer[M];
memcpy(buffer,ids,n);
sort(buffer,buffer+n);//平均O(nlgn)
for (size_t i=0;i if (buffer[i]!
=i)
return (byte)i;
return (byte)n;
}
但读者可能会想到,把整个数组排序可能会做了很多无用工。
而且,快速排序(quicksort)的最坏时间复杂度是O(n^2)。
因此,就有了下一个解。
堆
笔者想到的另一个解是使用堆(heap)数据结构。
堆可保证第一个元素是最小的元素(通常是最大的,但这题目里我们希望取得最小的),而每次弹出这个元素,取出第二小的元素只需要O(lgn)的时间。
sort_stl()需要完整排序,而使用堆则是逐步进行的,中途找到没用到的id就可以停下来,所以平均来说会省下很多时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//堆(总是传回最小id)
//时间复杂度:
O(nlgn)
//临时内存大小:
M字节(如果可改变ids则是0)
byteheap_stl(byte*ids,size_t n){
assert(ids!
=NULL);
assert(n
bytebuffer[M];
memcpy(buffer,ids,n);
byte*end=buffer+n;
make_heap(buffer,end,greater());//O(n)
for (byteid=0;buffer!
=end;id++,end--){
if (buffer[0]!
=id)
return id;
pop_heap(buffer,end,greater());//O(lgn)
}
return (byte)n;
}
最坏的情况,是要把最小的M-1个元素最弹出,才能求得id=M-1。
这情况其实等价于堆排序(heapsort)。
剖分
另一个方法和二分查找相似,就是把数组剖分(partition)为两部分,这应该是Doyle提出的原意。
原理是,设一个中间c=M/2,用它把无序ids集合剖分为两个无序集合,前一个集合的元素小于c,后一个的元素大于或等于c。
那么,应该有一个集合的元素数量少于id区间的大小,再把该集合继续剖分,直至变成空集。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//剖分(总是传回最小id)
//时间复杂度:
O(n)
//临时内存大小:
M字节(如果可改变ids则是0)
bytepartition_stl(byte*ids,size_t n){
assert(ids!
=NULL);
assert(n
bytebuffer[M];
memcpy(buffer,ids,n);
byte*first=buffer,*last=buffer+n;
size_t l=0,r=M;
for (;;){
size_t c=(l+r)/2;
byte*middle=partition(first,last,bind2nd(less(),c));//O(n)
//后置条件:
l<=[first,middle)内元素
//以下的条件测试次序保证了传回最小id
if (first==middle)
return (byte)l;
else if ((size_t)distance(first,middle) last=middle;
r=c;
}
else if (middle==last)
return (byte)c;
else if ((size_t)distance(middle,last) first=middle;
l=c;
}
else
assert(false);
}
}
此算法的妙处在于,时间复杂度仅为O(n)!
为什么呢?
因为partition()的时间复杂度是O(n),而此算法中每个迭代需处理的元素是n,n/2,n/4,...,把这个几何数列求和,得出2n,所以此算法为线性时间。
布尔集合
也许,最多网友都想到的解,就是把ids无序数组变换为另一个集合表示方式,能更快地测试A是否不含某id。
这种表达方式是使用一个布尔数组(booleanarray),储存某id是否在ids无序数组里。
用数学方式,可以称这个数组为一个函数
:
建立这个数组之后,再扫描一次,找出没使用到的id。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//布尔集合(总是传回最小id)
//时间复杂度:
O(n)
//临时内存大小:
M字节
byteboolset(byte*ids,size_t n){
assert(ids!
=NULL);
assert(n
bool id_used[M]={false };
//填充id_used
for (size_t i=0;i assert(!
id_used[ids[i]]);//此处断言失败代表ids有重复元素
id_used[ids[i]]=true;
}
//扫描id_used去找出最小未用id
for (size_t i=0;i if (!
id_used[i])
return (byte)i;
assert(false);
return 0;
}
这类解法在纯函数API中是最快的,但必须使用额外内存。
位集合
上述的解,每个数组元素由于只需储存1个位(bit),可以把8个布尔值置于字节里,减少额外内存。
这种集合称为位集合(bitset)或位图(bitmap)。
此外,在32位CPU上,可一次检查32位是否全0或全1,这可是一个优化。
这次,我们直接储存补集A,即是那些分配了的id会把位设为0,那么在扫描时就不需做一个not位元运算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//位集合(总是传回最小id)
//时间复杂度:
O(n)
//临时内存大小:
floor((M+31)/32)*4字节
bytebitset_standard(byte*ids,size_t n){
assert(ids!
=NULL);
assert(n
const size_t dword_count=(M+31)/32;
dwordid_unused_bits[dword_count];
//开始时设全部id为未用(即设位为1)
memset(id_unused_bits,~0,sizeof(id_unused_bits));
//填充id_unused_bits(ids内的位清为0)
for (size_t i=0;i size_t index=ids[i]/32;
dwordbitIndex=ids[i]%32;
assert(id_unused_bits[index]&(1< id_unused_bits[index]^=(1< }
//扫描id_unused_bits,找出最小未用id
for (size_t index=0;index if (dwordbits=id_unused_bits[index]){
for (dwordbitIndex=0;bitIndex<32;bitIndex++)
if (bits&(1< dwordid=index*32+bitIndex;
assert(id return (byte)id;
}
}
}
assert(false);
return 0;
}
在某些CPU上,还会支持一个汇编指令bsf(bitscanforward),可扫描一个32位值里,第一个为1的位索引(从LSB至MSB)。
这正正是我们想要的。
以下使用了VisualC++的内