百度2014研發類校園招聘筆試題解答


先總體說下題型,共有3道簡答題,3道算法編程題和1道系統設計題,題目有難有易,限時兩小時完成。
 
一、簡答題

1. 動態鏈接庫和靜態鏈接庫的優缺點

2. 輪詢任務調度和可搶占式調度有什么區別?

3. 列出數據庫中常用的鎖及其應用場景

 

二、算法設計題

1. 給定N是一個正整數,求比N大的最小“不重復數”,這里的不重復是指沒有兩個相等的相鄰位,如1102中的11是相等的兩個相鄰位故不是不重復數,而12301是不重復數。

2. 設N是一個大整數,求長度為N的字符串的最長回文子串。

3. 坐標軸上從左到右依次的點為a[0]、a[1]、a[2]……a[n-1],設一根木棒的長度為L,求L最多能覆蓋坐標軸的幾個點?

 

三、系統設計題

1. 在現代系統的設計過程中,為了減輕請求的壓力,通常采用緩存技術,為了進一步提升緩存的命中率,同常采用分布是緩存方案。調度模塊針對不同內容的用戶請求分配給不同的緩存服務器向用戶提供服務。請給出一個分布式緩存方案,滿足如下要求:

1) 單台緩存服務器故障,整個分布式緩存集群,可以繼續提供服務。

2)通過一定得分配策略,可以保證充分利用每個緩存服務的存儲空間,及負載均衡。當部分服務器故障或系統擴容時,改分配策略可以保證較小的緩存文件重分配開銷。

3)當不同緩存服務器的存儲空間存在差異時,分配策略可以滿足比例分配。


 
下面給出我自己的一些解答,不保證100%正確,歡迎批評指正。
 

一、簡答題

1. 動態鏈接庫和靜態鏈接庫的優缺點
 
解答:(1) 動態鏈接庫(Dynamic Linked Library):Windows為應用程序提供了豐富的函數調用,這些函數調用都包含在動態鏈接庫中。其中有3個最重要的DLL,Kernel32.dll、 User32.dll和GDI32.dll。有兩種使用方式:一種是靜態加載,即在應用程序啟動時被加載;一種是動態加載,即是該動態鏈接庫在被使用時才被應用程序加載。優點如下:

a. 共享:多個應用程序可以使用同一個動態庫,啟動多個應用程序的時候,只需要將動態庫加載到內存一次即可;
b. 開發模塊好:要求設計者對功能划分的比較好。

缺點是不能解決引用計數等問題。

(2)靜態庫(Static Library):函數和數據被編譯進一個二進制文件(通常擴展名為.LIB)。在使用靜態庫的情況下,在編譯鏈接可執行文件時,鏈接器從庫中復制這些函數和數據並把它們和應用程序的其它模塊組合起來創建最終的可執行文件(.EXE文件)。靜態鏈接庫作為代碼的一部分,在編譯時被鏈接。優缺點如下:
代碼的裝載速度快,因為編譯時它只會把你需要的那部分鏈接進去,應用程序相對比較大。但是如果多個應用程序使用的話,會被裝載多次,浪費內存。

 
2. 輪詢任務調度和可搶占式調度有什么區別?
 
解答:(1)輪詢調度的原理是每一次把來自用戶的請求輪流分配給內部中的服務器,從1開始,直到N(內部服務器個數),然后重新開始循環。 只有在當前任務主動放棄CPU控制權的情況下(比如任務掛起),才允許其他任務(包括高優先級的任務)控制CPU。其優點是其簡潔性,它無需記錄當前所有連接的狀態,所以它是一種無狀態調度。但不利於后面的請求及時得到響應。
(2)搶占式調度允許高優先級的任務打斷當前執行的任務,搶占CPU的控制權。這有利於后面的高優先級的任務也能及時得到響應。但實現相對較復雜且可能出現低優先級的任務長期得不到調度。
 
3. 列出數據庫中常用的鎖及其應用場景
 
