百度2011.10.16校園招聘會筆試題

weixin_34219944發表於2013-08-14

 一、演算法設計
1、設rand(s,t)返回[s,t]之間的隨機小數,利用該函式在一個半徑為R的圓內找隨機n個點,並給出時間複雜度分析。

思路:這個使用數學中的極座標來解決,先呼叫[s1,t1]隨機產生一個數r,歸一化後乘以半徑,得到R*(r-s1)/(t1-s1),然後在呼叫[s2,t2]隨機產生一個數a,歸一化後得到角度:360*(a-s2)/(t2-s2)

其他思路:由於rand(s,t)可以返回[s,t]之間的隨機小數。對於一個點,我們可以根據兩個條件得到它:a.如果能夠隨機求出點的x和y座標。b:已知點到圓心的距離和該連線的角度,也可以。

第一種情況:x = rand(-1,1)*R;y = rand(-1,1)*R則點(x,y)就是其中一個點。迴圈N次得到N個點。

第二種情況 :l = R*rand(-1,1).角度θ  =  arcsin(rand(-1,1));則x = l*sinθ ;y = l*cosθ

設rand(s,t)的時間複雜度為T(n),則總的時間複雜度為O(2n*T(n)) ===>O(n*T(n))

2、(蓄水池問題)為分析使用者行為,系統常需儲存使用者的一些query,但因query非常多,故系統不能全存,設系統每天只存m個query,現設計一個演算法,對使用者請求的query進行隨機選擇m個,請給一個方案,使得每個query被抽中的概率相等,並分析之,注意:不到最後一刻,並不知使用者的總請求量。

思路:如果使用者查詢的數量小於m,那麼直接就存起來。如果使用者查詢的數量大於m,假設為m+i,那麼在1-----m+i之間隨機產生一個數,如果選擇的是前面m條查詢進行存取,那麼概率為m/(m+i),如果選擇的是後面i條記錄中的查詢,那麼用這個記錄來替換前面m條查詢記錄的概率為m/(m+i)*(1-1/m)=(m-1)/(m+i),當查詢記錄量很大的時候,m/(m+i)== (m-1)/(m+i),所以每個query被抽中的概率是相等的。

解析:本體考察的是抽樣演算法。由於不知道總的請求量,故不能根據總數求出每條記錄被抽取的概率。有一種演算法是蓄水池抽樣。該演算法的大致思路是:對於前m條記錄。百分百抽中。對於之後的記錄(t = m+1,m+2,........),取隨機數rand(0,t)。如果結果落在【0,m)之間,則選中該記錄,並隨機替換前面選中的m條記錄中的某一條記錄。這樣在第n條記錄時刻,每條記錄被選中的概率為m/n,是等概率的。

該思路對應的程式碼如下:

#include <stdio.h>
#include <stdlib.h>
#define N 10000
#define M 20

int getRandM(int *a,int n,int *result,int m){
    int i,j;
    for(i = 0;i < m;i++){
        result[i] = a[i];
    }

    for(j = i; j < n; j++){
        int randN = rand()%j;
        if(randN >= 0 && randN  < m){
            int randChange = rand()%m;
            result[randChange] = a[j];
        }
    }
}

int main(){
    int inStream[N];
    for( int i = 0;i < N;i++){
        inStream[i] = i;
    }
    int outStream[M];
    getRandM(inStream,N,outStream,M);
    for(int i = 0;i<M;i++){
        printf("%d ",outStream[i]);
    }
    printf("\n");
}

3、C++ STL中vector的相關問題:
(1)、呼叫push_back時,其內部的記憶體分配是如何進行的?
(2)、呼叫clear時,內部是如何具體實現的?若想將其記憶體釋放,該如何操作?
為了提高效率,實際上vector 並不是隨每一個元素的插入而增長自己,而是當vector 需要增長自身時,它實際分配的空間比當前所需的空間要多一些.。也就是說它分配了一些額外的記憶體容量,額外容量的確切數目由具體實現定義,這個策略使容器的增長效率更高.但是你刪除資料的時候,它卻不會縮小。vector為了防止大量分配連續記憶體的開銷,保持一塊預設的尺寸的記憶體,clear只是清資料了,未清記憶體,因為vector的capacity容量未變化,系統維護一個的預設值。

引用總結(http://www.cnblogs.com/kex1n/archive/2011/10/19/2217342.html):

vector的工作原理是系統預先分配一塊CAPACITY大小的空間,當插入的資料超過這個空間的時候,這塊空間會讓某種方式擴充套件,但是你刪除資料的時候,它卻不會縮小。
vector為了防止大量分配連續記憶體的開銷,保持一塊預設的尺寸的記憶體,clear只是清資料了,未清記憶體,因為vector的capacity容量未變化,系統維護一個的預設值。

有什麼方法可以釋放掉vector中佔用的全部記憶體呢?

標準的解決方法如下
template < class T >
void ClearVector( vector< T >& vt )
{
      vector< T > vtTemp;
      veTemp.swap( vt );
}

事實上,vector根本就不管記憶體,它只是負責向記憶體管理框架acquire/release記憶體,記憶體管理框架如果發現記憶體不夠了,就malloc,但是當vector釋放資源的時候(比如destruct), stl根本就不呼叫free以減少記憶體,因為記憶體分配在stl的底層:stl假定如果你需要更多的資源就代表你以後也可能需要這麼多資源(你的list, hashmap也是用這些記憶體),所以就沒必要不停地malloc/free。如果是這個邏輯的話這可能是個trade-off

一般的STL記憶體管理器allocator都是用記憶體池來管理記憶體的,所以某個容器申請記憶體或釋放記憶體都只是影響到記憶體池的剩餘記憶體量,而不是真的把記憶體歸還給系統。這樣做一是為了避免記憶體碎片,二是提高了記憶體申請和釋放的效率——不用每次都在系統記憶體裡尋找一番。

二、系統設計
正常使用者端每分鐘最多發一個請求至服務端,服務端需做一個異常客戶端行為的過濾系統,設伺服器在某一刻收到客戶端A的一個請求,則1分鐘內的客戶端任何其它請求都需要被過濾,現知每一客戶端都有一個IPv6地址可作為其ID,客戶端個數太多,以至於無法全部放到單臺伺服器的記憶體hash表中,現需簡單設計一個系統,使用支援高效的過濾,可使用多臺機器,但要求使用的機器越少越好,請將關鍵的設計和思想用圖表和程式碼表現出來。
解析:該題目的考點有:hash。分散式。首先伺服器接收到客戶端的請求,然後根據客戶端的ID值,根據某個特定的hash函式(例如ip各段和 mod n),將該客戶的請求轉向某一臺子伺服器,由該伺服器代理該客戶端的請求並作相應的過濾。其他客戶的請求也與之相同,需要注意的是考慮各個子伺服器的負載均衡,使得幾乎每臺子伺服器處理的客戶機的數量是接近平均分配的。如果還要處理客戶的其他請求,且考慮到子伺服器可能當機的情況,需要做分散式一致hash,使得子伺服器當機時,不會有太多的客戶請求受到影響。

分散式一致hash的原理圖:

關於分散式一致hash的詳細介紹,可以參考這篇文章:

http://blog.csdn.net/sparkliang/article/details/5279393

三、求一個全排列函式:
如p([1,2,3])輸出:
[123]、[132]、[213]、[231]、[321]、[312]
 求一個組合函式。

方法1:依次從字串中取出一個字元作為最終排列的第一個字元,對剩餘字元組成的字串生成全排列,最終結果為取出的字元和剩餘子串全排列的組合。

#include <iostream>
#include <string>
using namespace std;
 
void permute1(string prefix, string str)
{
    if(str.length() == 0)
        cout << prefix << endl;
    else
    {
        for(int i = 0; i < str.length(); i++)
            permute1(prefix+str[i], str.substr(0,i)+str.substr(i+1,str.length()));
    }
}
 
void permute1(string s)
{
    permute1("",s);
}
 
int main(void)
{
    //method1, unable to remove duplicate permutations.
    permute1("abc");
    return 0;
}

優點:該方法易於理解,但無法移除重複的排列,如:s="ABA",會生成兩個“AAB”。

方法2:利用交換的思想,具體見例項,但該方法不如方法1容易理解。

我們以三個字元abc為例來分析一下求字串排列的過程。首先我們固定第一個字元a,求後面兩個字元bc的排列。當兩個字元bc的排列求好之後,我們把第一個字元a和後面的b交換,得到bac,接著我們固定第一個字元b,求後面兩個字元ac的排列。現在是把c放到第一位置的時候了。記住前面我們已經把原先的第一個字元a和後面的b做了交換,為了保證這次c仍然是和原先處在第一位置的a交換,我們在拿c和第一個字元交換之前,先要把b和a交換回來。在交換b和a之後,再拿c和處在第一位置的a進行交換,得到cba。我們再次固定第一個字元c,求後面兩個字元b、a的排列。

既然我們已經知道怎麼求三個字元的排列,那麼固定第一個字元之後求後面兩個字元的排列,就是典型的遞迴思路了。

基於前面的分析,我們可以得到如下的參考程式碼:

void Permutation(char* pStr, char* pBegin)
{
    assert(pStr && pBegin);
    //if(!pStr || !pBegin)
        //return ;
    if(*pBegin == '\0')
        printf("%s\n",pStr);
    else
    {
        char temp;
        for(char* pCh = pBegin; *pCh != '\0'; pCh++)
        {
            if(pCh != pBegin && *pCh == *pBegin)   //為避免生成重複排列,當不同位置的字元相同時不再交換
                continue;
            temp = *pCh;
            *pCh = *pBegin;
            *pBegin = temp;

            Permutation(pStr, pBegin+1);

            temp = *pCh;
            *pCh = *pBegin;
            *pBegin = temp;
        }
    }
}

int main(void)
{
    char str[] = "aba";
    Permutation(str,str);
    return 0;
}

如p([1,2,3])輸出:
[1]、[2]、[3]、[1,2]、[2,3]、[1,3]、[1,2,3]
這兩問可以用虛擬碼。

 

http://blog.csdn.net/ohmygirl/article/details/7859497。

相關文章