騰訊筆試題

feifeizhongda發表於2017-12-26

1、解釋const的含義及實現機制
const的含義及實現機制,比如:const int i,是怎麼做到i只可讀的?
答:const用來說明所定義的變數是隻讀的。
這些在編譯期間完成,編譯器可能使用常數直接替換掉對此變數的引用。

2、買200返100優惠券,實際上折扣是多少?

到商店裡買200的商品返還100優惠券(可以在本商店代替現金)。請問實際上折扣是多少?
答:由於優惠券可以代替現金,所以可以使用200元優惠券買東西,然後還可以獲得100元的優惠券。
假設開始時花了x元,那麼可以買到 x + x/2 + x/4 + ...的東西。所以實際上折扣是50%.(當然,大部分時候很難一直兌換下去,所以50%是折扣的上限)
如果使用優惠券買東西不能獲得新的優惠券,那麼
總過花去了200元,可以買到200+100元的商品,所以實際折扣為 200/300 = 67.7%.

3、簡述tcp三次握手的過程,accept發生在三次握手哪個階段?

要想明白Socket連線,先要明白TCP連線。裝置能夠使用聯網功能是因為裝置底層實現了TCP/IP協議,可以使裝置終端通過無線網路建立TCP連線。TCP協議可以對上層網路提供介面,使上層網路資料的傳輸建立在“無差別”的網路之上。
建立起一個TCP連線需要經過“三次握手”:
第一次握手:客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;
第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;
第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。

三次握手完成後,客戶端和伺服器就建立了tcp連線。這時可以呼叫accept函式獲得此連線。


握手過程中傳送的包裡不包含資料,三次握手完畢後,客戶端與伺服器才正式開始傳送資料。理想狀態下,TCP連線一旦建立,在通訊雙方中的任何一方主動關閉連線之前,TCP 連線都將被一直保持下去,如下圖:

斷開連線時伺服器和客戶端均可以主動發起斷開TCP連線的請求,斷開過程需要經過“四次揮手”(過程就不細寫了,就是伺服器和客戶端互動,最終確定斷開,如下圖)
由於TCP連線是全雙工的,因此每個方向都必須單獨進行關閉。這個原則是當一方完成它的資料傳送任務後就能傳送一個FIN來終止這個方向 的連線。收到一個 FIN只意味著這一方向上沒有資料流動,一個TCP連線在收到一個FIN後仍能傳送資料。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
(1)主動端A傳送一個FIN,用來關閉客戶A到被動端B的資料傳送。
(2)被動端B收到這個FIN,它發回一個ACK,確認序號為收到的序號加1。和SYN一樣,一個FIN將佔用一個序號。
(3)被動端B關閉與主動端A的連線,傳送一個FIN給主動端A。
(4)主動端A發回ACK報文確認,並將確認序號設定為收到序號加1。

4、用UDP協議通訊時怎樣得知目標機是否獲得了資料包?

答:可以在每個資料包中插入一個唯一的ID,比如timestamp或者遞增的int。傳送方在傳送資料時將此ID和傳送時間記錄在本地。接收方在收到資料後將ID再發給傳送方作為迴應傳送方如果收到迴應,則知道接收方已經收到相應的資料包;如果在指定時間內沒有收到迴應,則資料包可能丟失,需要重複上面的過程重新傳送一次,直到確定對方收到。

5、求一個論壇的線上人數,假設有一個論壇,其註冊ID有兩億個,每個ID從登陸到退出會向一個日誌檔案中記下登陸時間和退出時間,要求寫一個演算法統計一天中論壇的使用者線上分佈,取樣粒度為秒。
答:一天總共有 3600*24 = 86400秒。
定義一個長度為86400的整數陣列int delta[86400],每個整數對應這一秒的人數變化值,可能為正也可能為負。開始時將陣列元素都初始化為0。
然後依次讀入每個使用者的登入時間和退出時間,將與登入時間對應的整數值加1,將與退出時間對應的整數值減1。
這樣處理一遍後陣列中儲存了每秒中的人數變化情況。
定義另外一個長度為86400的整數陣列int online_num[86400],每個整數對應這一秒的論壇線上人數。
假設一天開始時論壇線上人數為0,則第1秒的人數online_num[0] = delta[0]。第n+1秒的人數online_num[n] = online_num[n-1] + delta[n]。
這樣我們就獲得了一天中任意時間的線上人數。

