字節跳動2019夏令營筆試總結

2019 ByteDance Summer Camp

19年夏令營,頭條請了天奇大神過去做talk,我是衝着天奇大佬去的,他是做DL編譯器的,跟我個人的研究方向很類似,所以很期望能跟他當面交流一下。

夏令營有兩次筆試機會,取成績最高的一次作爲最終的成績。由於消息看到得晚,在下開始申請的時候,第一次筆試已經結束了。本人只參加了第二次筆試,筆試題目構成如下:單選題3題、不定向選擇題1題、填空題2題、編程題3題、設計類1題,共5類題目,時間兩個半小時(晚上7點-9點半)。

summer camp 2019 img

筆試是在牛客網完成的,筆試有一個特殊的要求:某一類題型全部做完了才能進入下一個題型,而且該類題型提交以後就不能修改了。比如說,選擇題有3題,你把3題全部做完了,點提交,才能去做其他類型的題目,一旦提交了就不能回過頭修改。所以合理安排時間非常重要,不能卡在某一類題目上太多時間,我是把非編程題類題目全部做完以後纔去做的編程題。中途有事,第二道編程題提交完,出去了一下,回來後第三題還沒看完就被強制提交了:D

單選題一:位示圖管理。操作系統磁盤管理的問題:給定柱面、磁道和扇區數據,利用位示圖對存儲管理;

單選題二:二進制含0的數量。操作系統內存頁映射的問題:限定問題場景,1GB內存,劃分爲131072個內存塊。給定一段簡單的C++代碼,開了一個1024大小的int數組,一段循環對該數組的每個元素按照某個規律賦值,同時給定操作系統邏輯頁號和內存塊號的映射表。假設數組的邏輯地址是64C0,從物理地址A6BC取4個字節的數據。問該數據二進制0的個數。
該題目不是很難,但是手工計算量挺大的,131072=2^17(我除了好久才除出來:D),1GB=2^30B,所以塊大小是2^30B/2^17=2^13B=8KB,所以內存邏輯頁的layout關係大概是:

第0頁:0 ... 1FFF
第1頁:2000 ... 3FFF
第2頁:4000 ... 5FFF
第3頁:6000 ... 7FFF
第4頁:8000 ... 9FFF
第5頁:A000 ... BFFF
第6頁:C000 ... DFFF
第7頁:E000 ... FFFF
...

數組的邏輯起始地址是64C0,數組大小是1024個int,即4KB,半個內存頁。邏輯頁號從0開始,可以大概算出來,64C0所在的邏輯頁號爲3,頁內偏移爲4C0=1216
根據邏輯頁和物理塊的映射表,可以查出來,第3頁對應的物理塊號是第5塊。而物理地址A6BC恰好也在第5塊,物理地址的塊內偏移是6BC=1724,數組的大小是4KB,所以並不會出現跨頁的現象,所以從物理地址A6BC取4個字節的數據實際上取的數據是(1724-1216)/4=127,所以只要計算出數組的第127個數據,把該數據轉換成二進制表示,數一下0的個數即可。

單選題三:牙齒-集合,智力題。黑帽子白帽子問題(或者紅眼藍眼問題)的變種,當時嘗試着用數學歸納法,理性地去分析了一下,發現沒有選項,所以就隨便選了一個:-)

不定向選擇題:全局變量,操作系統多線程局部變量和全局變量問題。a是全局變量,初始值爲0,兩個線程分別執行以下代碼:

for (int i = 1; i <= 2; ++i)
  a += (i * ((i % 2) ? 1 : -1);

問最終a的可能取值是多少?
這裏的i是私有變量,每個線程會有單獨的副本,而a是全局變量,對每個線程都可見。把循環展開以後,每個線程的實際操作就是:

a += 1;
a += -2;

運算符+=相當於先讀後寫,有讀寫相關,所以每個線程的實際操作如下:

load R0, (a);   讀a的值
add R1, R0, 1;  計算臨時結果
store R1, (a);  寫回結果
load R2, (a);   讀a的值
add R3, R2, -2; 計算臨時結果
store R3, (a);  寫回計算結果

R表示讀,C表示計算,W表示寫,則可以簡寫成RCWRCW,R和W之間存在相關,W和W之間存在相關,對兩個線程枚舉出所有可能相關的情況(每個線程內部的指令相對有序),就是最終可能的結果。

填空題一:含6的個數。數學題,1^2 + 3^2 + 5^2 + ... + (2n-1)^2 = M,當n=5*10^10的時候,求M中含數字6的個數。拿到這一題,第一反應,純數學問題啊,但是M怎麼算出來呢?本人的記憶裏,1^2 + 2^2 + ... + n^2等於什麼呢?忘記了,大概是n(n+1)(2n+1)/6,手動舉了幾個corner case驗證了一下,確實是這個,那麼:

S0 = 1^2 + 2^2 + 3^2 + ... + (2n)^2 = 2n(2n+1)(2*2n+1)/6
S1 = 2^2 + 4^2 + ... + (2n)^2 = (2*1)^2 + (2*2)^2 + ... + (2*n)^2
   = 4*(1^2 + 2^2 + 3^2 + ... + n^2)
   = 4*n(n+1)(2n+1)/6
S0 = M + S1
所以,M = S0 - M = n(2n-1)(2n+1)/3

所以,當n = 5*10^10的時候,M的值直接代入公式,

M = 5*10^10*(2*5*10^10-1)(2*5*10^10+1)/3
  = 5*10^10*(10^11-1)(10^11+1)/3

M是個整數,所以(10^11 - 1)*(10^11 + 1)必定能被3整除,然後這個問題就成了一個找規律的問題了,x = (10^k - 1)*(10^k + 1)的結果很有規律:

k = 1, x = 99;
k = 2, x = 9999;
k = 3, x = 999999;
...

規律很明顯,當k=11的時候,(10^11-1)(10^11+1)=999...9,總共22個9,所以

M = 5*10^10*333...3(22個3)

所以答案也很明顯了,M應該有21個6。

填空題二:10枚外觀相同的硬幣,1枚假硬幣重量跟其他硬幣不同,給一個天平,最少稱重多少次能夠確定假硬幣,最多稱重多少次能確定假幣?而且題目也沒說假幣是比其他硬幣輕了還是重了,感覺這題歧義挺大的,看你怎麼理解最多和最少了。
我是這麼理解的,因爲題目只是說假幣跟其他硬幣質量不同,並沒有說假幣是輕還是重,所以不能按照5,5->2,2,1的方式去簡單地判斷。

如果剛開始分爲`5, 5`兩堆,那麼第1次稱重。
假設取了輕的一邊:
  爲了確認假幣就是輕的,你需要在輕的一邊中繼續確認,`5->2, 2, 1`三堆,第2次稱重2v2:
    如果2v2質量相同,你需要確認假幣是輕的還是重的,所以需要在4箇中隨機取1個,跟剩下的一個進行第3次1v1稱重:
      如果1v1質量不同,那麼剩下的那個肯定是假幣,而且這種情況的結果只能是假幣是輕的。
      如果1v1質量相同,那麼說明第1次稱重取錯了,可以確認假幣是重的。所以應該在另外5箇中再最多進行2次或者最少進行1次的稱重判斷。
    如果2v2質量不同,那麼可以確認假幣就是輕的,那麼再進行1次判斷即可。
假設取了重的一邊,思路類似。

所以在這種思維模式下,最少需要3次,最多需要5次。

看到羣裏有人說把硬幣分3, 3, 4三堆的情況。

先3v3第1次稱重:
  如果3v3質量相同,則假幣在4中,2v2第2次稱重:
    如果取了輕的一邊,那麼1v1第3次稱重;
      如果1v1質量相同,那麼說明取錯了,可以確認假幣就是重的;所以再進行第4次稱重就可以確認假幣;
      如果1v1質量不同,那隻可能是輕的是假幣。
    如果取了重的一邊,思路類似。
  如果3v3質量不同,則還需要判斷假幣的輕重:
    假設取了輕的一邊,先1v1進行第2次稱重:
      如果質量相同,需要確定假幣是輕還是重的,需要在2箇中隨便取一個跟剩下的進行第3次1v1稱重;
        如果1v1質量相同,可以確認假幣是重的,在重的3箇中在進行一次稱重就可以確認假幣;
        如果1v1質量不同,可以確認假幣是輕的,輕的一邊即爲假幣;
      如果質量不同,可以確認輕的一邊即爲假幣;
    假設取了重的一邊,思路類似。

所以在這種思維模式下,最少需要2次,最多需要4次。

見評論區@聖聆 的解釋,ta的解釋應該最符合出題者的本意。最少2次,最多3次。大概思路是:

把10枚硬幣分成3, 3, 3, 1共4堆,記爲a, b, c, d

若a v b質量相同,且a v c質量相同,則可以確定d爲假幣,總共2次稱重;
若a v b質量相同,且a v c質量不同:
  若c重,則假幣在c中,且爲重假幣,在c中再經過一次稱重就可以確定假幣;
  若c輕,則假幣在c中,且爲輕假幣,在c中再經過一次稱重就可以確定假幣;
若a v b質量不同,則稱重a v c:
  若a v c質量相同,則假幣在b,對比之前a v b的輕重情況就可以確定是重假幣還是輕假幣,在b中再經過一次稱重就可以確定假幣;
  若a v c質量不同,對比a v b,a v c的輕重情況。若b、c重,則輕假幣在a;若b、c輕則重假幣在a;知道輕重假幣後也再只需一次就可以稱出假幣;

這種思維方式應該最符合出題者的本意,所以最少2次,最多3次。

編程題一:立方體塔,題目大意:給定w個白色方塊和b個黑色方塊,用它們去搭一個塔。塔的要求是第h層有h個相同顏色的方塊。輸入是w和b,輸出塔的最高層數以及塔的種類(某一層顏色不同可以認爲是不同的種類)。

編程題二:變量名拆分。給定一個字符串,即變量名,和一個字符串集合,判斷能否拆開變量名,使得拆開後的字符串集合是給定字符串集合的子集。e.g.:輸入變量名:“thisisadog”,字符串集合:{“this”, “thisis”, “is”, “a”, “dog”},這個例子應該輸出True.
其實是多模式字符串匹配問題。

編程題三:寶石迷陣。時間有限,在下還沒看完題目就被強制提交了。

問答題:羣聊消息。系統設計問題,針對釘釘等企業應用的羣聊消息狀態(已讀或者未讀)功能設計核心數據結構。要求解決場景問題,指出操作流程和時間複雜度,並指出至少兩個關鍵挑戰。

感覺主要解決三個場景問題:

1. 新用戶加入或者退出羣聊;
2. 新消息的發佈;
3. 某個用戶讀取了某個消息。

當時主要想法是,實際應用場景中,消息的數目明顯多餘用戶數目的,所以設計的核心數據結構是一個hashmap,msg_id爲key,value是一個bitmap,長度爲用戶數目,bitmap以usr_id索引用戶對該消息的讀取狀態,0表示未讀,1表示讀取,初始化構造的時候,默認消息是未讀的。

  1. 當有新的用戶加入或者退出羣聊的時候,bitmap的長度需要更新,不考慮之前歷史信息的情況下(即後進羣的人看不到之前的消息),有新消息發佈的時候再對bitmap擴容。

  2. 新消息的發佈,則hashmap中需要插入新的記錄,bitmap的默認值全0,表示消息未讀取,複雜度O(1)。

  3. 用戶u讀取了消息m,則根據msg_id拿到位圖的bitmap,利用usr_id更新位圖的狀態。

msg_id和usr_id用64位整型表示,假設有N個用戶和M個消息,則佔用內存大小爲N*M/8.

所以,對外暴露的接口至少要有4個:

void usr_comming(long usr_id); // 新用戶加入羣聊
void usr_leaving(long usr_id); // 用戶退出羣聊
void new_msg_pub(long pub_usr_id, long msg_id); // 用戶發佈消息
void usr_read_msg(long usr_id, long msg_id); // 用戶讀取了某條消息

兩個關鍵挑戰:

  1. 用戶數目變化時,考慮到系統的複雜均衡,可以採用延時策略。對於用戶的減少,即用戶退出了羣聊,爲了保證系統的穩定,可以考慮並不立即縮短bitmap的寬度,可以延時到某個時刻再執行縮短操作;對於用戶的增加,

  2. 隨着消息數目增加,佔用的內存量也會增加,考慮到系統的存儲壓力,所以需要維護一定的時間窗口,把時間窗口之外的數據存到磁盤中。對於那些全部已讀的消息,也可以考慮把它們存到磁盤中。

個人總結

  • 單選題手工計算量挺大的,某一個小的環節算錯了,該題目基本就gg了;本人當時筆試的時候,內存頁表映射那題,加減法算錯了,怎麼都得不出選項:-)數學題的公式推了5分鐘,最後把忘記除以6,也挺無語的。所以需要很細心,不能大意;
  • 智力題有時候還是需要動腦子練習一下的;
  • 第一次做系統設計題,也不知道該從哪些角度去回答這些問題,經過此次總結,大概知道了如何回答系統設計題。首先需要分析應用場景,需要解決哪些核心的問題,然後封裝抽象出核心的數據結構,給出類的設計,以及暴露出來的核心接口,並簡述每個接口的實現流程並做複雜度分析。最後從系統擴展性的角度指出可能會出現的瓶頸問題;
  • 個人沒有任何找工作筆試的經驗,看來找工作之前還是有必要準備一下的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章