【讀薄《編程珠璣》】貳 啊哈!算法

這篇文章是[《讀薄<編程珠璣>》系列博客][1]的第 貳 篇,在這篇文章中,作者提出了三個問題,並且給出了相應的解決方案,本文闡述了這三個問題以及解決方案,並且對課後習題進行了分析。

問題集合

  • 0x00: 給定一個最多包含40億個隨機排列的32位整數的順序文件,找出一個不在文件中的32位整數(在文件中至少缺失一個這樣的數)。在具有足夠內存的情況下,如何解決該問題?如果有幾個外部的『臨時』文件可用,但是僅有幾百字節的內存,又該如何解決該問題?

  • 0x01: 將一個 n 元一維向量向左旋轉 i 個位置。例如,當 n=8, i=3 時,向量 abcdefg 旋轉爲 defgabc。簡單的代碼使用一個 n 元的中間向量在 n 步內完成該工作。你能否僅適用數十個額外字節的存儲空間,在正比於 n 的時間內完成向量的旋轉?

  • 0x02: 給定一個英語字典,找出其中的所有變位詞的集合。例如,『pots』、『stop』、『tops』互爲變位詞,因爲每個單詞都能通過改變其他單詞中的字母順序來獲得。

  • 0x03:考慮查找給定輸入單詞的所有變位詞問題。僅給定單詞和詞典的情況下,如何解決該問題?如果有一些時間和空間可以在響應任何查詢之前預先處理字典,又會如何?

  • 0x04:給定包含 4,300,000,000 個 32 位證書的順序文件,如何找出一個至少出現兩次的整數?

  • 0x05:前面涉及了兩個需要經敲代碼來實現的向量旋轉算法。將其分別作爲獨立的程序實現。在每個程序中,i 和 n 的最大公約數如何出現?

  • 0x06:幾位讀者指出,既然所有的三個旋轉算法需要執行的運行時間都正比於 n,雜技算法的運行速度顯然是求逆算法的兩倍。雜技算法對數組中的每個元素僅存儲和讀取一次,而求逆算法需要兩次。在實際的計算機上實驗以比較兩者的速度差異,特別注意內存引用位置附近的問題。

  • 0x07:向量旋轉算法將向量 ab 變爲 ba。如何將向量 abc 變爲 cba?(這對交換非相鄰內存塊的問題進行了建模)

  • 0x08:20世紀70年代末期,貝爾實驗室開發出了『用戶操作的電話號碼簿輔助程序』,該程序允許僱員使用標準的按鍵電話在公司電話號碼簿中查找號碼。

    要查找該系統設計者的名字 Mike Lesk,可以按『LESK*M*』(也就是『5375*6*』),隨後,系統會輸出他的電話號碼。這樣的服務現在隨處可見。該系統中出現的一個問題是,不同的名字有可能具有相同的按鍵編碼。在 Lesk 的系統中發生這種情況時,系統會詢問用戶更多的信息。給定一個大的名字文件時(例如標準的大城市電話號碼簿),如何定位這些『錯誤匹配』呢?(當 LESK 在這種規模的電話號碼簿上做實驗時,他發現錯誤匹配的概率僅僅是0.2%)如何實現一個以名字的按鍵編碼爲參數,並返回所有可能的匹配名字的函數?

  • 0x09:在20世紀60年代早期,Vic Vyssotsky 與一個程序員一起工作,該程序員需要轉置一個存儲在磁帶上的4000*4000的矩陣(每條記錄的格式相同,爲數十個字節)。他的同事最初提出的程序需要運行50個小時。Vyssotsky 如何將運行時間減少到半小時呢?

  • 0x10:給定一個 n 元實數集合、一個實數 t 和一個整數 k,如何快速確定是否存在一個 k 元子集,其元素之和不超過 k ?

  • 0x11:順序搜索和二分搜索代表了搜索時間和預處理時間之間的折中。處理一個 n 元表格時,需要執行多少次二分搜索才能彌補對錶進行排序所消耗的預處理時間?

方案集合

0x00

該問題的解思想在於:二分法

首先,將40億個數字遍歷一遍,分爲 2 組,第一位爲 1 的一組,爲 0 的一組(假設只缺少一個數字),則數量少的一組必定有缺少的數,然後對該組再次進行分組,如此進行下去直到找到該缺少的數字。

如果有足夠的空間使用第一章中介紹的位向量也是可以的。

0x01

解法1: 思想在於向量中的元素移動後的最終位置其實是確定的,我們需要的是一個臨時變量,存放着該元素移動後的位置,然後把對應的元素放到相應位置即可

解法2: 思想在於旋轉向量的問題其實就是將該向量分爲了兩部分 ab,然後變成 ba 的過程。其中 a 爲向量的前 i 個元素,將 ab 變爲 ba 過程是這樣的:首先對 a 求逆得到 a’,然後對 b 求逆得到 b’,最後對 (a’b’) 求逆,所得到的結果就是 ba,過程如下:

reverse(0, i-1); /* cbadefgh */
reverse(i, n-1); /* cbahgfed */
reverse(0, n-1); /* defghabc */