解答:數據庫中的鎖是網絡數據庫中的一個非常重要的概念,它主要用於多用戶環境下保證數據庫完整性和一致性。各種大型數據庫所采用的鎖的基本理論是一致的,但在具體實現上各有差別。目前,大多數數據庫管理系統都或多或少具有自我調節、自我管理的功能,因此很多用戶實際上不 清楚鎖的理論和所用數據庫中鎖的具體實現。在數據庫中加鎖時,除了可以對不同的資源加鎖,還可以使用不同程度的加鎖方式,即鎖有多種模式,SQL Server中鎖模式包括:  

1)共享鎖
SQL Server中,共享鎖用於所有的只讀數據操作。共享鎖是非獨占的,允許多個並發事務讀取其鎖定的資源。默認情況下,數據被讀取后,SQL Server立即釋放共享鎖。例如,執行查詢“SELECT * FROM my_table”時,首先鎖定第一頁,讀取之后,釋放對第一頁的鎖定,然后鎖定第二頁。這樣,就允許在讀操作過程中,修改未被鎖定的第一頁。但是,事務 隔離級別連接選項設置和SELECT語句中的鎖定設置都可以改變SQL Server的這種默認設置。例如,“ SELECT * FROM my_table HOLDLOCK”就要求在整個查詢過程中,保持對表的鎖定,直到查詢完成才釋放鎖定。  

2)修改鎖
修 改鎖在修改操作的初始化階段用來鎖定可能要被修改的資源,這樣可以避免使用共享鎖造成的死鎖現象。因為使用共享鎖時,修改數據的操作分為兩步,首先獲得一 個共享鎖,讀取數據,然后將共享鎖升級為獨占鎖,然后再執行修改操作。這樣如果同時有兩個或多個事務同時對一個事務申請了共享鎖,在修改數據的時候,這些 事務都要將共享鎖升級為獨占鎖。這時,這些事務都不會釋放共享鎖而是一直等待對方釋放,這樣就造成了死鎖。如果一個數據在修改前直接申請修改鎖,在數據修 改的時候再升級為獨占鎖,就可以避免死鎖。修改鎖與共享鎖是兼容的,也就是說一個資源用共享鎖鎖定后,允許再用修改鎖鎖定。  

3)獨占鎖
獨占鎖是為修改數據而保留的。它所鎖定的資源,其他事務不能讀取也不能修改。獨占鎖不能和其他鎖兼容。  

4)結構鎖
結構鎖分為結構修改鎖(Sch-M)和結構穩定鎖(Sch-S)。執行表定義語言操作時,SQL Server采用Sch-M鎖,編譯查詢時,SQL Server采用Sch-S鎖。  

5)意向鎖
意 向鎖說明SQL Server有在資源的低層獲得共享鎖或獨占鎖的意向。例如,表級的共享意向鎖說明事務意圖將獨占鎖釋放到表中的頁或者行。意向鎖又可以分為共享意向鎖、 獨占意向鎖和共享式獨占意向鎖。共享意向鎖說明事務意圖在共享意向鎖所鎖定的低層資源上放置共享鎖來讀取數據。獨占意向鎖說明事務意圖在共享意向鎖所鎖定 的低層資源上放置獨占鎖來修改數據。共享式獨占鎖說明事務允許其他事務使用共享鎖來讀取頂層資源,並意圖在該資源低層上放置獨占鎖。  

6)批量修改鎖
批量復制數據時使用批量修改鎖。可以通過表的TabLock提示或者使用系統存儲過程sp_tableoption的“table lock on bulk load”選項設定批量修改鎖。  

 

二、算法設計題

1. 給定N是一個正整數,求比N大的最小“不重復數”,這里的不重復是指沒有兩個相等的相鄰位,如1102中的11是相等的兩個相鄰位故不是不重復數,而12301是不重復數。
 
算法思想:當然最直接的方法是采用暴力法,從N+1開始逐步加1判斷是否是不重復數,是就退出循環輸出,這種方法一般是不可取的,例如N=11000000,你要一個個的加1要加到12010101,一共循環百萬次,每次都要重復判斷是否是不重復數,效率極其低下,因此是不可取的。這里我采用的方法是:從N+1的最高位往右開始判斷與其次高位是否相等,如果發現相等的(即為重復數)則將次高位加1,注意這里可能進位,如8921—>9021,后面的直接置為010101...形式,如1121—>1201,此時便完成“不重復數”的初步構造,但此時的“不重復數”不一定是真正的不重復的數,因為可能進位后的次高位變為0或進位后變成00,如9921—>10001,此時需要再次循環判斷重新構造直至滿足條件即可,這種方法循環的次數比較少,可以接受。
 