6、在一個檔案中有 10G 個整數,亂序排列,要求找出中位數。記憶體限制為 2G。
答:不妨假設10G個整數是64bit的。
2G記憶體可以存放256M個64bit整數。
我們可以將64bit的整數空間平均分成256M個取值範圍,用2G的記憶體對每個取值範圍內出現整數個數進行統計。這樣遍歷一邊10G整數後,我們便知道中數在那個範圍內出現,以及這個範圍內總共出現了多少個整數。
如果中數所在範圍出現的整數比較少,我們就可以對這個範圍內的整數進行排序,找到中數。如果這個範圍內出現的整數比較多,我們還可以採用同樣的方法將此範圍再次分成多個更小的範圍(256M=2^28,所以最多需要3次就可以將此範圍縮小到1,也就找到了中數)。

拿到此題目首先考慮的是記憶體的限制,因而無法用快速排序或是部分排序。若是求的是最大值或最小值,或是K小的值(k<2G)則可以採用堆排序O(NlogK)。但現在是求中位數即排在第5G和5G+1的數

演算法思路分析:假設是無符號整數,

第一步: 借鑑桶排序的思路,因為整數為32位,我們先按高16位2^16=64K進行計數,即分成64K段,這樣需要的計數陣列大小為2^16,若陣列型別為int型,存在缺點,若10G的數都是相同,這樣陣列存的計數最大為2^32=4G,就會出現溢位,所以陣列型別採用long long8位元組型。佔用記憶體為2^16*8B=518KB。而記憶體給了2G,可見利用得過少,表明還有很大的改進空間。 改進:分成2G/8B=2^28=256M段,這樣段越多,第二步掃描分析的資料就越少。

long long Counter[1 < <28];//256M桶

unsigned int x; 
memset(Counter,0,sizeof(Counter));

foreachnumber(x)
{ 
Counter[x>>4]++; //高28位
}

long long sum=0; 
for(i=0;i <1 < <28;i++)
{ 
sum+=Counter[i]; 
if(sum>=5LL < <30)break;//找到中位數所在的段
} 
sum-=5LL < <30; 
sum=Counter[i]-sum;//為達到5G,中位數所在的段需要的個數

第二步:前步已把10G資料按高28位分到了256M桶中,且已經找到中位數在哪一段,只要把此段按低4位分到16個段中,即可以找到

int segment=i; 
memset(Counter,0,sizeof(Counter)); 
foreachnumber(x)
{ 
if((x>>16)==segment)
{ 
   Counter[x&(~((-1) < <16))]++; //低4位。 -1的8位二進位制表示為11111111
} 
} 
long long lsum=0; 
for(i=0;i <1 < <4;i++)
{ 
lsum+=Counter[i]; 
if(lsum>=sum)break;

}
int keynum = (segment<<4)|(i);

總共只要讀兩遍整數,對每個整數也只是常數時間的操作,總體來說是線性時間

若是有符號的整數,只需改變對映即可

解法三:

關於中位數:資料排序後,位置在最中間的數值。即將資料分成兩部分,一部分大於該數值,一部分小於該數值。中位數的位置:當樣本數為奇數時,中位數=(N+1)/2 ; 當樣本數為偶數時,中位數為N/2與1+N/2的均值(那麼10G個數的中位數,就第5G大的數與第5G+1大的數的均值了)。

分析:明顯是一道工程性很強的題目,和一般的查詢中位數的題目有幾點不同。
1. 原資料不能讀進記憶體,不然可以用快速選擇,如果數的範圍合適的話還可以考慮桶排序或者計數排序,但這裡假設是32位整數,仍有4G種取值,需要一個16G大小的陣列來計數。

2. 若看成從N個數中找出第K大的數,如果K個數可以讀進記憶體,可以利用最小或最大堆,但這裡K=N/2,有5G個數,仍然不能讀進記憶體。

3. 接上,對於N個數和K個數都不能一次讀進記憶體的情況,《程式設計之美》裡給出一個方案:設k<K,且k個數可以完全讀進記憶體,那麼先構建k個數的堆,先找出第0到k大的數,再掃描一遍陣列找出第k+1到2k的數,再掃描直到找出第K個數。雖然每次時間大約是nlog(k),但需要掃描ceil(K/k)次,這裡要掃描5次。