0x02

對於該問題,我們可以標識字典中的每一個詞,使用基於排序的標識,按照字母表的順序來對出現的字母進行排序,例如,”deposit” 的標識爲 “deiopst”。改進後的標識,我們可以使用類似 “e1h1l1o2” 來標識 “hello”。

0x03

如果僅給單詞和詞典,我們需要對字典中的單詞進行依次算標識,然後進行比較;

如果有一些時間和空間可以預先處理字典,我們可以先計算出每個單詞的標識然後建立(標識, 單詞)對然後方便查找(或者直接排序)

0x04

思路同 0x00,先掃描一遍,把第一位是 「0」和「1」的數字放到兩個文件中,數量多的一個對第二位數字再分組,以此類推,最後一定能找出一個數字。

0x05

第一個算法的實現(雜耍算法)

/* 求最大公約數 */
int gcd(int a, int b){
    return b==0?a:gcd(b, a%b);
}

/* 第一個算法 */
int a1(int *start, int *end, int i){
    int len = end - start;
    int g = gcd(len, i);
    for(int index = 0; index < g; index++){
        int t = *(start+index);
        int next = index;
        while((next+i)%len != index){
            *(start+next) = *(start+(next+i)%len);
            next = (next+i)%len;
        }
        *(start+next) = t;
    }
    return 0;
}

第二個算法的實現(交換算法)

/* 第二個算法 */
void rangeswap( int *a, int i, int j, int m){
    //交換 a[i...i+m] 和 a[j...j+m]
    for(int index = 0; index < m; index++){
        int temp = a[i + index];
        a[i + index] = a[j + index];
        a[j + index] = temp;
    }
}

void a2( int *a, int shift ){
    if(shift == 0 || shift == num)
        return;
    int n = shift;
    int j = num - shift;
    int i = shift;
    while(i != j ){
        if(i > j){
            rangeswap(a, n-i, n, j);
            i -= j;
        }
        else{
            rangeswap(a, n-i, n+j-i, i);
            j -= i;
        }
    }
    rangeswap(a, n-i, n, i);
}

第三個算法的實現(求逆算法)

/* 第三個算法 */
void reverse(int m, int n){
    int mid = (m+n)/2;
    int temp;
    for(int i = m, j=n; i <= mid; i++, j--){
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

void a3(int* a, int i){
    reverse(0, i-1);
    reverse(i, num-1);
    reverse(0, num-1);
}

0x06

把上述三種算法分別運行,向量長度爲1,000,000,旋轉長度爲 1 - 50,作圖如下:(橫座標爲旋轉長度,縱座標爲運行時間,單位爲毫秒)

data

測試代碼如下:

int main(int argc, const char * argv[]) {

    ofstream oFile;

    oFile.open("/Users/jason/data.csv", ios::out | ios::trunc);
    for(int i = 1; i <= 50; i++){
        oFile << i << ',';
    }
    oFile << endl;
    /*算法一運行時間統計(向量長度1000000,旋轉距離1-50)*/
    for(int i = 1; i <=50; i++){
        init();
        clock_t start, finish;
        double totaltime;
        start = clock();
        a1(a, a+num, i);
        finish = clock();
        totaltime = (double)(finish-start)/CLOCKS_PER_SEC;
        oFile << totaltime*1000 << ',';
    }
    oFile << endl;


    /*算法二運行時間統計(向量長度1000000,旋轉距離1-50)*/
    for(int i = 1; i <= 50; i++){
        init();
        clock_t start, finish;
        double totaltime;
        start = clock();
        a2(a, i);
        finish = clock();
        totaltime = (double)(finish-start)/CLOCKS_PER_SEC;
        oFile << totaltime*1000 << ',';
    }
    oFile << endl;

    /*算法三運行時間統計(向量長度1000000,旋轉距離1-50)*/
    for(int i = 1; i <= 50; i++){
        init();
        clock_t start, finish;
        double totaltime;
        start = clock();
        a3(a, i);
        finish = clock();
        totaltime = (double)(finish-start)/CLOCKS_PER_SEC;
        oFile << totaltime*1000 << ',';
    }
    oFile << endl;


    oFile.close();

    return 0;
}

0x07

分別對 a, b, c 求逆,得到 a’, b’, c’,再對 (a’b’c’) 求逆,得到 cba

0x08

首先計算出所有名字的標識,例(『LESK*M*』的標識是『5375*6*』),然後對所有標識進行排序,查找時使用二分搜索2(答案中說實際中往往使用散列表或數據庫系統)

0x09

(作者給的答案)

爲了轉置矩陣,Vyssotsky 爲每條記錄插入列號和行號,然後調用系統的磁帶排序程序先按列排序,再按行排序,最後使用另一個程序刪除列號和行號

0x10

我能想到的方法是:對這個集合進行排序,然後算出前 k 位的和與 t 進行比較

0x11

TODO


本文的版權歸作者 羅遠航 所有,採用 Attribution-NonCommercial 3.0 License。任何人可以進行轉載、分享,但不可在未經允許的情況下用於商業用途;轉載請註明出處。感謝配合!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章