Source Code:
// 求比指定數大且最小的“不重復數”

#include <stdio.h>

void minNotRep(int n)
{
    // 需要多次判斷
    while(1)
    {
        int a[20], len = 0, i, b = 0;
        // flag為true表示是“重復數”,為false表示表示是“不重復數”
        bool flag = false;

        // 將n的各位上數字存到數組a中
        while(n)
        {
            a[len++] = n % 10;
            n = n / 10;
        }

        // 從高位開始遍歷是否有重復位
        for(i = len - 1; i > 0; i--)
        {
            // 有重復位則次高位加1(最高位有可能進位但這里不需要額外處理)
            if(a[i] == a[i - 1] && !flag)
            {
                a[i - 1]++;
                flag = true;
            }
            else if(flag)
            {
                // 將重復位后面的位置為0101...形式
                a[i - 1] = b;
                b = (b == 0) ? 1 : 0;
            }
        }

        // 重組各位數字為n,如果是“不重復數”則輸出退出否則繼續判斷
        for(i = len - 1; i >= 0; i--)
        {
            n = n * 10 + a[i];
        }

        if(!flag)
        {
            printf("%d\n", n);
            break;
        }
    }
}

int main()
{
    int N;

    while(scanf("%d", &N))
    {
        minNotRep(N + 1);
    }

    return 0;
}

 
2. 設N是一個大整數,求長度為N的字符串的最長回文子串。
 
算法1:第一個方法當然是暴力法,外面的兩層循環找到所有子串,第三層循環判斷子串是否是回文。方法的時間復雜度為O(n^3),空間復雜度為O(1)。
 
算法2:采用動態規划法判斷子串是否是回文。開辟一個P[i][j]用來表示str[i..j]是否為回文,P[i][j]的狀態轉移方程如下:
  1. 當i==j時,P[i][j]=true

  2. 當i+1==j時,P[i][j]=str[i]==str[j]

  3. 其他,P[i][j]=P[i+1][j-1]&&(str[i]==str[j])

那么P[i][j]中j-i+1最大的且值為true的就是最長回文子串。這樣,這個方法的時間復雜度為O(n^2),空間復雜度為O(n^2)。比暴力法有很大的改進。

Source Code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int longestPalSubstr(char *str)
{
    int n = strlen(str);
    int i, j, len, maxlen = 0, maxi = 0, maxj = 0;
    bool **P = (bool**)malloc(sizeof(bool) * n);

    for(i = 0; i < n; i++)
    {
        P[i] = (bool*)malloc(sizeof(bool) * n);
    }

    // initialize P[i][i]
    for(i = 0; i < n; i++)
    {
        P[i][i] = true;
    }

    // compute P[n][n] by length
    for(len = 2; len <= n; len++)
    {
        for(i = 0; i < n - len + 1; i++)
        {
            j = i + len - 1;
            if(len == 2)
            {
                P[i][j] = (str[i] == str[j]);
            }
            else
            {
                P[i][j] = ((str[i] == str[j]) && P[i + 1][j - 1]);
            }
        }
    }

//    int k;
    for(i = 0; i < n; i++) 
    {
        for(j = i; j < n; j++)
        {
//            printf("%d ", P[i][j]);
            if(P[i][j] && maxlen < (j - i + 1))
            {
                maxlen = j - i + 1;
                maxi = i;
                maxj = j;
            }
        }
//        printf("\n");
//        for(k = 0; k <= i; k++)
//            printf("  ");
    }

    printf("The longest palin substr is ");
    for(i = maxi; i <= maxj; i++)
    {
        printf("%c", str[i]);
    }
    printf(", maxlen is %d\n\n", maxlen);

    return maxlen;
}

