手机分配短讯id的面试题目.docx

上传人:b****5 文档编号:5372122 上传时间:2022-12-15 格式:DOCX 页数:25 大小:33.49KB
下载 相关 举报
手机分配短讯id的面试题目.docx_第1页
第1页 / 共25页
手机分配短讯id的面试题目.docx_第2页
第2页 / 共25页
手机分配短讯id的面试题目.docx_第3页
第3页 / 共25页
手机分配短讯id的面试题目.docx_第4页
第4页 / 共25页
手机分配短讯id的面试题目.docx_第5页
第5页 / 共25页
点击查看更多>>
下载资源
资源描述

手机分配短讯id的面试题目.docx

《手机分配短讯id的面试题目.docx》由会员分享,可在线阅读,更多相关《手机分配短讯id的面试题目.docx(25页珍藏版)》请在冰豆网上搜索。

手机分配短讯id的面试题目.docx

手机分配短讯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字节

//注:

因为n

bytelinear_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++的内

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 院校资料

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1