解法:首先假設是32位無符號整數。
1. 讀一遍10G個整數,把整數對映到256M個區段中,用一個64位無符號整數給每個相應區段記數。
說明:整數範圍是0 - 2^32 - 1,一共有4G種取值,對映到256M個區段,則每個區段有16(4G/256M = 16)種值,每16個值算一段, 0~15是第1段,16~31是第2段,……2^32-16 ~2^32-1是第256M段。一個64位無符號整數最大值是0~8G-1,這裡先不考慮溢位的情況。總共佔用記憶體256M×8B=2GB。

2. 從前到後對每一段的計數累加,當累加的和超過5G時停止,找出這個區段(即累加停止時達到的區段,也是中位數所在的區段)的數值範圍,設為[a,a+15],同時記錄累加到前一個區段的總數,設為m。然後,釋放除這個區段佔用的記憶體。

3. 再讀一遍10G個整數,把在[a,a+15]內的每個值計數,即有16個計數。

4. 對新的計數依次累加,每次的和設為n,當m+n的值超過5G時停止,此時的這個計數所對應的數就是中位數。

總結:
1.以上方法只要讀兩遍整數,對每個整數也只是常數時間的操作,總體來說是線性時間。

2. 考慮其他情況。
若是有符號的整數,只需改變對映即可。若是64為整數,則增加每個區段的範圍,那麼在第二次讀數時,要考慮更多的計數。若過某個計數溢位,那麼可認定所在的區段或代表整數為所求,這裡只需做好相應的處理。噢,忘了還要找第5G+1大的數了,相信有了以上的成果,找到這個數也不難了吧。

3. 時空權衡。
花費256個區段也許只是恰好配合2GB的記憶體(其實也不是,呵呵)。可以增大區段範圍,減少區段數目,節省一些記憶體,雖然增加第二部分的對單個數值的計數,但第一部分對每個區段的計數加快了(總體改變??待測)。

4. 對映時儘量用位操作,由於每個區段的起點都是2的整數冪,對映起來也很方便。

 7、兩個整數集合A和B,求其交集。

答:(1) 讀取整數集合A中的整數,將讀到的整數插入到map中,並將對應的值設為1。
(2)讀取整數集合B中的整數,如果該整數在map中並且值為1,則將此數加入到交集當中,並將在map中的對應值改為2。
通過更改map中的值,避免了將同樣的值輸出兩次。

8、有1到10w這10w個數,去除2個並打亂次序,如何找出那兩個數?
答:解法一:申請10w個bit的空間,每個bit代表一個數字是否出現過。
開始時將這10w個bit都初始化為0,表示所有數字都沒有出現過。
然後依次讀入已經打亂循序的數字,並將對應的bit設為1。
當處理完所有數字後,根據為0的bit得出沒有出現的數字。
解法二:首先計算1到10w的和,平方和。
然後計算給定數字的和,平方和。
兩次的到的數字相減,可以得到這兩個數字的和,平方和。
所以我們有
x + y = n
x^2 + y^2 = m
解方程可以得到x和y的值。

9、有1000瓶水,其中有一瓶有毒,小白鼠只要嘗一點帶毒的水24小時後就會死亡,至少要多少隻小白鼠才能在24小時時鑑別出那瓶水有毒?
答:最容易想到的就是用1000只小白鼠,每隻喝一瓶。但顯然這不是最好答案。
既然每隻小白鼠喝一瓶不是最好答案,那就應該每隻小白鼠喝多瓶。那每隻應該喝多少瓶呢?
首先讓我們換種問法,如果有x只小白鼠,那麼24小時內可以從多少瓶水中找出那瓶有毒的?
由於每隻小白鼠都只有死或者活這兩種結果,所以x只小白鼠最大可以表示2^x種結果。如果讓每種結果都對應到某瓶水有毒,那麼也就可以從2^x瓶水中找到有毒的那瓶水。那如何來實現這種對應關係呢?
第一隻小白鼠喝第1到2^(x-1)瓶,第二隻小白鼠喝第1到第2^(x-2)和第2^(x-1)+1到第2^(x-1) + 2^(x-2)瓶....以此類推。
回到此題,總過1000瓶水,所以需要最少10只小白鼠。

10、給40億個不重複的unsigned int的整數,沒排過序的,然後再給幾個數,如何快速判斷這幾個數是否在那40億個數當中?

答:unsigned int 的取值範圍是0到2^32-1。我們可以申請連續的2^32/8=512M的記憶體,用每一個bit對應一個unsigned int數字。首先將512M記憶體都初始化為0,然後每處理一個數字就將其對應的bit設定為1。當需要查詢時,直接找到對應bit,看其值是0還是1即可。

 

相關文章