int main()
{
    char str[100];
    
    while(1)
    {
        gets(str);
        if(strlen(str) == 0) break;
        longestPalSubstr(str);
    }

    return 0;
}

 

 

算法3:第三個方法,可以從上面那個方法的狀態轉移方程獲得啟發,對於每一個回文子串可以先確定一個中心,然后向兩邊擴展,這樣可以在時間復雜度O(n^2),空間復雜度O(1)的情況下完成,需要注意的是,長度為奇數和偶數的中心的情況是不同的。

Source Code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int longestPalSubstr(char *str)
{
    int len = strlen(str);
    int i, maxLen = 1, start = 0;
    int low, high;
    
    // 將每個字符作為中心向兩邊擴展判斷
    for(i = 1; i < len; i++)
    {
        // 處理長度為偶數的情況
        low = i - 1;
        high = i;
        while(low >= 0 && high < len && str[low] == str[high])
        {
            if(maxLen < high - low + 1)
            {
                start = low;
                maxLen = high - low + 1;
            }
            low--;
            high++;
        }

        // 處理長度為奇數的情況
        low = i - 1; 
        high = i + 1;
        while(low >= 0 && high < len && str[low] == str[high])
        {
            if(maxLen < high - low + 1)
            {
                start = low;
                maxLen = high - low + 1;
            }
            low--;
            high++;
        }
    }

    printf("The longest palin substr is ");
    for(i = start; i < start + maxLen; i++)
    {
        printf("%c", str[i]);
    }
    printf(", maxlen is %d\n\n", maxLen);

    return maxLen;
}

int main()
{
    char str[100];
    
    while(1)
    {
        gets(str);
        if(strlen(str) == 0) break;
        longestPalSubstr(str);
    }

    return 0;
}

 

算法4:第四個方法采用后綴數組,將最長回文子串的問題轉化為最長公共前綴的問題。具體的做法就是:將整個字符串翻轉之后,拼接到原字符串后,注意用特殊字 符分開,這樣問題就變成了新的字符串的某兩個后綴的最長公共前綴的問題了。這個方法比較強大,很多字符串的問題都能夠巧妙的解決。不過實現起來也相對比較:難,好的實現和差的實現時間復雜度相差很大。由於對后綴數組不是很清楚,未寫代碼,等學習了后綴數組再過來補。

算法5:第五個方法叫做Manacher算法,是一種線性時間的方法,非常巧妙。首先,我們在上面的方法中個,都要考慮回文長度為奇數或者偶數的情況。這個:方法,引入一個技巧,使得奇數和偶數的情況統一處理了。具體做法如下:

abba轉換為#a#b#b#a#,也就是在每一個字符兩邊都加上一個特殊字符。

然后創建一個新的P[i]表示,以第i個字符為中心的回文字串的半徑。例如上面的例子,對應的P如下,設S為原始字符串:

S # a # b # b # a #
P 1 2 1 2 5 2 1 2 1

通過觀察上面的表,大家可以發現P[i]-1就是實際回文字串的長度。如果知道P,遍歷一次就知道最長的回文子串。可以該如何計算P呢?這是這個算法最核心的部分。

下面的討論基本轉自博客:http://www.felix021.com/blog/read.php?2040 該博客中對Manacher算法介紹得也非常好,向大家推薦。

算法引入兩個變量id和mx,id表示最長回文子串的中心位置,mx表示最長回文字串的邊界位置,即:mx=id+P[id]。

在這里有一個非常有用而且神奇的結論:如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i) 分開理解就是:

  1. 如果mx - i > P[j], 則P[i]=P[j]

  2. 否則,P[i]  = mx - i.

這兩個該如何理解呢?具體的解釋請看下面的兩個圖。

(1)當 mx - i > P[j] 的時候,以S[j]為中心的回文子串包含在以S[id]為中心的回文子串中,由於 i 和 j 對稱,以S[i]為中心的回文子串必然包含在以S[id]為中心的回文子串中,所以必有 P[i] = P[j],見下圖。

