Spiga

手机分配短讯id的面试题目(厘清需求篇)

2010-08-31 01:42 by Milo Yip, 9727 visits, 收藏, 编辑

前阵子,笔者在TopLanguage论坛里参与讨论了一个不错的面试题目,在此和大家分享,也当作个人的讨论总结。本文列出该问题,并模拟应试者向面试官的对话,以厘清问题需求。

题目原文

事缘Dbger发起的帖子中,liuxinyu举了一个面试题目,原文如下:

有个老的手机短信程序,由于当时的手机CPU,内存都很烂。所以这个短信程序只能记住256条短信,多了就删了。

每个短信有个唯一的ID,在0到255之间。当然用户可能自己删短信,现在我们收到了一个新短信,请分配给它一个唯一的ID。说白了,就是请你写一个函数:

byte function(byte* ids);

该函数接受一个ids数组,返回一个可用的ID,由于手机很破,我要求你的程序尽量快,并少用内存。注意:ids是无序的。

厘清需求

笔者认为,面试时,并不一定要急着解答问题。面试(interview)并非考试(examination),面试中双方可透过交谈去评估对方是否合适的雇主/雇员。若对面试题目有疑问,应尽量沟通厘清。这也反映实际软件开发时,程序员须具备足够的问题需求分析和沟通能力。笔者相信,大部分负责任的面试官,也喜见应试者提出切合实际的问题。

以下笔者尝试以对话形式,虚构一个厘清需求的情境。 (甲=面试官,乙=应试者)

甲微笑着说:你对纸上的题目清楚么?

乙认真地看着试题,犹豫片刻。

乙突然发问:在这个函数原型中,ids应该是一个非固定长度的数组,代表现时已分配的ID集合。那么该函数怎样可以得知ids数组的长度呢?我想到,由于0-255都是id的范围,似乎不可用简单的结束符,例如像null-terminated string那样用0去表示数组的结束。

突然而來的长问题,轮到甲犹豫了,立刻把纸上题目快读一遍,发现似乎真是个问题。

甲:嗯……这是个好问题,可能是出题同事的手误。这样吧,加上一个size_t n的参数,代表ids数组的有效元素数目。

乙轻轻点头,觉得这个修正最为简单直觉。之后更放心把想到的问题都提出来。

乙:想确认一点,n是否必然少于255,换句话说,该函数必然能正常输出一个id?

甲:是的。本题目不要求处理其他输入错误的情况,例如ids为NULL、ids里有重复值等。可假定输入数据都是有效的。

乙:想确认一点,传回的id只要不存在于ids数组便可,不需传回最小的id吧?

甲:没错,没这个需求。

乙有点不好意思地说:对于函数原型,我还有个问题……

甲心想不是还有手误吧。但甲还是带着微笑,示意继续问。

乙:其实也是关于ids数组的。由于原型没写明为const byte *,想问此函数可否改动ids数组的内容?或者再进一步说,此函数是否纯函数(pure function),没有副作用(side effect)?

甲松了一口气,因为这不算是问题的缺陷。总算保住公司的面子。

甲充满信心地说:对于ids数组的内容是否能改变,你可分为两个情况去考虑。而除了这个参数之外,此函数不会有其他副作用。

乙:哦,明白了。此外,虽然我未考虑周详,但似乎要获得更高运行效率,便需要额外的存储状态,或许要设另一个接口才行。

甲喜形于色地回答:是的,也许可以透过空间换取时间。那么你对接口有提议么?

乙边说边写:我想,或许另一种接口的形式是这样的……

struct manager {
    // 这里有一些状态变量(暂未决定)

    byte alloc();
    void dealloc(byte id);
};

甲:好。这个是比原题目更抽像的情况。若时间足够,你也可在答题时对这种接口进行补充。

乙:最后想问,该手机的CPU是32位的吧?

甲没有预先对此设限,就顺着回答了。

甲:就当作是32位的吧。

乙:我暂时没想到其他问题了。

甲:那好,你可以开始在纸上作答,我们一小时后再讨论。

乙:谢谢您。我现在对题目的需求应有充分理解了。我会尽量想出多个解。

甲觉得乙有细心阅读题目,发问的内容和技巧也不错。乙也认为甲能耐心地聆听,并给与合理清晰的指示,甚至给与他一些弹性、一些发挥的机会。

最后一句中,乙提及的「多个解」也使甲留下印象和期待。因为很多时候,某个问题并不一定有最优解,能设想多个解,并分析比较每个解的优缺点,能体现程序员的基础能力。

预告

解答篇应于本周五发表(已如期发表)。有兴趣的读者不妨自己试试,多想几个不同的解。若不熟悉C/C++,也可用诸如C#、Java等语言作答。其实本题并不难,适合一般编程职位的面试。笔者将会提供的解,有些来自笔者,有些来自网友,也非什么神奇特别的解,只是作为讨论的总结,和大家分享。也许读者会想到更好的解呢。

Add your comment

31 条回复

  1. #1楼 Dbger      2010-08-31 06:11
    的确,面试者如此在答题前能问出如此高水平的问题,本身已经是能力的一种证明了。
     回复 引用 查看   
  2. #2楼 kinetics      2010-08-31 08:42
    等待牛牛的解答
     回复 引用 查看   
  3. #3楼 补丁      2010-08-31 08:58
    作为考官,我很想把这个面试的砍死
     回复 引用 查看   
  4. #4楼 assiwe      2010-08-31 09:12
    引用补丁:作为考官,我很想把这个面试的砍死

    +1
     回复 引用 查看   
  5. #5楼 Tony Qu      2010-08-31 09:22
    toplanguage貌似是google groups论坛,莫非楼主是爬墙上的?
     回复 引用 查看   
  6. #6楼 ChrisPei      2010-08-31 09:35
    @Tony Qu
    楼主不在大陆
     回复 引用 查看   
  7. #7楼 knight_stalker      2010-08-31 10:07
    第一时间想到的最简单而且非常慢的解法是差集的首元素…… 完整代码(Ruby)如下:

    IDS = (0..255).to_a
    def next_id(ids)
      (IDS - ids).first
    end
    


    如果 ids 包含了 0 到 255 所有数字,返回 nil。
     回复 引用 查看   
  8. #8楼[楼主] Milo Yip      2010-08-31 10:17
    @assiwe
    @补丁
    引用补丁:作为考官,我很想把这个面试的砍死

    哈哈。或許我寫得不好,面試的不夠低調。讀完這個去面試,有甚麼三長兩短不要怪我啊 :)
     回复 引用 查看   
  9. #9楼[楼主] Milo Yip      2010-08-31 10:19
    @Tony Qu
    引用Tony Qu:toplanguage貌似是google groups论坛,莫非楼主是爬墙上的?

    不需的,多數人是用mailing list。
     回复 引用 查看   
  10. #10楼 atyuwen      2010-08-31 10:20
    发个解答 bitmap + bsf...未经测试

    static inline unsigned long find_first_bit(unsigned long x)
    {
    __asm bsf eax, x;
    }

    //////////////////////////////////////////////////////////////////////////
    // preconditions...
    //////////////////////////////////////////////////////////////////////////
    int id_allocate(unsigned char *ids, unsigned int n)
    {
    unsigned long bitmap[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
    for (int i = 0; i != n; ++i)
    {
    int id = ids[i], k = id / 32, r = id % 32;
    bitmap[k] ^= (1 << r);
    }

    for (int i = 0; i != 8; ++i)
    {
    if (bitmap[i] != 0) return 32 * i + find_first_bit(bitmap[i]);
    }
    return -1;
    }
     回复 引用 查看   
  11. #11楼[楼主] Milo Yip      2010-08-31 10:20
    @ChrisPei
    引用ChrisPei:
    @Tony Qu
    楼主不在大陆

    又是看不到頁面的About吧……這個皮膚是有點問題。我在上海。
     回复 引用 查看   
  12. #12楼[楼主] Milo Yip      2010-08-31 10:22
    @knight_stalker
    引用knight_stalker:
    第一时间想到的最简单而且非常慢的解法是差集的首元素……

    這是可以的。不過用腳本比較麻煩的是,難以計算所需內存(包括stack)和時間複雜度。
     回复 引用 查看   
  13. #13楼 暗香浮动      2010-08-31 10:25
    @ChrisPei
    爬墙已经成为常态了.楼主在上海吧.
     回复 引用 查看   
  14. #14楼 农村的芬芳      2010-08-31 12:36
    楼主在香港
     回复 引用 查看   
  15. #15楼 DiryBoy      2010-08-31 13:12
    难说这个byte*是不是一个按位储存已用id的数组呢,这样一来长度就是固定的32了~
    按照这种理解的代码:
    byte function(byte* ids)
    {
      byte i, pos = 0;
      for ( i = 0; i < 32; i++ )
      {
        byte info = *(ids+i);
        if ( info == 255 ) continue;
        info = info ^ (info + 1);
        while ( info = info >> 1 ) pos++;
        break;
      }
      return i*8+pos;
    }
    
     回复 引用 查看   
  16. #16楼 小No      2010-08-31 13:21
    引用Milo Yip:
    @Tony Qu
    引用Tony Qu:toplanguage貌似是google groups论坛,莫非楼主是爬墙上的?

    不需的,多數人是用mailing list。


    嗯,我关注NHbiernate的时候也是用mailing list,
    不知道有没有人搞一个mailing list版的facebook,这样就不用翻墙了
     回复 引用 查看   
  17. #17楼 hwpayg      2010-08-31 16:24
    #include<stdio.h>
    #include<string.h>
    #define BYTE unsigned char
    int main(void)
    {
    	int i;
    	unsigned short us[255];
    	BYTE b[]={0,1,2,3,4,5,6,7,8,9,10,11,13,15,12,17,14};
    	for(i=0;i<=255;i++)
    	{
    		us[i]=i;
    	}
    	for(i=0;i<sizeof(b)/sizeof(unsigned char);i++)
    	{
    		int temp;
    		temp=b[i];
    		us[temp]=256;
    	}
    	for(i=0;i<=255;i++)
    	{
    		if(us[i]!=256)
    		{
    		printf("%d",us[i]);
    		break;
    		}
    	}
    
    	return 0;
    }
    
    
     回复 引用 查看   
  18. #18楼 Wei.SteVe      2010-08-31 19:07
    引用hwpayg:
    #include<stdio.h>
    #include<string.h>
    #define BYTE unsigned char
    int main(void)
    {
    	int i;
    	unsigned short us[255];
    	BYTE b[]={0,1,2,3,4,5,6,7,8,9,10,11,13,15,12,17,14};
    	for(i=0;i<=255;i++)
    	{
    		us[i]=i;
    	}
    	for(i=0;i<sizeof(b)/sizeof(unsigned char);i++)
    	{
    		int temp;
    		temp=b[i];
    		us[temp]=256;
    	}
    	for(i=0;i<=255;i++)
    	{
    		if(us[i]!=256)
    		{
    		printf("%d",us[i]);
    		break;
    		}
    	}
    
    	return 0;
    }
    
    

    效率较差
     回复 引用 查看   
  19. #19楼 knight_stalker      2010-08-31 20:43
    白天米时间整 C 代码……

    其实题目本来就是比较奇怪,需要尽量快,但给的条件里,最好的情况也需要遍历完 ids。应该说设计本身就有问题 ……

    乙的回答中,设计抽象的接口是最聪明的(也会让考官想抽人)。
    如果把 alloc 和 dealloc 当成内存池一般管理起来的话,可以获得最快的结果O(1),代码也不会过于"黑客之光",只是吃内存比较厉害:

    manager.h
    #pragma once
    
    typedef unsigned char byte;
    
    typedef struct {
    	// 返回可用 id 0-255,若无法分配,返回 -1
    	int (*alloc)();
    	// 回收 id
    	void (*dealloc)(byte id);
    } Manager;
    
    // 使用 manager 之前应先 init
    extern void init_manager();
    
    // 分配id: id = manager.alloc()
    // 回收id: manager.dealloc(id)
    extern Manager manager;
    


    回忆一下 便雅悯 C++ 里面的 pool allocator 范例和各种链表操作,实现如下:

    manager.c
    #include "manager.h"
    
    // 链表单元
    struct Link {
    	byte id;
    	struct Link* next;
    };
    static struct Link chunk[256]; // 内存
    typedef struct Link* LinkP;
    // 环链表,tail->next = head
    static LinkP head;
    static LinkP tail;
    
    static int alloc() {
    	byte id;
    	if (!head)
    		return -1; // 没 id 了
    	id = head->id;
    	if (tail == head) { // 分配最后一个元素
    		head = 0;
    		tail = 0;
    	} else {
    		head = head->next;
    		tail->next = head;
    	}
    	chunk[id].next = 0; // 标记为已分配
    	return id;
    }
    
    static void dealloc(byte id) {
    	LinkP link = (LinkP)chunk + id;
    	if (link->next) return; // 已经回收过了
    	if (head) {
    		link->next = head;
    		head = link;
    		tail->next = head;
    	} else { // 回收第一个元素
    		head = link;
    		tail = link;
    		link->next = link;
    	}
    }
    
    void init_manager() {
    	int i;
    	LinkP next = 0;
    	LinkP link = (LinkP)chunk;
    	// 初始化 chunk,把未分配单元构造成环链表
    	for (i=0; i<256; i++, link++) {
    		link->id = (byte)i;
    		link->next = next;
    		next = link;
    	}
    	tail = (LinkP)chunk;
    	head = tail + 255;
    	tail->next = head;
    	// 完成 manager
    	manager.alloc = alloc;
    	manager.dealloc = dealloc;
    }
    
    Manager manager;
    

     回复 引用 查看   
  20. #20楼 Joyer Huang      2010-09-01 01:04
    这个,
    static int point_at_free_list_head, pointer_at_free_list_tail;
    然后就是简单的问题问题了。
     回复 引用 查看   
  21. #21楼 feng wang      2010-09-01 10:19
    其实这个接口
    引用byte function(byte* ids);
    也是可以接受的,不需要额外的长度参数.
    比如认为 ids 指向一个长度为 256 个位 的内存区域, 每个位映射了短信使用情况(0:未使用, 1:使用)

    所以可以将我在 TL 的解法简化为这样(未测试)
    byte function(byte* ids)
    {      //其实这里应该首先处理一下内存不对齐的情形,不过貌似好的编译器会帮忙优化
           unsigned long* data_ = (unsigned long*) ids; 
           size_t tag = 0;
           for ( size_t i = 0; i < 8; ++i )
           {
               tag = (~data_[i]) & (data_[i]+1);
               if ( tag )
               {
                   data_[i] |= tag;
                   tag |= tag - 1;
                   tag -= (tag>>1) & 0x55555555;
                   tag = (tag & 0x33333333) + ((tag >> 2) & 0x33333333);
                   tag = (tag + (tag >> 4)) & 0x0F0F0F0F;
                   tag += tag >> 8;
                   tag += tag >> 16;
                   tag &= 0x3F;
                   tag += i << 5;
                   return tag;
               }
           }
           return -1; //error
    }
    
     回复 引用 查看   
  22. #22楼[楼主] Milo Yip      2010-09-01 10:50
    @knight_stalker
    每個pointer要32-bit啊,可考慮用byte做array index。
    P.S. Hack's Delight應該譯作黑客的樂事之類吧 :)
     回复 引用 查看   
  23. #23楼[楼主] Milo Yip      2010-09-01 10:54
    @feng wang
    其實原題目應該意味著ids內的是ID,而且
    引用注意:ids是无序的。

    不過我沒看懂那堆bit operation的意思。能解譯一下麼?
     回复 引用 查看   
  24. #24楼[楼主] Milo Yip      2010-09-01 10:56
    @hwpayg
    若用閣下的方法,us用bool/char就可以了。0(false)表示集合中沒有該ID,1(true)表示有。
     回复 引用 查看   
  25. #25楼 feng wang      2010-09-01 11:35
    引用Milo Yip:
    @feng wang
    其實原題目應該意味著ids內的是ID,而且
    引用注意:ids是无序的。

    不過我沒看懂那堆bit operation的意思。能解譯一下麼?

    //析出data_[i]中最右边的0, 11110111 -> 00001000,如没有则全为0
    tag = (~data_[i]) & (data_[i]+1);
    



                           //将 data_[i]中最右边的0置1, 11110111 -> 11111111 
    	               data_[i] |= tag;
                           //将 tag 后边所有的0置 1, 00010000 -> 00011111
    	               tag |= tag - 1;
                           //下边统计tag中1的个数,分治算法, 在 Hacker's Delight 中有论述
                          //思路是将一个32位的计数分为两个16位的,然后将结果合并; 而16位的计数可以分解为两个8位的, 依次类推 
                          
    	               tag -= (tag>>1) & 0x55555555;
                           
    	               tag = (tag & 0x33333333) + ((tag >> 2) & 0x33333333);
                           
    	               tag = (tag + (tag >> 4)) & 0x0F0F0F0F;
    	               tag += tag >> 8;
                          
    	               tag += tag >> 16;
    	               tag &= 0x3F;
    	               tag += i << 5;
    
     回复 引用 查看   
  26. #26楼 airwolf2026      2010-09-01 13:42
    引用Milo Yip:
    @ChrisPei
    引用ChrisPei:
    @Tony Qu
    楼主不在大陆

    又是看不到頁面的About吧……這個皮膚是有點問題。我在上海。


    为啥那么多人不看about?莫非真的他们看不到?可能ie6???哈哈
     回复 引用 查看   
  27. #27楼 hwpayg      2010-09-01 14:05
    引用Milo Yip:
    @hwpayg
    若用閣下的方法,us用bool/char就可以了。0(false)表示集合中沒有該ID,1(true)表示有。

    us用bool/char行么?想了很久发现确实可以。
     回复 引用 查看   
  28. #28楼 feng wang      2010-09-01 14:45
    @Milo Yip
    引用Milo Yip:
    @feng wang
    其實原題目應該意味著ids內的是ID,而且
    引用注意:ids是无序的。

    不過我沒看懂那堆bit operation的意思。能解譯一下麼?


    我理解的是: 正因为ids是无序的, 所以里边存储的不应该是id, 而是将 id 用某种算法处理之后映射到的一块内存区间; 因为 ids 里边如果是 id 的无序存放,那么查找/删除之类的操作都将非常耗费时间, 我想不出将数据结构设计得这么笨拙的理由。
     回复 引用 查看   
  29. #29楼 大船      2010-09-02 18:05
    看到这个面试题,感觉简直和Programming Pearls的开篇一模一样:D
    准确的理解需求真的是太重要了。
    解法也差不多,都是利用bitmap。用一个256长度的char数组做mapping是比较快的方法。

    byte getValidMsgId(byte *ids, size_t n)
    {
        char bitmap[256];
        byte i = 0;
     
        memset(bitmap, 0, 256);
        while (i < n)
            bitmap[ids[i++]] = 1;
        
        for (i = 0; i < 256; i++)
        {
            if (bitmap[i] == 0)
            return i;
        }
    }
    
    int main()
    {
        byte id;
        byte ids[] = {0, 1, 13, 12, 5, 10, 3, 2, 4, 9, 100, 78, 23, 11, 7, 19, 95};
        size_t len = sizeof(ids)/sizeof(byte);
    
        id = getValidMsgId(ids, len);
        printf("we get id number = %d\n", (int)id);
        return 0;
    }
    

    每次返回的是合法的最小id号。
     回复 引用 查看   
  30. #30楼 013231      2010-09-02 23:30
    C#代码,不修改接口.
        /*
         * 不修改接口:
         * C#中数组长度可由Length属性读出,无需传递数组长度参数.
         * 用32*8个标志位表示id是否存在.第n个标志位为0表示不存在值为n的id,为1表示存在值为n的id.
         * 首先遍历ids数组确定标志位,再检测每个标志位,遇到第一个为0的标志位即返回.
         * 按题意不处理任何可能的输入错误.
         */
        static class Program
        {
            static void Main(string[] args)
            {
                //测试用例
                byte[] ids = { 0, 1, 2, 33, 255, 5, 31, 32 };
                function(ids).Print();
            }
            static byte function(byte[] ids)
            {
                Int32[] signs = new Int32[8];//32*8个标志位
                int idsCount = ids.Length;
                for (int i = 0; i != idsCount; ++i)
                {
                    int signIntIndex = ids[i] >> 5;//高3位;
                    signs[signIntIndex] = signs[signIntIndex] | 1 << (ids[i] & 0x1F);
                    //Console.WriteLine("{0}:\tsings[{1}]=sings[{1}]|{{1<<{2}}}",ids[i], singIntIndex, ids[i] & 0x1F);
                }
                for (int i = 0; i != 8; ++i)
                    for (int j = 0; j != 32; ++j)
                        if ((signs[i] & 1 << j) == 0)
                            return (byte)(i * 32 + j);
                return 0;
            }
        }
    
     回复 引用 查看   
  31. #31楼 013231      2010-09-03 00:21
    C#,使用manager接口:
        /*使用manager接口:
         * 分配id时将signs置1,取消分配时置0
         */
        class manager
        {
            Int32[] signs = new Int32[8];
            public byte alloc()
            {
                for (int i = 0; i != 8; ++i)
                    for (int j = 0; j != 32; ++j)
                        if ((signs[i] & 1 << j) == 0)
                        {
                            signs[i] = signs[i] | 1 << j;
                            return (byte)(i * 32 + j);
                        }
                return 0;
            }
            public void dealloc(byte id)
            {
                int signIntIndex = id >> 5;//高3位;
                signs[signIntIndex] = signs[signIntIndex] & ~(1 << (id & 0x1F));
            }
        }
    

    
            static void Main(string[] args)
            {
                //测试用例
                manager mng = new manager();
                mng.alloc().Print();
                mng.alloc().Print();
                mng.dealloc(0);
                mng.alloc().Print();
                mng.alloc().Print();
                for (int i = 0; i != 256 - 4; ++i)
                    mng.alloc();
                mng.alloc().Print();
                mng.dealloc(128);
                mng.alloc().Print();
            }
    

    输出0,1,0,2,255,128
     回复 引用 查看