(2)當 P[j] >= mx - i 的時候,以S[j]為中心的回文子串不一定完全包含於以S[id]為中心的回文子串中,但是基於對稱性可知,下圖中兩個綠框所包圍的部分是相同的,也就是 說以S[i]為中心的回文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx - i。至於mx之后的部分是否對稱,就只能老老實實去匹配了。

對於 mx <= i 的情況,無法對 P[i]做更多的假設,只能P[i] = 1,然后再去匹配了。

理解了上面的一點,就沒有問題了。

Source Code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int longestPalSubstr(char *str)
{
    char s[100];
    int i, maxLen = 1, start = 0, j;
    int len = strlen(str);
    int mx = 0, id = 0, min;

    s[0] = '$';
    s[1] = '#';
    for(i = 0, j = 2; i < len; i++, j += 2)
    {
        s[j] = str[i];
        s[j + 1] = '#';
    }
    s[j] = '\0';

    len = len * 2 + 1;
    int *p = (int *)malloc(sizeof(int) * len);
    memset(p, 0, len);
    p[0] = 1;

    for(i = 1; i < len; i++)
    {
        min = p[2 * id - i] > (mx - i) ? (mx - i) : p[2 * id - i];
        p[i] = mx > i ? min : 1;
        while(s[i + p[i]] == s[i - p[i]])
        {
            p[i]++;
        }
        if(i + p[i] > mx)
        {
            mx = i + p[i];
            id = i;
        }
    }

    for(i = 0; i < len; i++)
    {
        //printf("%d ", p[i]);
        if(maxLen < p[i] - 1)
        {
            maxLen = p[i] - 1;
            start = i - maxLen;
        }
    }

    printf("The longest palin substr is ");
    for(i = start; i < start + 2 * maxLen + 1; i++)
    {
        if(s[i] != '#')
        {
            printf("%c", s[i]);
        }
    }
    printf(", maxlen is %d\n\n", maxLen);

    return maxLen;
}

int main()
{
    char str[100];
    
    while(1)
    {
        gets(str);
        if(strlen(str) == 0) break;
        longestPalSubstr(str);
    }

    return 0;
}
 
3. 坐標軸上從左到右依次的點為a[0]、a[1]、a[2]……a[n-1],設一根木棒的長度為L,求L最多能覆蓋坐標軸的幾個點?
 
算法思想:開始時我把題目理解錯了,以為是求a中最大子序列和使其等於L,實際上是求滿足a[j]-a[i] <= L && a[j+1]-a[i] > L這兩個條件的j與i中間的所有點個數中的最大值,即j-i+1最大,這樣題目就簡單多了,方法也很簡單:直接從左到右掃描,兩個指針i和j,i從位置0開始,j從位置1開始,如果a[j] - a[i] <= L則j++並記錄中間經過的點個數,如果a[j] - a[i] > L則j--回退,覆蓋點個數-1回到剛好滿足條件的時候,將滿足條件的最大值與所求最大值比較,然后i++,j++直到求出最大的點個數。

有兩點需要注意:

(1)這里可能沒有i和j使得a[j] - a[i]剛好等於L的,所以判斷條件不能為a[j] - a[i] = L。
(2)可能存在不同的覆蓋點但覆蓋的長度相同,此時只選第一次覆蓋的點。
 
Source code:
// 求最大覆蓋點

#include <stdio.h>

int maxCover(int a[], int n, int L)
{
    int count = 2, maxCount = 1, start;
    int i = 0, j = 1;

    while(i < n && j < n)
    {

        while((j < n) && (a[j] - a[i] <= L))
        {
            j++;
            count++;
        }

        // 退回到滿足條件的j
        j--;
        count--;
        
        if(maxCount < count)
        {
            start = i;
            maxCount = count;
        }
        i++;
        j++;    
    }

    printf("covered point: ");
    for(i = start; i < start + maxCount; i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");

    return maxCount;
}

int main()
{
    // test
    int a[] = {1, 3, 7, 8, 10, 11, 12, 13, 15, 16, 17, 18, 21};

    printf("max count: %d\n\n", maxCover(a, 13, 8));

    int b[] = {1,2,3,4,5,100,1000};

    printf("max count: %d\n", maxCover(b, 7, 8));

    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM