C語言經典算法大全

老掉牙 
河內塔 
費式數列 
巴斯卡三角形 
三色棋 
老鼠走迷官(一) 
老鼠走迷官(二) 
騎士走棋盤 
八個皇后 
八枚銀幣 
生命遊戲 
字串覈對 
雙色、三色河內塔 
揹包問題(Knapsack Problem) 
數、運算 
蒙地卡羅法求 PI
Eratosthenes篩選求質數 
超長整數運算(大數運算) 
長 PI 
最大公因數、最小公倍數、因式分解 
完美數 
阿姆斯壯數 
最大訪客數 
中序式轉後序式(前序式) 
後序式的運算 
關於賭博 
洗撲克牌(亂數排列) 
Craps賭博遊戲 
約瑟夫問題(Josephus Problem) 
集合問題 
排列組合 
格雷碼(Gray Code) 
產生可能的集合 
m元素集合的n個元素子集 
數字拆解 
排序 
得分排行 
選擇、插入、氣泡排序 
Shell 排序法 - 改良的插入排序 
Shaker 排序法 - 改良的氣泡排序 
Heap 排序法 - 改良的選擇排序 
快速排序法(一) 
快速排序法(二) 
快速排序法(三) 
合併排序法 
基數排序法 
搜尋 
循序搜尋法(使用衛兵) 
二分搜尋法(搜尋原則的代表) 
插補搜尋法 
費氏搜尋法 
矩陣 
稀疏矩陣 
多維矩陣轉一維矩陣 
上三角、下三角、對稱矩陣 
奇數魔方陣 
4N 魔方陣 
2(2N+1) 魔方陣 


1.河內之塔
說明河內之塔(Towers of Hanoi)是法國人M.Claus(Lucas)於1883年從泰國帶至法國的,河內爲越戰時北越的首都,即現在的胡志明市;1883年法國數學家 Edouard Lucas曾提及這個故事,據說創世紀時Benares有一座波羅教塔,是由三支鑽石棒(Pag)所支撐,開始時神在第一根棒上放置64個由上至下依由小至大排列的金盤(Disc),並命令僧侶將所有的金盤從第一根石棒移至第三根石棒,且搬運過程中遵守大盤子在小盤子之下的原則,若每日僅搬一個盤子,則當盤子全數搬運完畢之時,此塔將毀損,而也就是世界末日來臨之時。
解法如果柱子標爲ABC,要由A搬至C,在只有一個盤子時,就將它直接搬至C,當有兩個盤子,就將B當作輔助柱。如果盤數超過2個,將第三個以下的盤子遮起來,就很簡單了,每次處理兩個盤子,也就是:A->B、A ->C、B->C這三個步驟,而被遮住的部份,其實就是進入程式的遞迴處理。事實上,若有n個盤子,則移動完畢所需之次數爲2^n - 1,所以當盤數爲64時,則所需次數爲:264- 1 = 18446744073709551615爲5.05390248594782e+16年,也就是約5000世紀,如果對這數字沒什幺概念,就假設每秒鐘搬一個盤子好了,也要約5850億年左右。 
#include <stdio.h>
void hanoi(int n, char A, char B, char C) {
    if(n == 1) {
        printf("Move sheet %d from %c to %c\n", n, A, C);
    }
    else {
        hanoi(n-1, A, C, B);
        printf("Move sheet %d from %c to %c\n", n, A, C);
        hanoi(n-1, B, A, C);
    }
}


int main() {
    int n;
    printf("請輸入盤數:");
    scanf("%d", &n);
    hanoi(n, 'A', 'B', 'C');
    return 0;





2.Algorithm Gossip: 費式數列
說明
Fibonacci爲1200年代的歐洲數學家,在他的着作中曾經提到:「若有一隻免子每個月生一隻小免子,一個月後小免子也開始生產。起初只有一隻免子,一個月後就有兩隻免子,二個月後有三隻免子,三個月後有五隻免子(小免子投入生產)......。 
如果不太理解這個例子的話,舉個圖就知道了,注意新生的小免子需一個月成長期纔會投入生產,類似的道理也可以用於植物的生長,這就是Fibonacci數列,一般習慣稱之爲費氏數列,例如以下: 1、1 、2、3、5、8、13、21、34、55、89...... 
解法
依說明,我們可以將費氏數列定義爲以下: 
fn = fn-1 + fn-2    if n > 1
fn = n       if n = 0, 1 
#include <stdio.h> 
#include <stdlib.h> 


#define N 20 


int main(void) { 
int Fib[N] = {0}; 
int i; 


Fib[0] = 0; 
Fib[1] = 1; 


for(i = 2; i < N; i++) 
Fib[i] = Fib[i-1] + Fib[i-2]; 


for(i = 0; i < N; i++) 
printf("%d ", Fib[i]); 
printf("\n"); 
return 0; 
}


3. 巴斯卡三角形

#include <stdio.h>
#define N 12
long combi(int n, int r){
    int i;
    long p = 1;
    for(i = 1; i <= r; i++)
        p = p * (n-i+1) / i;
    return p;
}
void paint() {
    int n, r, t;
    for(n = 0; n <= N; n++) {
        for(r = 0; r <= n; r++) {
            int i;/* 排版設定開始 */
            if(r == 0) {  
                for(i = 0; i <= (N-n); i++) 
                    printf("   ");
            }else {
                printf("   ");
            } /* 排版設定結束 */
            printf("%3d", combi(n, r));
        }
        printf("\n");
    }
}
4.Algorithm Gossip: 三色棋
說明
三色旗的問題最早由E.W.Dijkstra所提出,他所使用的用語爲Dutch Nation Flag(Dijkstra爲荷蘭人),而多數的作者則使用Three-Color Flag來稱之。


假設有一條繩子,上面有紅、白、藍三種顏色的旗子,起初繩子上的旗子顏色並沒有順序,您希望將之分類,並排列爲藍、白、紅的順序,要如何移動次數纔會最少,注意您只能在繩子上進行這個動作,而且一次只能調換兩個旗子。
解法
在一條繩子上移動,在程式中也就意味只能使用一個陣列,而不使用其它的陣列來作輔助,問題的解法很簡單,您可以自己想像一下在移動旗子,從繩子開頭進行,遇到藍色往前移,遇到白色留在中間,遇到紅色往後移,如下所示:
只是要讓移動次數最少的話,就要有些技巧:
如果圖中W所在的位置爲白色,則W+1,表示未處理的部份移至至白色羣組。 
如果W部份爲藍色,則B與W的元素對調,而B與W必須各+1,表示兩個羣組都多了一個元素。 
如果W所在的位置是紅色,則將W與R交換,但R要減1,表示未處理的部份減1。
 
注意B、W、R並不是三色旗的個數,它們只是一個移動的指標;什幺時候移動結束呢?一開始時未處理的R指標會是等於旗子的總數,當R的索引數減至少於W的索引數時,表示接下來的旗子就都是紅色了,此時就可以結束移動,如下所示:


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


#define BLUE 'b' 
#define WHITE 'w' 
#define RED 'r' 


#define SWAP(x, y) { char temp; \
                     temp = color[x]; \
                     color[x] = color[y]; \
                     color[y] = temp; }


int main() {
    char color[] = {'r', 'w', 'b', 'w', 'w', 
                    'b', 'r', 'b', 'w', 'r', '\0'}; 


    int wFlag = 0;
    int bFlag = 0;
    int rFlag = strlen(color) - 1;
    int i; 


    for(i = 0; i < strlen(color); i++) 
        printf("%c ", color[i]); 
    printf("\n"); 


    while(wFlag <= rFlag) {
        if(color[wFlag] == WHITE)
            wFlag++;
        else if(color[wFlag] == BLUE) {
            SWAP(bFlag, wFlag);
            bFlag++; wFlag++;
        } 
        else { 
            while(wFlag < rFlag && color[rFlag] == RED)
              rFlag--;
            SWAP(rFlag, wFlag);
            rFlag--;
        } 
    } 


    for(i = 0; i < strlen(color); i++) 
        printf("%c ", color[i]); 
    printf("\n"); 


    return 0; 

 






5.Algorithm Gossip: 老鼠走迷官(一)
說明老鼠走迷宮是遞迴求解的基本題型,我們在二維陣列中使用2表示迷宮牆壁,使用1來表示老鼠的行走路徑,試以程式求出由入口至出口的路徑。
解法老鼠的走法有上、左、下、右四個方向,在每前進一格之後就選一個方向前進,無法前進時退回選擇下一個可前進方向,如此在陣列中依序測試四個方向,直到走到出口爲止,這是遞迴的基本題,請直接看程式應就可以理解。
#include <stdio.h>
#include <stdlib.h> 


int visit(int, int); 


int maze[7][7] = {{2, 2, 2, 2, 2, 2, 2}, 
                  {2, 0, 0, 0, 0, 0, 2}, 
                  {2, 0, 2, 0, 2, 0, 2}, 
                  {2, 0, 0, 2, 0, 2, 2}, 
                  {2, 2, 0, 2, 0, 2, 2}, 
                  {2, 0, 0, 0, 0, 0, 2}, 
                  {2, 2, 2, 2, 2, 2, 2}}; 


int startI = 1, startJ = 1;  // 入口
int endI = 5, endJ = 5;  // 出口
int success = 0;


int main(void) { 
    int i, j; 


    printf("顯示迷宮:\n"); 
    for(i = 0; i < 7; i++) { 
        for(j = 0; j < 7; j++) 
            if(maze[i][j] == 2) 
                printf("█"); 
            else 
                printf("  "); 
        printf("\n"); 
    } 


    if(visit(startI, startJ) == 0)
        printf("\n沒有找到出口!\n"); 
    else { 
        printf("\n顯示路徑:\n"); 
        for(i = 0; i < 7; i++) { 
            for(j = 0; j < 7; j++) { 
                if(maze[i][j] == 2) 
                    printf("█"); 
                else if(maze[i][j] == 1) 
                    printf("◇"); 
                else 
                    printf("  "); 
            } 
            printf("\n"); 
        } 
    } 


    return 0; 



int visit(int i, int j) { 
    maze[i][j] = 1; 


    if(i == endI && j == endJ)
        success = 1; 


    if(success != 1 && maze[i][j+1] == 0) visit(i, j+1); 
    if(success != 1 && maze[i+1][j] == 0) visit(i+1, j); 
    if(success != 1 && maze[i][j-1] == 0) visit(i, j-1); 
    if(success != 1 && maze[i-1][j] == 0) visit(i-1, j); 


    if(success != 1) 
        maze[i][j] = 0; 
    
    return success; 
}  








6.Algorithm Gossip: 老鼠走迷官(二)
說明由於迷宮的設計,老鼠走迷宮的入口至出口路徑可能不只一條,如何求出所有的路徑呢?
解法求所有路徑看起來複雜但其實更簡單,只要在老鼠走至出口時顯示經過的路徑,然後退回上一格重新選擇下一個位置繼續遞迴就可以了,比求出單一路徑還簡單,我們的程式只要作一點修改就可以了。 
#include <stdio.h>
#include <stdlib.h> 


void visit(int, int);


int maze[9][9] = {{2, 2, 2, 2, 2, 2, 2, 2, 2},
                  {2, 0, 0, 0, 0, 0, 0, 0, 2},
                  {2, 0, 2, 2, 0, 2, 2, 0, 2},
                  {2, 0, 2, 0, 0, 2, 0, 0, 2},
                  {2, 0, 2, 0, 2, 0, 2, 0, 2},
                  {2, 0, 0, 0, 0, 0, 2, 0, 2},
                  {2, 2, 0, 2, 2, 0, 2, 2, 2},
                  {2, 0, 0, 0, 0, 0, 0, 0, 2},
                  {2, 2, 2, 2, 2, 2, 2, 2, 2}};


int startI = 1, startJ = 1;  // 入口
int endI = 7, endJ = 7;  // 出口


int main(void) { 
    int i, j; 


    printf("顯示迷宮:\n"); 
    for(i = 0; i < 7; i++) { 
        for(j = 0; j < 7; j++) 
            if(maze[i][j] == 2) 
                printf("█"); 
            else 
                printf("  "); 
        printf("\n"); 
    } 


    visit(startI, startJ);


    return 0; 



void visit(int i, int j) {
    int m, n;


    maze[i][j] = 1; 


    if(i == endI && j == endJ) {
        printf("\n顯示路徑:\n");
        for(m = 0; m < 9; m++) {
            for(n = 0; n < 9; n++)
                if(maze[m][n] == 2)
                    printf("█");
                else if(maze[m][n] == 1)
                    printf("◇");
                else
                    printf("  ");
            printf("\n");
        }
    }


    if(maze[i][j+1] == 0) visit(i, j+1);
    if(maze[i+1][j] == 0) visit(i+1, j);
    if(maze[i][j-1] == 0) visit(i, j-1);
    if(maze[i-1][j] == 0) visit(i-1, j);


    maze[i][j] = 0;
}  
















7.Algorithm Gossip: 騎士走棋盤
說明騎士旅遊(Knight tour)在十八世紀初倍受數學家與拼圖迷的注意,它什麼時候被提出已不可考,騎士的走法爲西洋棋的走法,騎士可以由任一個位置出發,它要如何走完[所有的位置?
解法騎士的走法,基本上可以使用遞回來解決,但是純綷的遞迴在維度大時相當沒有效率,一個聰明的解法由J.C. Warnsdorff在1823年提出,簡單的說,先將最難的位置走完,接下來的路就寬廣了,騎士所要走的下一步,「爲下一步再選擇時,所能走的步數最少的一步。」,使用這個方法,在不使用遞迴的情況下,可以有較高的機率找出走法(找不到走法的機會也是有的)。
#include <stdio.h> 


int board[8][8] = {0}; 


int main(void) {
    int startx, starty;
    int i, j;
    printf("輸入起始點:");
    scanf("%d %d", &startx, &starty);


    if(travel(startx, starty)) {
        printf("遊歷完成!\n");
    }
    else {
        printf("遊歷失敗!\n");
    }


    for(i = 0; i < 8; i++) {
        for(j = 0; j < 8; j++) {
            printf("%2d ", board[i][j]);
        }
        putchar('\n');
    }
    return 0;



int travel(int x, int y) {
    // 對應騎士可走的八個方向
    int ktmove1[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
    int ktmove2[8] = {1, 2, 2, 1, -1, -2, -2, -1};


    // 測試下一步的出路
    int nexti[8] = {0};
    int nextj[8] = {0};
    // 記錄出路的個數
    int exists[8] = {0};
    int i, j, k, m, l;
    int tmpi, tmpj;
    int count, min, tmp;


    i = x;
    j = y;
    board[i][j] = 1;


    for(m = 2; m <= 64; m++) {
        for(l = 0; l < 8; l++) 
            exists[l] = 0;


        l = 0;


        // 試探八個方向
        for(k = 0; k < 8; k++) {
            tmpi = i + ktmove1[k];
            tmpj = j + ktmove2[k];


            // 如果是邊界了,不可走
            if(tmpi < 0 || tmpj < 0 || tmpi > 7 || tmpj > 7)
                continue;


            // 如果這個方向可走,記錄下來
            if(board[tmpi][tmpj] == 0) {
                nexti[l] = tmpi;
                nextj[l] = tmpj;
                // 可走的方向加一個
                l++;
            }
        }


        count = l;
        // 如果可走的方向爲0個,返回
        if(count == 0) {
            return 0;
        }
        else if(count == 1) {
            // 只有一個可走的方向
            // 所以直接是最少出路的方向
            min = 0;
        }
        else {
            // 找出下一個位置的出路數
            for(l = 0; l < count; l++) {
                for(k = 0; k < 8; k++) {
                    tmpi = nexti[l] + ktmove1[k];
                    tmpj = nextj[l] + ktmove2[k];
                    if(tmpi < 0 || tmpj < 0 || 
                       tmpi > 7 || tmpj > 7) {
                        continue;
                    }
                    if(board[tmpi][tmpj] == 0)
                        exists[l]++;
                }
            }
            tmp = exists[0];
            min = 0;
            // 從可走的方向中尋找最少出路的方向
            for(l = 1; l < count; l++) {
                if(exists[l] < tmp) {
                    tmp = exists[l];
                    min = l;
                }
            }
        }


        // 走最少出路的方向
        i = nexti[min];
        j = nextj[min];
        board[i][j] = m;
    }


    return 1;

 
8.Algorithm Gossip: 八皇后
說明西洋棋中的皇后可以直線前進,吃掉遇到的所有棋子,如果棋盤上有八個皇后,則這八個皇后如何相安無事的放置在棋盤上,1970年與1971年, E.W.Dijkstra與N.Wirth曾經用這個問題來講解程式設計之技巧。
解法關於棋盤的問題,都可以用遞迴求解,然而如何減少遞迴的次數?在八個皇后的問題中,不必要所有的格子都檢查過,例如若某列檢查過,該該列的其它格子就不用再檢查了,這個方法稱爲分支修剪。


#include <stdio.h> 
#include <stdlib.h> 
#define N 8 


int column[N+1]; // 同欄是否有皇后,1表示有 
int rup[2*N+1]; // 右上至左下是否有皇后 
int lup[2*N+1]; // 左上至右下是否有皇后 
int queen[N+1] = {0}; 
int num; // 解答編號 


void backtrack(int); // 遞迴求解 


int main(void) { 
    int i; 
    num = 0; 


    for(i = 1; i <= N; i++) 
        column[i] = 1; 


    for(i = 1; i <= 2*N; i++) 
        rup[i] = lup[i] = 1; 


    backtrack(1); 


    return 0; 



void showAnswer() {
    int x, y;
    printf("\n解答 %d\n", ++num);
    for(y = 1; y <= N; y++) {
        for(x = 1; x <= N; x++) {
            if(queen[y] == x) {
                printf(" Q");
            }
            else {
                printf(" .");
            }
        }
        printf("\n");
    }
}


void backtrack(int i) { 
    int j;


    if(i > N) { 
        showAnswer();
    } 
    else { 
        for(j = 1; j <= N; j++) { 
            if(column[j] == 1 && 
                 rup[i+j] == 1 && lup[i-j+N] == 1) { 
                queen[i] = j; 
                // 設定爲佔用
                column[j] = rup[i+j] = lup[i-j+N] = 0; 
                backtrack(i+1); 
                column[j] = rup[i+j] = lup[i-j+N] = 1; 
            } 
        } 
    } 

 








9.Algorithm Gossip: 八枚銀幣
說明現有八枚銀幣a b c d e f g h,已知其中一枚是假幣,其重量不同於真幣,但不知是較輕或較重,如何使用天平以最少的比較次數,決定出哪枚是假幣,並得知假幣比真幣較輕或較重。
解法單就求假幣的問題是不難,但問題限制使用最少的比較次數,所以我們不能以單純的迴圈比較來求解,我們可以使用決策樹(decision tree),使用分析與樹狀圖來協助求解。一個簡單的狀況是這樣的,我們比較a+b+c與d+e+f ,如果相等,則假幣必是g或h,我們先比較g或h哪個較重,如果g較重,再與a比較(a是真幣),如果g等於a,則g爲真幣,則h爲假幣,由於h比g輕而 g是真幣,則h假幣的重量比真幣輕。


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
 
void compare(int[], int, int, int); 
void eightcoins(int[]); 
 
int main(void) { 
    int coins[8] = {0}; 
    int i; 


    srand(time(NULL)); 


    for(i = 0; i < 8; i++) 
        coins[i] = 10; 


    printf("\n輸入假幣重量(比10大或小):"); 
    scanf("%d", &i); 
    coins[rand() % 8] = i; 


    eightcoins(coins); 


    printf("\n\n列出所有錢幣重量:"); 
    for(i = 0; i < 8; i++) 
        printf("%d ", coins[i]); 


    printf("\n"); 


    return 0; 



void compare(int coins[], int i, int j, int k) { 
    if(coins[i] > coins[k]) 
        printf("\n假幣 %d 較重", i+1); 
    else 
        printf("\n假幣 %d 較輕", j+1); 



void eightcoins(int coins[]) { 
    if(coins[0]+coins[1]+coins[2] == 
       coins[3]+coins[4]+coins[5]) { 
        if(coins[6] > coins[7]) 
            compare(coins, 6, 7, 0); 
        else 
            compare(coins, 7, 6, 0); 
    } 
    else if(coins[0]+coins[1]+coins[2] > 
            coins[3]+coins[4]+coins[5]) { 
        if(coins[0]+coins[3] == coins[1]+coins[4]) 
            compare(coins, 2, 5, 0); 
        else if(coins[0]+coins[3] > coins[1]+coins[4]) 
            compare(coins, 0, 4, 1); 
        if(coins[0]+coins[3] < coins[1]+coins[4]) 
            compare(coins, 1, 3, 0); 
    } 
    else if(coins[0]+coins[1]+coins[2] <
            coins[3]+coins[4]+coins[5]) { 
        if(coins[0]+coins[3] == coins[1]+coins[4]) 
            compare(coins, 5, 2, 0); 
        else if(coins[0]+coins[3] > coins[1]+coins[4]) 
            compare(coins, 3, 1, 0); 
        if(coins[0]+coins[3] < coins[1]+coins[4]) 
            compare(coins, 4, 0, 1); 
    } 







10.Algorithm Gossip: 生命遊戲
說明生命遊戲(game of life)爲1970年由英國數學家J. H. Conway所提出,某一細胞的鄰居包括上、下、左、右、左上、左下、右上與右下相鄰之細胞,遊戲規則如下:
孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。 
擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。 
穩定:如果細胞的鄰居爲二個或三個,則下一次狀態爲穩定存活。 
復活:如果某位置原無細胞存活,而該位置的鄰居爲三個,則該位置將復活一細胞。
解法生命遊戲的規則可簡化爲以下,並使用CASE比對即可使用程式實作:
鄰居個數爲0、1、4、5、6、7、8時,則該細胞下次狀態爲死亡。 
鄰居個數爲2時,則該細胞下次狀態爲復活。 
鄰居個數爲3時,則該細胞下次狀態爲穩定。 
#include <stdio.h> 
#include <stdlib.h> 
#include <ctype.h> 


#define MAXROW 10 
#define MAXCOL 25 
#define DEAD 0 
#define ALIVE 1 
int map[MAXROW][MAXCOL], newmap[MAXROW][MAXCOL]; 


void init(); 
int neighbors(int, int);
void outputMap();
void copyMap();


int main() { 
   int row, col; 
   char ans; 
   init();
   while(1) {
      outputMap();
      for(row = 0; row < MAXROW; row++) {
         for(col = 0; col < MAXCOL; col++) {
            switch (neighbors(row, col)) {
               case 0: 
               case 1: 
               case 4: 
               case 5: 
               case 6: 
               case 7: 
               case 8: 
                  newmap[row][col] = DEAD; 
                  break; 
               case 2: 
                  newmap[row][col] = map[row][col]; 
                  break; 
               case 3: 
                  newmap[row][col] = ALIVE; 
                  break; 
            } 
         }
      }


      copyMap();
      printf("\nContinue next Generation ? ");
      getchar();
      ans = toupper(getchar());
      if(ans != 'Y') break;
   }
   return 0; 



void init() {
   int row, col; 
    
   for(row = 0; row < MAXROW; row++) 
      for(col = 0; col < MAXCOL; col++) 
         map[row][col] = DEAD; 


   puts("Game of life Program"); 
   puts("Enter x, y where x, y is living cell");
   printf("0 <= x <= %d, 0 <= y <= %d\n", 
                 MAXROW-1, MAXCOL-1); 
   puts("Terminate with x, y = -1, -1");


   while(1) {
      scanf("%d %d", &row, &col); 
      if(0 <= row && row < MAXROW && 
         0 <= col && col < MAXCOL)
         map[row][col] = ALIVE;
      else if(row == -1 || col == -1)
         break;
      else 
         printf("(x, y) exceeds map ranage!"); 
   }
}


int neighbors(int row, int col) {
   int count = 0, c, r; 
   for(r = row-1; r <= row+1; r++) 
      for(c = col-1; c <= col+1; c++) { 
         if(r < 0 || r >= MAXROW || c < 0 || c >= MAXCOL) 
            continue; 
         if(map[r][c] == ALIVE) 
            count++; 
      } 


   if(map[row][col] == ALIVE) 
      count--; 
   return count; 



void outputMap() {
   int row, col; 
   printf("\n\n%20cGame of life cell status\n"); 
   for(row = 0; row < MAXROW; row++) { 
      printf("\n%20c", ' '); 
      for(col = 0; col < MAXCOL; col++) 
         if(map[row][col] == ALIVE) putchar('#'); 
         else putchar('-'); 
   } 



void copyMap() {
   int row, col; 
   for(row = 0; row < MAXROW; row++) 
      for(col = 0; col < MAXCOL; col++) 
         map[row][col] = newmap[row][col]; 
}  
11.Algorithm Gossip: 字串覈對
說明今日的一些高階程式語言對於字串的處理支援越來越強大(例如Java、Perl等),不過字串搜尋本身仍是個值得探討的課題,在這邊以Boyer- Moore法來說明如何進行字串說明,這個方法快且原理簡潔易懂。
解法字串搜尋本身不難,使用暴力法也可以求解,但如何快速搜尋字串就不簡單了,傳統的字串搜尋是從關鍵字與字串的開頭開始比對,例如 Knuth-Morris-Pratt 演算法 字串搜尋,這個方法也不錯,不過要花時間在公式計算上;Boyer-Moore字串覈對改由關鍵字的後面開始覈對字串,並製作前進表,如果比對不符合則依前進表中的值前進至下一個覈對處,假設是p好了,然後比對字串中p-n+1至p的值是否與關鍵字相同。 
如果關鍵字中有重複出現的字元,則前進值就會有兩個以上的值,此時則取前進值較小的值,如此就不會跳過可能的位置,例如texture這個關鍵字,t的前進值應該取後面的3而不是取前面的7。
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 


void table(char*); // 建立前進表 
int search(int, char*, char*); // 搜尋關鍵字 
void substring(char*, char*, int, int); // 取出子字串 


int skip[256]; 


int main(void) { 
char str_input[80]; 
char str_key[80]; 
char tmp[80] = {'\0'}; 
int m, n, p; 
printf("請輸入字串:"); 
gets(str_input); 
printf("請輸入搜尋關鍵字:"); 
gets(str_key); 
m = strlen(str_input); // 計算字串長度 
n = strlen(str_key); 
table(str_key); 
p = search(n-1, str_input, str_key); 


while(p != -1) { 
substring(str_input, tmp, p, m); 
printf("%s\n", tmp); 
p = search(p+n+1, str_input, str_key); 



printf("\n"); 
return 0; 



void table(char *key) { 
int k, n; 
n = strlen(key); 
for(k = 0; k <= 255; k++) 
skip[k] = n; 
for(k = 0; k < n - 1; k++) 
skip[key[k]] = n - k - 1; 



int search(int p, char* input, char* key) { 
int i, m, n; 
char tmp[80] = {'\0'}; 
m = strlen(input); 
n = strlen(key); 


while(p < m) { 
substring(input, tmp, p-n+1, p); 
if(!strcmp(tmp, key)) // 比較兩字串是否相同 
return p-n+1; 
p += skip[input[p]]; 

return -1; 



void substring(char *text, char* tmp, int s, int e) { 
int i, j; 
for(i = s, j = 0; i <= e; i++, j++) 
mp[j] = text[i]; 
tmp[j] = '\0'; 
}
 
12.Algorithm Gossip: 雙色、三色河內塔
說明雙色河內塔與三色河內塔是由之前所介紹過的河內塔規則衍生而來,雙色河內塔的目的是將下圖左上的圓環位置經移動成爲右下的圓環位置: 


而三色河內塔則是將下圖左上的圓環經移動成爲右上的圓環:


解法無論是雙色河內塔或是三色河內塔,其解法觀念與之前介紹過的河內塔是類似的,同樣也是使用遞回來解,不過這次遞迴解法的目的不同,我們先來看只有兩個盤的情況,這很簡單,只要將第一柱的黃色移動至第二柱,而接下來第一柱的藍色移動至第三柱。 
再來是四個盤的情況,首先必須用遞迴完成下圖左上至右下的移動:


接下來最底層的就不用管它們了,因爲它們已經就定位,只要再處理第一柱的上面兩個盤子就可以了。那麼六個盤的情況呢?一樣!首先必須用遞迴完成下圖左上至右下的移動: 


接下來最底層的就不用管它們了,因爲它們已經就定位,只要再處理第一柱上面的四個盤子就可以了,這又與之前只有四盤的情況相同,接下來您就知道該如何進行解題了,無論是八個盤、十個盤以上等,都是用這個觀念來解題。
那麼三色河內塔呢?一樣,直接來看九個盤的情況,首先必須完成下圖的移動結果:


接下來最底兩層的就不用管它們了,因爲它們已經就定位,只要再處理第一柱上面的三個盤子就可以了。


雙色河內塔 C 實作 
#include <stdio.h>


void hanoi(int disks, char source, char temp, char target) {
    if (disks == 1) {
        printf("move disk from %c to %c\n", source, target);
        printf("move disk from %c to %c\n", source, target);
    } else {
        hanoi(disks-1, source, target, temp);
        hanoi(1, source, temp, target);
        hanoi(disks-1, temp, source, target);
    }
}


void hanoi2colors(int disks) {
    char source = 'A';
    char temp = 'B';
    char target = 'C';
    int i;
    for(i = disks / 2; i > 1; i--) {
        hanoi(i-1, source, temp, target);
        printf("move disk from %c to %c\n", source, temp);
        printf("move disk from %c to %c\n", source, temp);
        hanoi(i-1, target, temp, source);
        printf("move disk from %c to %c\n", temp, target);
    }
    printf("move disk from %c to %c\n", source, temp);
    printf("move disk from %c to %c\n", source, target);
}


int main() {
    int n;
    printf("請輸入盤數:");
    scanf("%d", &n);


    hanoi2colors(n);


    return 0;



三色河內塔 C 實作 
#include <stdio.h>


void hanoi(int disks, char source, char temp, char target) {
    if (disks == 1) {
        printf("move disk from %c to %c\n", source, target);
        printf("move disk from %c to %c\n", source, target);
        printf("move disk from %c to %c\n", source, target);
    } else {
        hanoi(disks-1, source, target, temp);
        hanoi(1, source, temp, target);
        hanoi(disks-1, temp, source, target);
    }
}


void hanoi3colors(int disks) {
    char source = 'A';
    char temp = 'B';
    char target = 'C';
    int i;
    if(disks == 3) {
        printf("move disk from %c to %c\n", source, temp);
        printf("move disk from %c to %c\n", source, temp);
        printf("move disk from %c to %c\n", source, target);
        printf("move disk from %c to %c\n", temp, target);
        printf("move disk from %c to %c\n", temp, source);
        printf("move disk from %c to %c\n", target, temp);;
    }
    else {
        hanoi(disks/3-1, source, temp, target);
        printf("move disk from %c to %c\n", source, temp);
        printf("move disk from %c to %c\n", source, temp);
        printf("move disk from %c to %c\n", source, temp);


        hanoi(disks/3-1, target, temp, source);
        printf("move disk from %c to %c\n", temp, target);
        printf("move disk from %c to %c\n", temp, target);
        printf("move disk from %c to %c\n", temp, target);


        hanoi(disks/3-1, source, target, temp);
        printf("move disk from %c to %c\n", target, source);
        printf("move disk from %c to %c\n", target, source);


        hanoi(disks/3-1, temp, source, target);
        printf("move disk from %c to %c\n", source, temp);
        
        for (i = disks / 3 - 1; i > 0; i--) {
            if (i>1) {
                hanoi(i-1, target, source, temp);
            }
            printf("move disk from %c to %c\n",target, source);
            printf("move disk from %c to %c\n",target, source);
            if (i>1) {
                hanoi(i-1, temp, source, target);
            }
            printf("move disk from %c to %c\n", source, temp);
        }
    }
}


int main() {
    int n;
    printf("請輸入盤數:");
    scanf("%d", &n);


    hanoi3colors(n);
    return 0;

13.Algorithm Gossip: 揹包問題(Knapsack Problem)
說明假設有一個揹包的負重最多可達8公斤,而希望在揹包中裝入負重範圍內可得之總價物品,假設是水果好了,水果的編號、單價與重量如下所示: 
0 李子 4KG NT$4500 
1 蘋果 5KG NT$5700 
2 橘子 2KG NT$2250 
3 草莓 1KG NT$1100 
4 甜瓜 6KG NT$6700 


解法揹包問題是關於最佳化的問題,要解最佳化問題可以使用「動態規劃」(Dynamic programming),從空集合開始,每增加一個元素就先求出該階段的最佳解,直到所有的元素加入至集合中,最後得到的就是最佳解。 


以揹包問題爲例,我們使用兩個陣列value與item,value表示目前的最佳解所得之總價,item表示最後一個放至揹包的水果,假設有負重量 1~8的揹包8個,並對每個揹包求其最佳解。 


逐步將水果放入揹包中,並求該階段的最佳解:
放入李子 
揹包負重 1 2 3 4 5 6 7
value 4500 4500 4500 4500 9000 
item 0 


放入蘋果 
揹包負重 1 2 3 4 5 6 7
value 4500 5700 5700 5700 9000 
item 1 1 1 0 


放入橘子 
揹包負重 1 2 3 4 5 6 7
value 2250 2250 4500 5700 6750 7950 9000 
item 2 2 1 2 2 0 


放入草莓 
揹包負重 1 2 3 4 5 6 7
value 1100 2250 3350 4500 5700 6800 7950 9050 
item 3 2 3 1 3 2


放入甜瓜 
揹包負重 1 2 3 4 5 6 7
value 1100 2250 3350 4500 5700 6800 7950 9050 
item 3 2 3 1 3 2


由最後一個表格,可以得知在揹包負重8公斤時,最多可以裝入9050元的水果,而最後一個裝入的 水果是3號,也就是草莓,裝入了草莓,揹包只能再放入7公斤(8-1)的水果,所以必須看揹包負重7公斤時的最佳解,最後一個放入的是2號,也就 是橘子,現在揹包剩下負重量5公斤(7-2),所以看負重5公斤的最佳解,最後放入的是1號,也就是蘋果,此時揹包負重量剩下0公斤(5-5),無法 再放入水果,所以求出最佳解爲放入草莓、橘子與蘋果,而總價爲9050元。


實作
 

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


#define LIMIT 8   // 重量限制 
#define N 5       // 物品種類 
#define MIN 1     // 最小重量 


struct body { 
    char name[20]; 
    int size; 
    int price; 
}; 


typedef struct body object; 


int main(void) { 
    int item[LIMIT+1] = {0}; 
    int value[LIMIT+1] = {0}; 
    int newvalue, i, s, p; 


    object a[] = {{"李子", 4, 4500}, 
                  {"蘋果", 5, 5700}, 
                  {"橘子", 2, 2250}, 
                  {"草莓", 1, 1100}, 
                  {"甜瓜", 6, 6700}}; 


    for(i = 0; i < N; i++) { 
        for(s = a[i].size; s <= LIMIT; s++) { 
            p = s - a[i].size; 
            newvalue = value[p] + a[i].price; 
            if(newvalue > value[s]) {// 找到階段最佳解 
                value[s] = newvalue; 
                item[s] = i; 
            } 
        } 
    } 


    printf("物品\t價格\n"); 
    for(i = LIMIT; i >= MIN; i = i - a[item[i]].size) { 
        printf("%s\t%d\n", 
                  a[item[i]].name, a[item[i]].price); 
    } 


    printf("合計\t%d\n", value[LIMIT]); 


    return 0; 
}  


Java 
class Fruit {
    private String name;
    private int size;
    private int price;
    
    public Fruit(String name, int size, int price) {
        this.name = name;
        this.size = size;
        this.price = price;
    }
    
    public String getName() {
        return name;
    }


    public int getPrice() {
        return price;
    }


    public int getSize() {
        return size;
    }
}


public class Knapsack {
    public static void main(String[] args) {
        final int MAX = 8;
        final int MIN = 1;
        int[] item = new int[MAX+1]; 
        int[] value = new int[MAX+1];  


        Fruit fruits[] = {
                new Fruit("李子", 4, 4500), 
                new Fruit("蘋果", 5, 5700), 
                new Fruit("橘子", 2, 2250), 
                new Fruit("草莓", 1, 1100), 
                new Fruit("甜瓜", 6, 6700)}; 


        for(int i = 0; i < fruits.length; i++) { 
            for(int s = fruits[i].getSize(); s <= MAX; s++) { 
                int p = s - fruits[i].getSize(); 
                int newvalue = value[p] + 
                                   fruits[i].getPrice(); 
                if(newvalue > value[s]) {// 找到階段最佳解 
                    value[s] = newvalue; 
                    item[s] = i; 
                } 
            } 
        } 


        System.out.println("物品\t價格"); 
        for(int i = MAX; 
            i >= MIN; 
            i = i - fruits[item[i]].getSize()) { 
            System.out.println(fruits[item[i]].getName()+ 
                    "\t" + fruits[item[i]].getPrice()); 
        } 


        System.out.println("合計\t" + value[MAX]);  
    }

























14.Algorithm Gossip: 蒙地卡羅法求 PI
 
說明蒙地卡羅爲摩洛哥王國之首都,該國位於法國與義大利國境,以賭博聞名。蒙地卡羅的基本原理爲以亂數配合面積公式來進行解題,這種以機率來解題的方式帶有賭博的意味,雖然在精確度上有所疑慮,但其解題的思考方向卻是個值得學習的方式。
解法蒙地卡羅的解法適用於與面積有關的題目,例如求PI值或橢圓面積,這邊介紹如何求PI值;假設有一個圓半徑爲1,所以四分之一圓面積就爲PI,而包括此四分之一圓的正方形面積就爲1,如下圖所示: 




如果隨意的在正方形中投射飛標(點)好了,則這些飛標(點)有些會落於四分之一圓內,假設所投射的飛標(點)有n點,在圓內的飛標(點)有c點,則依比例來算,就會得到上圖中最後的公式。


至於如何判斷所產生的點落於圓內,很簡單,令亂數產生X與Y兩個數值,如果X^2+Y^2等於1就是落在圓內。 
 
#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 


#define N 50000 


int main(void) { 
    int i, sum = 0; 
    double x, y; 


    srand(time(NULL)); 


    for(i = 1; i < N; i++) { 
        x = (double) rand() / RAND_MAX; 
        y = (double) rand() / RAND_MAX; 
        if((x * x + y * y) < 1) 
            sum++; 
    } 
    printf("PI = %f\n", (double) 4 * sum / N); 
    return 0; 







15.Algorithm Gossip: Eratosthenes篩選求質數
 
說明除了自身之外,無法被其它整數整除的數稱之爲質數,要求質數很簡單,但如何快速的求出質數則一直是程式設計人員與數學家努力的課題,在這邊介紹一個着名的 Eratosthenes求質數方法。
解法首先知道這個問題可以使用迴圈來求解,將一個指定的數除以所有小於它的數,若可以整除就不是質數,然而如何減少迴圈的檢查次數?如何求出小於N的所有質數?


首先假設要檢查的數是N好了,則事實上只要檢查至N的開根號就可以了,道理很簡單,假設A*B = N,如果A大於N的開根號,則事實上在小於A之前的檢查就可以先檢查到B這個數可以整除N。不過在程式中使用開根號會精確度的問題,所以可以使用 i*i <= N進行檢查,且執行更快。


再來假設有一個篩子存放1~N,例如:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ........ N


先將2的倍數篩去:
2 3 5 7 9 11 13 15 17 19 21 ........ N


再將3的倍數篩去:
2 3 5 7 11 13 17 19 ........ N


再來將5的倍數篩去,再來將7的質數篩去,再來將11的倍數篩去........,如此進行到最後留下的數就都是質數,這就是Eratosthenes篩選方法(Eratosthenes Sieve Method)。


檢查的次數還可以再減少,事實上,只要檢查6n+1與6n+5就可以了,也就是直接跳過2與3的倍數,使得程式中的if的檢查動作可以減少。 
實作
 

#include <stdio.h> 
#include <stdlib.h> 
#define N 1000 


int main(void) { 
    int i, j; 
    int prime[N+1]; 


    for(i = 2; i <= N; i++) 
        prime[i] = 1; 


    for(i = 2; i*i <= N; i++) { // 這邊可以改進 
        if(prime[i] == 1) { 
            for(j = 2*i; j <= N; j++) { 
                if(j % i == 0) 
                    prime[j] = 0; 
            } 
        } 
    } 


    for(i = 2; i < N; i++) { 
        if(prime[i] == 1) { 
            printf("%4d ", i); 
            if(i % 16 == 0) 
                printf("\n"); 
        } 
    } 


    printf("\n"); 
    return 0; 





16.Algorithm Gossip: 超長整數運算(大數運算)
說明基於記憶體的有效運用,程式語言中規定了各種不同的資料型態,也因此變數所可以表達的最大整數受到限制,例如123456789123456789這樣的 整數就不可能儲存在long變數中(例如C/C++等),我們稱這爲long數,這邊翻爲超長整數(避免與資料型態的長整數翻譯混淆),或俗稱大數運算。
解法一個變數無法表示超長整數,則就使用多個變數,當然這使用陣列最爲方便,假設程式語言的最大資料型態可以儲存至65535的數好了,爲了計算方便及符合使用十進位制的習慣,讓每一個陣列元素可以儲存四個位數,也就是0到9999的數,例如: 




很多人問到如何計算像50!這樣的問題,解法就是使用程式中的乘法函式,至於要算到多大,就看需求了。


由於使用陣列來儲存數值,關於數值在運算時的加減乘除等各種運算、位數的進位或借位就必須自行定義,加、減、乘都是由低位數開始運算,而除法則是由高位數開始運算,這邊直接提供加減乘除運算的函式供作參考,以下的N爲陣列長度。 


void add(int *a, int *b, int *c) { 
    int i, carry = 0; 


    for(i = N - 1; i >= 0; i--) { 
        c[i] = a[i] + b[i] + carry; 
        if(c[i] < 10000) 
            carry = 0; 
        else { // 進位 
            c[i] = c[i] - 10000; 
            carry = 1; 
        } 
    } 



void sub(int *a, int *b, int *c) { 
    int i, borrow = 0; 
    for(i = N - 1; i >= 0; i--) { 
        c[i] = a[i] - b[i] - borrow; 
        if(c[i] >= 0) 
            borrow = 0; 
        else { // 借位 
            c[i] = c[i] + 10000; 
            borrow = 1; 
        } 
    } 



void mul(int *a, int b, int *c) { // b 爲乘數 
    int i, tmp, carry = 0; 
    for(i = N - 1; i >=0; i--) { 
        tmp = a[i] * b + carry; 
        c[i] = tmp % 10000;    
        carry = tmp / 10000; 
    } 



void div(int *a, int b, int *c) {  // b 爲除數 
    int i, tmp, remain = 0; 
    for(i = 0; i < N; i++) { 
        tmp = a[i] + remain; 
        c[i] = tmp / b; 
        remain = (tmp % b) * 10000; 
    } 

17.Algorithm Gossip: 長 PI
說明圓周率後的小數位數是無止境的,如何使用電腦來計算這無止境的小數是一些數學家與程式設計師所感興趣的,在這邊介紹一個公式配合 大數運算,可以計算指定位數的圓周率。
解法首先介紹J.Marchin的圓周率公式:
PI = [16/5 - 16 / (3*53) + 16 / (5*55) - 16 / (7*57) + ......] -
      [4/239 - 4/(3*2393) + 4/(5*2395) - 4/(7*2397) + ......]


可以將這個公式整理爲:
PI = [16/5 - 4/239] - [16/(53) - 4/(2393)]/3+ [16/(55) - 4/(2395)]/5 + ......


也就是說第n項,若爲奇數則爲正數,爲偶數則爲負數,而項數表示方式爲:
[16/52*n-1 - 4/2392*n-1] / (2*n-1)


如果我們要計算圓周率至10的負L次方,由於[16/52*n-1 - 4/2392*n-1]中16/52*n-1比4/2392*n-1來的大,具有決定性,所以表示至少必須計算至第n項:
[16/52*n-1 ] / (2*n-1) = 10-L


將上面的等式取log並經過化簡,我們可以求得:
n = L / (2log5) = L / 1.39794


所以若要求精確度至小數後L位數,則只要求至公式的第n項,其中n等於:
n = [L/1.39794] + 1


在上式中[]爲高斯符號,也就是取至整數(不大於L/1.39794的整數);爲了計簡方便,可以在程式中使用下面這個公式來計簡第n項:
[Wn-1/52- Vn-1 / (2392)] / (2*n-1)


這個公式的演算法配合大數運算函式的演算法爲: div(w, 25, w); 
div(v, 239, v); 
div(v, 239, v); 
sub(w, v, q); 
div(q, 2*k-1, q) 
 
至於大數運算的演算法,請參考之前的文章,必須注意的是在輸出時,由於是輸出陣列中的整數值,如果陣列中整數位數不滿四位,則必須補上0,在C語言中只要 使用格式指定字%04d,使得不足位數部份自動補上0再輸出,至於Java的部份,使用 NumberFormat來作格式化。


#include <stdio.h> 
#define L 1000 
#define N L/4+1 


// L 爲位數,N是array長度 


void add(int*, int*, int*); 
void sub(int*, int*, int*); 
void div(int*, int, int*); 


int main(void) { 
    int s[N+3] = {0}; 
    int w[N+3] = {0}; 
    int v[N+3] = {0}; 
    int q[N+3] = {0}; 
    int n = (int)(L/1.39793 + 1); 
    int k; 


    w[0] = 16*5; 
    v[0] = 4*239; 


    for(k = 1; k <= n; k++) { 
        // 套用公式 
        div(w, 25, w); 
        div(v, 239, v); 
        div(v, 239, v); 
        sub(w, v, q); 
        div(q, 2*k-1, q); 


        if(k%2) // 奇數項 
            add(s, q, s); 
        else    // 偶數項 
            sub(s, q, s); 
    } 


    printf("%d.", s[0]); 
    for(k = 1; k < N; k++) 
        printf("%04d", s[k]); 
    printf("\n"); 
    return 0; 



void add(int *a, int *b, int *c) { 
    int i, carry = 0; 


    for(i = N+1; i >= 0; i--) { 
        c[i] = a[i] + b[i] + carry; 
        if(c[i] < 10000) 
            carry = 0; 
        else { // 進位 
            c[i] = c[i] - 10000; 
            carry = 1; 
        } 
    } 



void sub(int *a, int *b, int *c) { 
    int i, borrow = 0; 
    for(i = N+1; i >= 0; i--) { 
        c[i] = a[i] - b[i] - borrow; 
        if(c[i] >= 0) 
            borrow = 0; 
        else { // 借位 
            c[i] = c[i] + 10000; 
            borrow = 1; 
        } 
    } 



void div(int *a, int b, int *c) {  // b 爲除數 
    int i, tmp, remain = 0; 
    for(i = 0; i <= N+1; i++) { 
        tmp = a[i] + remain; 
        c[i] = tmp / b; 
        remain = (tmp % b) * 10000; 
    } 

18.Algorithm Gossip: 最大公因數、最小公倍數、因式分解
 
說明最大公因數使用輾轉相除法來求,最小公倍數則由這個公式來求:
GCD * LCM = 兩數乘積
解法最大公因數可以使用遞迴與非遞迴求解,因式分解基本上就是使用小於輸入數的數值當作除數,去除以輸入數值,如果可以整除就視爲因數,要比較快的解法就是求出小於該數的所有質數,並試試看是不是可以整除,求質數的問題是另一個課題,請參考 Eratosthenes 篩選求質數。


實作(最大公因數、最小公倍數)
#include <stdio.h> 
#include <stdlib.h> 


int main(void) { 
    int m, n, r; 
    int s;


    printf("輸入兩數:"); 
    scanf("%d %d", &m, &n); 
    s = m * n;


    while(n != 0) { 
        r = m % n; 
        m = n; 
        n = r; 
    } 


    printf("GCD:%d\n", m); 
    printf("LCM:%d\n", s/m); 


    return 0; 

實作(因式分解)
 
C(不用質數表) 
#include <stdio.h> 
#include <stdlib.h> 


int main(void) { 
    int i, n; 


    printf("請輸入整數:"); 
    scanf("%d", &n); 
    printf("%d = ", n); 


    for(i = 2; i * i <= n;) { 
        if(n % i == 0) { 
            printf("%d * ", i); 
            n /= i; 
        } 
        else 
            i++; 
    } 


    printf("%d\n", n); 


    return 0; 



C(使用質數表) 
#include <stdio.h> 
#include <stdlib.h> 


#define N 1000 


int prime(int*);  // 求質數表 
void factor(int*, int);  // 求factor 


int main(void) { 
    int ptable[N+1] = {0}; 
    int count, i, temp; 


    count = prime(ptable); 


    printf("請輸入一數:"); 
    scanf("%d", &temp); 
    factor(ptable, temp); 
    printf("\n"); 
    return 0; 
 } 


 int prime(int* pNum) { 
    int i, j; 
    int prime[N+1]; 
    for(i = 2; i <= N; i++) 
        prime[i] = 1; 


    for(i = 2; i*i <= N; i++) { 
        if(prime[i] == 1) { 
            for(j = 2*i; j <= N; j++) { 
                if(j % i == 0) 
                    prime[j] = 0; 
            } 
        } 
    } 


    for(i = 2, j = 0; i < N; i++) { 
        if(prime[i] == 1) 
            pNum[j++] = i; 
    } 
    return j; 

void factor(int* table, int num) { 
    int i; 
    for(i = 0; table[i] * table[i] <= num;) { 
        if(num % table[i] == 0) { 
            printf("%d * ", table[i]); 
            num /= table[i]; 
        } 
        else 
            i++; 
    } 
    printf("%d\n", num); 
}
19.Algorithm Gossip: 完美數
說明如果有一數n,其真因數(Proper factor)的總和等於n,則稱之爲完美數(Perfect Number),例如以下幾個數都是完美數:
6 = 1 + 2 + 3
28 = 1 + 2 + 4 + 7 + 14
496 = 1 + 2 + 4 + 8 + 16 + 31 + 62 + 124 + 248


程式基本上不難,第一眼看到時會想到使用迴圈求出所有真因數,再進一步求因數和,不過若n值很大,則此法會花費許多時間在迴圈測試上,十分沒有效率,例如求小於10000的所有完美數。
解法如何求小於10000的所有完美數?並將程式寫的有效率?基本上有三個步驟:
求出一定數目的質數表 
利用質數表求指定數的因式分解 
利用因式分解求所有真因數和,並檢查是否爲完美數 


步驟一 與 步驟二 在之前討論過了,問題在步驟三,如何求真因數和?方法很簡單,要先知道將所有真因數和加上該數本身,會等於該數的兩倍,例如:
2 * 28 = 1 + 2 + 4 + 7 + 14 + 28


等式後面可以化爲:
2 * 28 = (20 + 21 + 22) * (70 + 71)


所以只要求出因式分解,就可以利用迴圈求得等式後面的值,將該值除以2就是真因數和了;等式後面第一眼看時可能想到使用等比級數公式來解,不過會使用到次方運算,可以在迴圈走訪因式分解陣列時,同時計算出等式後面的值,這在下面的實作中可以看到。 
 
#include <stdio.h> 
#include <stdlib.h> 


#define N 1000 
#define P 10000 


int prime(int*);  // 求質數表 
int factor(int*, int, int*);  // 求factor 
int fsum(int*, int);  // sum ot proper factor 


int main(void) { 
    int ptable[N+1] = {0}; // 儲存質數表 
    int fact[N+1] = {0};   // 儲存因式分解結果 
    int count1, count2, i; 


    count1 = prime(ptable); 


    for(i = 0; i <= P; i++) { 
        count2 = factor(ptable, i, fact); 
        if(i == fsum(fact, count2)) 
            printf("Perfect Number: %d\n", i); 
    } 
    
    printf("\n"); 


    return 0; 



int prime(int* pNum) { 
    int i, j; 
    int prime[N+1]; 


    for(i = 2; i <= N; i++) 
        prime[i] = 1; 


    for(i = 2; i*i <= N; i++) { 
        if(prime[i] == 1) { 
            for(j = 2*i; j <= N; j++) { 
                if(j % i == 0) 
                    prime[j] = 0; 
            } 
        } 
    } 


    for(i = 2, j = 0; i < N; i++) { 
        if(prime[i] == 1) 
            pNum[j++] = i; 
    } 


    return j; 



int factor(int* table, int num, int* frecord) { 
    int i, k; 


    for(i = 0, k = 0; table[i] * table[i] <= num;) { 
        if(num % table[i] == 0) { 
            frecord[k] = table[i]; 
            k++; 
            num /= table[i]; 
        } 
        else 
            i++; 
    } 


    frecord[k] = num; 


    return k+1; 



int fsum(int* farr, int c) { 
    int i, r, s, q; 


    i = 0; 
    r = 1; 
    s = 1; 
    q = 1; 


    while(i < c) { 
        do { 
            r *= farr[i]; 
            q += r; 
            i++; 
        } while(i < c-1 && farr[i-1] == farr[i]); 
        s *= q; 
        r = 1; 
        q = 1; 
    } 


    return s / 2; 

20.Algorithm Gossip: 阿姆斯壯數
 
說明
 在三位的整數中,例如153可以滿足13 + 53 + 33 = 153,這樣的數稱之爲Armstrong數,試寫出一程式找出所有的三位數Armstrong數。
解法
 Armstrong數的尋找,其實就是在問如何將一個數字分解爲個位數、十位數、百位數......,這隻要使用除法與餘數運算就可以了,例如輸入 input爲abc,則:
a = input / 100 
b = (input%100) / 10 
c = input % 10 
 
#include <stdio.h> 
#include <time.h> 
#include <math.h> 


int main(void) { 
    int a, b, c; 
    int input; 


    printf("尋找Armstrong數:\n"); 


    for(input = 100; input <= 999; input++) { 
        a = input / 100; 
        b = (input % 100) / 10; 
        c = input % 10; 
        if(a*a*a + b*b*b + c*c*c == input) 
            printf("%d ", input); 
    } 


    printf("\n"); 


    return 0; 

21.Algorithm Gossip: 最大訪客數
說明
 現將舉行一個餐會,讓訪客事先填寫到達時間與離開時間,爲了掌握座位的數目,必須先估計不同時間的最大訪客數。
解法
 這個題目看似有些複雜,其實相當簡單,單就計算訪客數這個目的,同時考慮同一訪客的來訪時間與離開時間,反而會使程式變得複雜;只要將來訪時間與離開時間分開處理就可以了,假設訪客 i 的來訪時間爲x[i],而離開時間爲y[i]。


在資料輸入完畢之後,將x[i]與y[i]分別進行排序(由小到大),道理很簡單,只要先計算某時之前總共來訪了多少訪客,然後再減去某時之前的離開訪客,就可以輕易的解出這個問題。 




#include <stdio.h> 
#include <stdlib.h> 
#define MAX 100 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


int partition(int[], int, int); 
void quicksort(int[], int, int); // 快速排序法
int maxguest(int[], int[], int, int); 


int main(void) { 
    int x[MAX] = {0}; 
    int y[MAX] = {0}; 
    int time = 0; 
    int count = 0; 


    printf("\n輸入來訪與離開125;時間(0~24):"); 
    printf("\n範例:10 15"); 
    printf("\n輸入-1 -1結束"); 
    while(count < MAX) { 
        printf("\n>>"); 
        scanf("%d %d", &x[count], &y[count]); 
        if(x[count] < 0) 
            break; 
        count++; 
    } 


    if(count >= MAX) { 
        printf("\n超出最大訪客數(%d)", MAX); 
        count--; 
    } 


    // 預先排序 
    quicksort(x, 0, count); 
    quicksort(y, 0, count); 


    while(time < 25) { 
        printf("\n%d 時的最大訪客數:%d", 
                   time, maxguest(x, y, count, time)); 
        time++; 
    } 


    printf("\n"); 


    return 0; 



int maxguest(int x[], int y[], int count, int time) { 
    int i, num = 0; 


    for(i = 0; i <= count; i++) { 
        if(time > x[i]) 
            num++; 
        if(time > y[i]) 
            num--; 
    } 


    return num; 



int partition(int number[], int left, int right) { 
    int i, j, s; 


    s = number[right]; 
    i = left - 1; 


    for(j = left; j < right; j++) { 
        if(number[j] <= s) { 
            i++; 
            SWAP(number[i], number[j]); 
        } 
    } 


    SWAP(number[i+1], number[right]); 
    return i+1; 



void quicksort(int number[], int left, int right) { 
    int q; 


    if(left < right) { 
        q = partition(number, left, right); 
        quicksort(number, left, q-1); 
        quicksort(number, q+1, right); 
    } 







22.Algorithm Gossip: 中序式轉後序式(前序式)
說明平常所使用的運算式,主要是將運算元放在運算子的兩旁,例如a+b/d這樣的式子,這稱之爲中序(Infix)表示式,對於人類來說,這樣的式子很容易理 解,但由於電腦執行指令時是有順序的,遇到中序表示式時,無法直接進行運算,而必須進一步判斷運算的先後順序,所以必須將中序表示式轉換爲另一種表示方 法。


可以將中序表示式轉換爲後序(Postfix)表示式,後序表示式又稱之爲逆向波蘭表示式(Reverse polish notation),它是由波蘭的數學家盧卡謝維奇提出,例如(a+b)*(c+d)這個式子,表示爲後序表示式時是ab+cd+*。
解法用手算的方式來計算後序式相當的簡單,將運算子兩旁的運算元依先後順序全括號起來,然後將所有的右括號取代爲左邊最接近的運算子(從最內層括號開始),最後去掉所有的左括號就可以完成後序表示式,例如:
a+b*d+c/d  =>  ((a+(b*d))+(c/d)) -> bd*+cd/+


如果要用程式來進行中序轉後序,則必須使用堆疊,演算法很簡單,直接敘述的話就是使用迴圈,取出中序式的字元,遇運算元直接輸出,堆疊運算子與左括號, ISP>ICP的話直接輸出堆疊中的運算子,遇右括號輸出堆疊中的運算子至左括號。 
例如(a+b)*(c+d)這個式子,依演算法的輸出過程如下: OP STACK OUTPUT 
( (
a (
+ (+
b (+ ab 
) - ab+ 
* * ab+ 
( *( ab+ 
c *( ab+c 
+ *(+ ab+c 
d *(+ ab+cd 
) * ab+cd+ 
- - ab+cd+* 


如果要將中序式轉爲前序式,則在讀取中序式時是由後往前讀取,而左右括號的處理方式相反,其餘不變,但輸出之前必須先置入堆疊,待轉換完成後再將堆疊中的 值由上往下讀出,如此就是前序表示式。 
實作
 

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


int postfix(char*); // 中序轉後序 
int priority(char); // 決定運算子優先順序 


int main(void) { 
    char input[80]; 


    printf("輸入中序運算式:"); 
    scanf("%s", input); 
    postfix(input); 


    return 0; 



int postfix(char* infix) { 
    int i = 0, top = 0; 
    char stack[80] = {'\0'}; 
    char op; 


    while(1) { 
        op = infix[i]; 


        switch(op) { 
            case '\0': 
                while(top > 0) { 
                    printf("%c", stack[top]); 
                    top--; 
                } 
                printf("\n"); 
                return 0; 
            // 運算子堆疊 
            case '(': 
                if(top < (sizeof(stack) / sizeof(char))) { 
                    top++; 
                    stack[top] = op; 
                } 
                break; 
            case '+': case '-': case '*': case '/': 
                while(priority(stack[top]) >= priority(op)) { 
                    printf("%c", stack[top]); 
                    top--; 
                } 
                // 存入堆疊 
                if(top < (sizeof(stack) / sizeof(char))) { 
                    top++; 
                    stack[top] = op; 
                } 
                break; 
            // 遇 ) 輸出至 ( 
            case ')': 
                while(stack[top] != '(') { 
                    printf("%c", stack[top]); 
                    top--; 
                } 
                top--;  // 不輸出( 
                break; 
            // 運算元直接輸出 
            default: 
                printf("%c", op); 
                break; 
        } 
        i++; 
    } 



int priority(char op) { 
    int p; 


    switch(op) { 
       case '+': case '-': 
            p = 1; 
            break; 
        case '*': case '/': 
            p = 2; 
            break; 
        default: 
            p = 0; 
            break; 
    } 


    return p; 























23.Algorithm Gossip: 後序式的運算
 說明 將中序式轉換爲後序式的好處是,不用處理運算子先後順序問題,只要依序由運算式由前往後讀取即可。 
解法
 運算時由後序式的前方開始讀取,遇到運算元先存入堆疊,如果遇到運算子,則由堆疊中取出兩個運算元進行對應的運算,然後將結果存回堆疊,如果運算式讀取完 畢,那麼堆疊頂的值就是答案了,例如我們計算12+34+*這個運算式(也就是(1+2)*(3+4)): 讀取 堆疊 
1
2 1 2 
+ 3 // 1+2 後存回 
3 3 3 
4 3 3 4 
+ 3 7 // 3+4 後存回 
* 21 // 3 * 7 後存回


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


void evalPf(char*); 
double cal(double, char, double); 


int main(void) { 
    char input[80]; 
    printf("輸入後序式:"); 
    scanf("%s", input); 
    evalPf(input); 
    return 0; 



void evalPf(char* postfix) { 
    double stack[80] = {0.0}; 
    char temp[2]; 
    char token; 
    int top = 0, i = 0; 
    temp[1] = '\0'; 
    while(1) { 
        token = postfix[i]; 
        switch(token) { 
            case '\0': 
                printf("ans = %f\n", stack[top]); 
                return; 
            case '+': case '-': case '*': case '/': 
                stack[top-1] = 
                       cal(stack[top], token, stack[top-1]); 
                top--; 
                break; 
            default: 
                if(top < sizeof(stack) / sizeof(float)) { 
                    temp[0] = postfix[i]; 
                    top++; 
                    stack[top] = atof(temp); 
                } 
                break; 
        } 
        i++; 
    } 

double cal(double p1, char op, double p2) { 
    switch(op) { 
        case '+': 
            return p1 + p2; 
        case '-': 
            return p1 - p2; 
        case '*': 
            return p1 * p2; 
        case '/': 
            return p1 / p2; 
    } 

24.Algorithm Gossip: 洗撲克牌(亂數排列)
說明
 洗撲克牌的原理其實與亂數排列是相同的,都是將一組數字(例如1~N)打亂重新排列,只不過洗撲克牌多了一個花色判斷的動作而已。
解法
 初學者通常會直接想到,隨機產生1~N的亂數並將之存入陣列中,後來產生的亂數存入陣列前必須先檢查陣列中是否已有重複的數字,如果有這個數就不存入,再重新產生下一個數,運氣不好的話,重複的次數就會很多,程式的執行速度就很慢了,這不是一個好方法。


以1~52的亂數排列爲例好了,可以將陣列先依序由1到52填入,然後使用一個迴圈走訪陣列,並隨機產生1~52的亂數,將產生的亂數當作索引取出陣列值,並與目前陣列走訪到的值相交換,如此就不用擔心亂數重複的問題了,陣列走訪完畢後,所有的數字也就重新排列了。


至於如何判斷花色?這只是除法的問題而已,取商數判斷花色,取餘數判斷數字,您可以直接看程式比較清楚。
實作
 

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 52


int main(void) {
    int poker[N + 1];
    int i, j, tmp, remain;


    // 初始化陣列 
    for(i = 1; i <= N; i++)
        poker[i] = i; 


    srand(time(0));


    // 洗牌 
    for(i = 1; i <= N; i++) {
        j = rand() % 52 + 1;
        tmp = poker[i];
        poker[i] = poker[j]; 
        poker[j] = tmp; 
    }


    for(i = 1; i <= N; i++) {
        // 判斷花色 
        switch((poker[i]-1) / 13) { 
            case 0: 
                printf("桃"); break;
            case 1: 
                printf("心"); break;
            case 2: 
                printf("磚"); break;
            case 3: 
                printf("梅"); break;
        } 


        // 撲克牌數字 
        remain = poker[i] % 13;
        switch(remain) { 
            case 0: 
                printf("K "); break;
            case 12: 
                printf("Q "); break;
            case 11: 
                printf("J "); break;
            default: 
                printf("%d ", remain); break;
        } 


        if(i % 13 == 0)
            printf("\n");
    } 


    return 0;



25.Algorithm Gossip: Craps賭博遊戲
說明一個簡單的賭博遊戲,遊戲規則如下:玩家擲兩個骰子,點數爲1到6,如果第一次點數和爲7或11,則玩家勝,如果點數和爲2、3或12,則玩家輸,如果和 爲其它點數,則記錄第一次的點數和,然後繼續擲骰,直至點數和等於第一次擲出的點數和,則玩家勝,如果在這之前擲出了點數和爲7,則玩家輸。
解法 規則看來有些複雜,但是其實只要使用switch配合if條件判斷來撰寫即可,小心不要弄錯勝負順序即可。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define WON 0
#define LOST 1
#define CONTINUE 2


int rollDice() { 
    return (rand() % 6) + (rand() % 6) + 2;
}


int main(void) {
    int firstRoll = 1;
    int gameStatus = CONTINUE;
    int die1, die2, sumOfDice;
    int firstPoint = 0;
    char c;


    srand(time(0));


    printf("Craps賭博遊戲,按Enter鍵開始遊戲****");


    while(1) {
         getchar();


        if(firstRoll) {
            sumOfDice = rollDice();
            printf("\n玩家擲出點數和:%d\n", sumOfDice);


            switch(sumOfDice) {
                case 7: case 11:
                    gameStatus = WON; break;
                case 2: case 3: case 12:
                    gameStatus = LOST; break;
                default:
                    firstRoll = 0;
                    gameStatus = CONTINUE;
                    firstPoint = sumOfDice;
                    break;
            }
        }
        else {
            sumOfDice = rollDice();
            printf("\n玩家擲出點數和:%d\n", sumOfDice);


            if(sumOfDice == firstPoint)
                gameStatus = WON;
            else if(sumOfDice == 7)
                gameStatus = LOST;
        }


        if(gameStatus == CONTINUE)
            puts("未分勝負,再擲一次****\n");
        else {
            if(gameStatus == WON)
                puts("玩家勝");
            else
                puts("玩家輸");


            printf("再玩一次?");
            scanf("%c", &c);
            if(c == 'n') {
                puts("遊戲結束");
                break;
            }
            firstRoll = 1;
        }
    }
    return 0;

26.Algorithm Gossip: 約瑟夫問題(Josephus Problem)
說明據說着名猶太歷史學家 Josephus有過以下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人 開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡爲止。


然而Josephus 和他的朋友並不想遵從,Josephus要他的朋友先假裝遵從,他將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。
解法約瑟夫問題可用代數分析來求解,將這個問題擴大好了,假設現在您與m個朋友不幸參與了這個遊戲,您要如何保護您與您的朋友?只要畫兩個圓圈就可以讓自己與朋友免於死亡遊戲,這兩個圓圈內圈是排列順序,而外圈是自殺順序,如下圖所示: 


使用程式來求解的話,只要將陣列當作環狀來處理就可以了,在陣列中由計數1開始,每找到三個無資料區就填入一個計數,直而計數達41爲止,然後將陣列由索引1開始列出,就可以得知每個位置的自殺順序,這就是約瑟夫排列,41個人而報數3的約琴夫排列如下所示:
14 36 1 38 15 2 24 30 3 16 34 4 25 17 5 40 31 6 18 26 7 37 19 8 35 27 9 20 32 10 41 21 11 28 39 12 22 33 13 29 23


由上可知,最後一個自殺的是在第31個位置,而倒數第二個自殺的要排在第16個位置,之前的人都死光了,所以他們也就不知道約琴夫與他的朋友並沒有遵守遊戲規則了。 


#include <stdio.h> 
#include <stdlib.h> 
#define N 41 
#define M 3 


int main(void) { 
    int man[N] = {0}; 
    int count = 1; 
    int i = 0, pos = -1; 
    int alive = 0; 


    while(count <= N) { 
        do { 
            pos = (pos+1) % N;  // 環狀處理 
            if(man[pos] == 0) 
                i++; 


            if(i == M) {  // 報數爲3了 
                i = 0; 
                break; 
            } 
        } while(1); 


        man[pos] = count; 
        count++; 
    } 
    printf("\n約琴夫排列:"); 
    for(i = 0; i < N; i++) 
        printf("%d ", man[i]); 
    printf("\n\n您想要救多少人?"); 
    scanf("%d", &alive); 
    printf("\nL表示這%d人要放的位置:\n", alive); 
    for(i = 0; i < N; i++) { 
        if(man[i] > alive) printf("D"); 
        else printf("L"); 
        if((i+1) % 5 == 0) printf("  "); 
    } 
    printf("\n"); 
return 0; 

27.Algorithm Gossip: 排列組合
說明將一組數字、字母或符號進行排列,以得到不同的組合順序,例如1 2 3這三個數的排列組合有:1 2 3、1 3 2、2 1 3、2 3 1、3 1 2、3 2 1。
解法可以使用遞迴將問題切割爲較小的單元進行排列組合,例如1 2 3 4的排列可以分爲1 [2 3 4]、2 [1 3 4]、3 [1 2 4]、4 [1 2 3]進行排列,這邊利用旋轉法,先將旋轉間隔設爲0,將最右邊的數字旋轉至最左邊,並逐步增加旋轉的間隔,例如:
1 2 3 4 -> 旋轉1 -> 繼續將右邊2 3 4進行遞迴處理
2 1 3 4 -> 旋轉1 2 變爲 2 1-> 繼續將右邊1 3 4進行遞迴處理
3 1 2 4 -> 旋轉1 2 3變爲 3 1 2 -> 繼續將右邊1 2 4進行遞迴處理
4 1 2 3 -> 旋轉1 2 3 4變爲4 1 2 3 -> 繼續將右邊1 2 3進行遞迴處理 


#include <stdio.h> 
#include <stdlib.h> 
#define N 4 


void perm(int*, int); 


int main(void) { 
    int num[N+1], i; 
    for(i = 1; i <= N; i++) 
        num[i] = i; 
    perm(num, 1); 
    return 0; 



void perm(int* num, int i) { 
    int j, k, tmp; 


    if(i < N) { 
        for(j = i; j <= N; j++) { 
            tmp = num[j]; 
            // 旋轉該區段最右邊數字至最左邊 
            for(k = j; k > i; k--) 
                num[k] = num[k-1]; 
            num[i] = tmp; 
            perm(num, i+1); 
            // 還原 
            for(k = i; k < j; k++) 
                num[k] = num[k+1]; 
            num[j] = tmp; 
        } 
    } 
    else {  // 顯示此次排列 
        for(j = 1; j <= N; j++) 
            printf("%d ", num[j]); 
        printf("\n"); 
    } 
}  






28.Algorithm Gossip: 格雷碼(Gray Code)
說明
 Gray Code是一個數列集合,每個數使用二進位來表示,假設使用n位元來表示每個數好了,任兩個數之間只有一個位元值不同,例如以下爲3位元的Gray Code:
000 001 011 010 110 111 101 100


由定義可以知道,Gray Code的順序並不是唯一的,例如將上面的數列反過來寫,也是一組Gray Code:
100 101 111 110 010 011 001 000


Gray Code是由貝爾實驗室的Frank Gray在1940年代提出的,用來在使用PCM(Pusle Code Modulation)方法傳送訊號時避免出錯,並於1953年三月十七日取得美國專利。
解法
 由於Gray Code相鄰兩數之間只改變一個位元,所以可觀 察Gray Code從1變0或從0變1時的位置,假設有4位元的Gray Code如下:
0000 0001 0011 0010 0110 0111 0101 0100
1100 1101 1111 1110 1010 1011 1001 1000


觀察奇數項的變化時,我們發現無論它是第幾個Gray Code,永遠只改變最右邊的位元,如果是1就改爲0,如果是0就改爲1。


觀察偶數項的變化時,我們發現所改變的位元,是由右邊算來第一個1的左邊位元。


以上兩個變化規則是固定的,無論位元數爲何;所以只要判斷位元的位置是奇數還是偶數,就可以決定要改變哪一個位元的值,爲了程式撰寫方便,將陣列索引 0當作最右邊的值,而在列印結果時,是由索引數字大的開始反向列印。


將2位元的Gray Code當作平面座標來看,可以構成一個四邊形,您可以發現從任一頂點出發,繞四邊形周長繞一圈,所經過的頂點座標就是一組Gray Code,所以您可以得到四組Gray Code。


同樣的將3位元的Gray Code當作平面座標來看的話,可以構成一個正立方體,如果您可以從任一頂點出發,將所有的邊長走過,並不重複經過頂點的話,所經過的頂點座標順序之組合也就是一組Gray Code。 


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


#define MAXBIT 20 
#define TRUE 1 
#define CHANGE_BIT(x) x = ((x) == '0' ? '1' : '0') 
#define NEXT(x) x = (1 - (x)) 


int main(void) { 
    char digit[MAXBIT]; 
    int i, bits, odd; 


    printf("輸入位元數:"); 
    scanf("%d", &bits); 


    for(i = 0; i < bits; i++) { 
        digit[i] = '0'; 
        printf("0"); 
    } 


    printf("\n"); 


    odd = TRUE; 


    while(1) { 
        if(odd) 
            CHANGE_BIT(digit[0]); 
        else { 
            // 計算第一個1的位置 
            for(i = 0; i < bits && digit[i] == '0'; i++) ; 
            if(i == bits - 1) // 最後一個Gray Code 
                 break; 
            CHANGE_BIT(digit[i+1]); 
        } 
        for(i = bits - 1; i >= 0; i--) 
            printf("%c", digit[i]); 


        printf("\n"); 
        NEXT(odd); 
    } 
    return 0; 

29.Algorithm Gossip: 產生可能的集合
 
說明
 給定一組數字或符號,產生所有可能的集合(包括空集合),例如給定1 2 3,則可能的集合爲:{}、{1}、{1,2}、{1,2,3}、{1,3}、{2}、{2,3}、{3}。
解法
 如果不考慮字典順序,則有個簡單的方法可以產生所有的集合,思考二進位數字加法,並注意1出現的位置,如果每個位置都對應一個數字,則由1所對應的數字所產生的就是一個集合,例如: 
000 {} 
001 {3} 
010 {2} 
011 {2,3} 
100 {1} 
101 {1,3} 
110 {1,2} 
111 {1,2,3} 


瞭解這個方法之後,剩下的就是如何產生二進位數?有許多方法可以使用,您可以使用unsigned型別加上&位元運算來產生,這邊則是使用陣列搜 尋,首先陣列內容全爲0,找第一個1,在還沒找到之前將走訪過的內容變爲0,而第一個找到的0則變爲 1,如此重複直到所有的陣列元素都變爲1爲止,例如:
000 => 100 => 010 => 110 => 001 => 101 => 011 => 111


如果要產生字典順序,例如若有4個元素,則:
{} => {1} => {1,2} => {1,2,3} => {1,2,3,4} =>
{1,2,4} =>
{1,3} => {1,3,4} =>
{1,4} =>
{2} => {2,3} => {2,3,4} =>
{2,4} =>
{3} => {3,4} =>
{4}


簡單的說,如果有n個元素要產生可能的集合,當依序產生集合時,如果最後一個元素是n,而倒數第二個元素是m的話,例如:
{a b c d e n}


則下一個集合就是{a b c d e+1},再依序加入後續的元素。


例如有四個元素,而當產生{1 2 3 4}集合時,則下一個集合就是{1 2 3+1},也就是{1 2 4},由於最後一個元素還是4,所以下一個集合就是{1 2+1},也就是{1 3},接下來再加入後續元素4,也就是{1 3 4},由於又遇到元素4,所以下一個集合是{1 3+1},也就是{1 4}。
實作
 C(無字典順序) 
#include <stdio.h> 
#include <stdlib.h> 


#define MAXSIZE 20 


int main(void) { 
    char digit[MAXSIZE]; 
    int i, j; 
    int n; 


    printf("輸入集合個數:"); 
    scanf("%d", &n); 


    for(i = 0; i < n; i++) 
        digit[i] = '0'; 


    printf("\n{}"); // 空集合 


    while(1) { 
        // 找第一個0,並將找到前所經過的元素變爲0 
        for(i = 0; i < n && digit[i] == '1'; digit[i] = '0', i++); 


        if(i == n)  // 找不到0 
            break; 
        else          // 將第一個找到的0變爲1 
            digit[i] = '1'; 


        // 找第一個1,並記錄對應位置 
        for(i = 0; i < n && digit[i] == '0'; i++); 


        printf("\n{%d", i+1); 
    
        for(j = i + 1; j < n; j++) 
            if(digit[j] == '1') 
                printf(",%d", j + 1); 


        printf("}"); 
    } 
    
    printf("\n"); 


    return 0; 



C(字典順序) 
#include <stdio.h> 
#include <stdlib.h> 


#define MAXSIZE 20 


int main(void) { 
    int set[MAXSIZE]; 
    int i, n, position = 0; 


    printf("輸入集合個數:"); 
    scanf("%d", &n); 
    printf("\n{}"); 
    set[position] = 1; 


    while(1) { 
        printf("\n{%d", set[0]);  // 印第一個數 
        for(i = 1; i <= position; i++) 
            printf(",%d", set[i]); 
        printf("}"); 


        if(set[position] < n) {  // 遞增集合個數 
            set[position+1] = set[position] + 1; 
            position++; 
        } 
        else if(position != 0) {  // 如果不是第一個位置 
            position--;       // 倒退 
            set[position]++;  // 下一個集合尾數 
        } 
        else  // 已倒退至第一個位置 
            break; 
    } 


    printf("\n"); 


    return 0; 





























30.Algorithm Gossip: m元素集合的n個元素子集
 
說明
 假設有個集合擁有m個元素,任意的從集合中取出n個元素,則這n個元素所形成的可能子集有那些?
解法
 假設有5個元素的集點,取出3個元素的可能子集如下:
{1 2 3}、{1 2 4 }、{1 2 5}、{1 3 4}、{1 3 5}、{1 4 5}、{2 3 4}、{2 3 5}、{2 4 5}、{3 4 5}


這些子集已經使用字典順序排列,如此纔可以觀察出一些規則:
如果最右一個元素小於m,則如同碼錶一樣的不斷加1 
如果右邊一位已至最大值,則加1的位置往左移 
每次加1的位置往左移後,必須重新調整右邊的元素爲遞減順序 


所以關鍵點就在於哪一個位置必須進行加1的動作,到底是最右一個位置要加1?還是其它的位置?


在實際撰寫程式時,可以使用一個變數positon來記錄加1的位置,position的初值設定爲n-1,因爲我們要使用陣列,而最右邊的索引值爲最大 的n-1,在position位置的值若小於m就不斷加1,如果大於m了,position就減1,也就是往左移一個位置;由於位置左移後,右邊的元素會 經過調整,所以我們必須檢查最右邊的元素是否小於m,如果是,則position調整回n-1,如果不是,則positon維持不變。 
實作
 

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


#define MAX 20 


int main(void) { 
    int set[MAX]; 
    int m, n, position; 
    int i; 


    printf("輸入集合個數 m:"); 
    scanf("%d", &m); 
    printf("輸入取出元素 n:"); 
    scanf("%d", &n); 


    for(i = 0; i < n; i++) 
        set[i] = i + 1; 


    // 顯示第一個集合 
    for(i = 0; i < n; i++) 
        printf("%d ", set[i]); 
    putchar('\n'); 
    
    position = n - 1; 


    while(1) { 
        if(set[n-1] == m) 
            position--; 
        else 
            position = n - 1; 


        set[position]++; 


        // 調整右邊元素 
        for(i = position + 1; i < n; i++) 
            set[i] = set[i-1] + 1; 


        for(i = 0; i < n; i++) 
            printf("%d ", set[i]); 
        putchar('\n'); 


        if(set[0] >= m - n + 1) 
            break; 
    } 


    return 0; 

31.Algorithm Gossip: 數字拆解
 
說明
 這個題目來自於 數字拆解,我將之改爲C語言的版本,並加上說明。


題目是這樣的:
3 = 2+1 = 1+1+1 所以3有三種拆法
4 = 3 + 1 = 2 + 2 = 2 + 1 + 1 = 1 + 1 + 1 + 1 共五種
5 = 4 + 1 = 3 + 2 = 3 + 1 + 1 = 2 + 2 + 1 = 2 + 1 + 1 + 1 = 1 + 1 +1 +1 +1


共七種


依此類推,請問一個指定數字NUM的拆解方法個數有多少個?
解法
 我們以上例中最後一個數字5的拆解爲例,假設f( n )爲數字n的可拆解方式個數,而f(x, y)爲使用y以下的數字來拆解x的方法個數,則觀察:
5 = 4 + 1 = 3 + 2 = 3 + 1 + 1 = 2 + 2 + 1 = 2 + 1 + 1 + 1 = 1 + 1 +1 +1 +1


使用函式來表示的話:
f(5) = f(4, 1) + f(3,2) + f(2,3) + f(1,4) + f(0,5)


其中f(1, 4) = f(1, 3) + f(1, 2) + f(1, 1),但是使用大於1的數字來拆解1沒有意義,所以f(1, 4) = f(1, 1),而同樣的,f(0, 5)會等於f(0, 0),所以:
f(5) = f(4, 1) + f(3,2) + f(2,3) + f(1,1) + f(0,0)


依照以上的說明,使用動態程式規畫(Dynamic programming)來進行求解,其中f(4,1)其實就是f(5-1, min(5-1,1)),f(x, y)就等於f(n-y, min(n-x, y)),其中n爲要拆解的數字,而min()表示取兩者中較小的數。


使用一個二維陣列表格table[x][y]來表示f(x, y),剛開始時,將每列的索引0與索引1元素值設定爲1,因爲任何數以0以下的數拆解必只有1種,而任何數以1以下的數拆解也必只有1種:
for(i = 0; i < NUM +1; i++){ 
    table[i][0] = 1; // 任何數以0以下的數拆解必只有1種 
    table[i][1] = 1; // 任何數以1以下的數拆解必只有1種 
}
 


接下來就開始一個一個進行拆解了,如果數字爲NUM,則我們的陣列維度大小必須爲NUM x (NUM/2+1),以數字10爲例,其維度爲10 x 6我們的表格將會如下所示:
1 1 0 0 0 0
1 1 0 0 0 0
1 1 2 0 0 0
1 1 2 3 0 0
1 1 3 4 5 0
1 1 3 5 6 7
1 1 4 7 9 0
1 1 4 8 0 0
1 1 5 0 0 0
1 1 0 0 0 0 


實作
 

#include <stdio.h> 
#include <stdlib.h> 
#define NUM 10    //  要拆解的數字 
#define DEBUG 0 


int main(void) { 
    int table[NUM][NUM/2+1] = {0}; // 動態規畫表格 
    int count = 0; 
    int result = 0; 
    int i, j, k; 


    printf("數字拆解\n"); 
    printf("3 = 2+1 = 1+1+1 所以3有三種拆法\n"); 
    printf("4 = 3 + 1 = 2 + 2 = 2 + 1 + 1 = 1 + 1 + 1 + 1");   
    printf("共五種\n"); 
    printf("5 = 4 + 1 = 3 + 2 = 3 + 1 + 1");
    printf(" = 2 + 2 + 1 = 2 + 1 + 1 + 1 = 1 + 1 +1 +1 +1");
    printf("共七種\n"); 
    printf("依此類推,求 %d 有幾種拆法?", NUM); 


    // 初始化 
    for(i = 0; i < NUM; i++){ 
        table[i][0] = 1;  // 任何數以0以下的數拆解必只有1種 
        table[i][1] = 1;  // 任何數以1以下的數拆解必只有1種 
    }        


    // 動態規劃 
    for(i = 2; i <= NUM; i++){ 
       for(j = 2; j <= i; j++){ 
            if(i + j > NUM) // 大於 NUM 
                continue; 
            
            count = 0;    
            for(k = 1 ; k <= j; k++){ 
                count += table[i-k][(i-k >= k) ? k : i-k];                  
            } 
            table[i][j] = count; 
        }            
    } 


    // 計算並顯示結果 
    for(k = 1 ; k <= NUM; k++) 
        result += table[NUM-k][(NUM-k >= k) ? k : NUM-k];                    
    printf("\n\nresult: %d\n", result); 


    if(DEBUG) { 
        printf("\n除錯資訊\n"); 
        for(i = 0; i < NUM; i++) { 
            for(j = 0; j < NUM/2+1; j++) 
                 printf("%2d", table[i][j]); 
            printf("\n"); 
        } 
    } 


    return 0; 

















32.Algorithm Gossip: 得分排行
說明假設有一教師依學生座號輸入考試分數,現希望在輸入完畢後自動顯示學生分數的排行,當然學生的分數可能相同。
解法這個問題基本上要解不難,只要使用額外的一個排行陣列走訪分數陣列就可以了,直接使用下面的程式片段作說明:
for(i = 0; i < count; i++) { 
    juni[i] = 1; 
    for(j = 0; j < count; j++) { 
        if(score[j] > score[i]) 
            juni[i]++; 
    } 

printf("得分\t排行\n"); 
for(i = 0; i < count; i++) 
    printf("%d\t%d\n", score[i], juni[i]); 


上面這個方法雖然簡單,但是反覆計算的次數是n^2,如果n值變大,那麼運算的時間就會拖長;改變juni陣列的長度爲n+2,並將初始值設定爲0,如下所示: 


接下來走訪分數陣列,並在分數所對應的排行陣列索引元素上加1,如下所示:


將排行陣列最右邊的元素設定爲1,然後依序將右邊的元素值加至左邊一個元素,最後排行陣列中的「分數+1」」就是得該分數的排行,如下所示: 


這樣的方式看起來複雜,其實不過在計算某分數之前排行的人數,假設89分之前的排行人數爲x人,則89分自然就是x+1了,這也是爲什麼排行陣列最右邊要設定爲1的原因;如果89分有y人,則88分自然就是x+y+1,整個陣列右邊元素向左加的原因正是如此。
如果分數有負分的情況,由於C/C++或Java等程式語言無法處理負的索引,所以必須加上一個偏移值,將所有的分數先往右偏移一個範圍即可,最後顯示的時候記得減回偏移值就可以了。 


#include <stdio.h> 
#include <stdlib.h> 
#define MAX 100 
#define MIN 0 


int main(void) { 
    int score[MAX+1] = {0}; 
    int juni[MAX+2] = {0}; 
    int count = 0, i; 


    do { 
       printf("輸入分數,-1結束:"); 
       scanf("%d", &score[count++]); 
    } while(score[count-1] != -1);
    count--; 


    for(i = 0; i < count; i++) 
        juni[score[i]]++; 
    juni[MAX+1] = 1; 


    for(i = MAX; i >= MIN; i--) 
        juni[i] = juni[i] + juni[i+1]; 
    printf("得分\t排行\n"); 
    for(i = 0; i < count; i++) 
        printf("%d\t%d\n", score[i], juni[score[i]+1]); 


    return 0; 















33.Algorithm Gossip: 選擇、插入、氣泡排序
說明選擇排序(Selection sort)、插入排序(Insertion sort)與氣泡排序(Bubble sort)這三個排序方式是初學排序所必須知道的三個基本排序方式,它們由於速度不快而不實用(平均與最快的時間複雜度都是O(n2)),然而它們排序的方式確是值得觀察與探討的。
解法
選擇排序 
將要排序的對象分作兩部份,一個是已排序的,一個是未排序的,從後端未排序部份選擇一個最小值,並放入前端已排序部份的最後一個,例如:


排序前:70 80 31 37 10 1 48 60 33 80


[1] 80 31 37 10 70 48 60 33 80 選出最小值1 
[1 10] 31 37 80 70 48 60 33 80 選出最小值10 
[1 10 31] 37 80 70 48 60 33 80 選出最小值31 
[1 10 31 33] 80 70 48 60 37 80 ...... 
[1 10 31 33 37] 70 48 60 80 80 ...... 
[1 10 31 33 37 48] 70 60 80 80 ...... 
[1 10 31 33 37 48 60] 70 80 80 ...... 
[1 10 31 33 37 48 60 70] 80 80 ...... 
[1 10 31 33 37 48 60 70 80] 80 ...... 


插入排序 
像是玩樸克一樣,我們將牌分作兩堆,每次從後面一堆的牌抽出最前端的牌,然後插入前面一堆牌的適當位置,例如:


排序前:92 77 67 8 6 84 55 85 43 67


[77 92] 67 8 6 84 55 85 43 67 將77插入92前 
[67 77 92] 8 6 84 55 85 43 67 將67插入77前 
[8 67 77 92] 6 84 55 85 43 67 將8插入67前 
[6 8 67 77 92] 84 55 85 43 67 將6插入8前 
[6 8 67 77 84 92] 55 85 43 67 將84插入92前 
[6 8 55 67 77 84 92] 85 43 67 將55插入67前 
[6 8 55 67 77 84 85 92] 43 67 ...... 
[6 8 43 55 67 77 84 85 92] 67 ...... 
[6 8 43 55 67 67 77 84 85 92] ...... 


氣泡排序法 
顧名思義,就是排序時,最大的元素會如同氣泡一樣移至右端,其利用比較相鄰元素的方法,將大的元素交換至右端,所以大的元素會不斷的往右移動,直到適當的位置爲止。


基本的氣泡排序法可以利用旗標的方式稍微減少一些比較的時間,當尋訪完陣列後都沒有發生任何的交換動作,表示排序已經完成,而無需再進行之後的迴圈比較與交換動作,例如:


排序前:95 27 90 49 80 58 6 9 18 50


27 90 49 80 58 6 9 18 50 [95] 95浮出 
27 49 80 58 6 9 18 50 [90 95] 90浮出 
27 49 58 6 9 18 50 [80 90 95] 80浮出 
27 49 6 9 18 50 [58 80 90 95] ...... 
27 6 9 18 49 [50 58 80 90 95] ...... 
6 9 18 27 [49 50 58 80 90 95] ...... 
6 9 18 [27 49 50 58 80 90 95] 由於接下來不會再發生交換動作,排序提早結束 


在上面的例子當中,還加入了一個觀念,就是當進行至i與i+1時沒有交換的動作,表示接下來的i+2至n已經排序完畢,這也增進了氣泡排序的效率。 


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void selsort(int[]);  // 選擇排序 
void insort(int[]);   // 插入排序 
void bubsort(int[]);  // 氣泡排序 


int main(void) {  
    int number[MAX] = {0}; 
    int i;  


    srand(time(NULL)); 


    printf("排序前:"); 
    for(i = 0; i < MAX; i++) { 
        number[i] = rand() % 100; 
        printf("%d ", number[i]); 
    } 


    printf("\n請選擇排序方式:\n"); 
    printf("(1)選擇排序\n(2)插入排序\n(3)氣泡排序\n:"); 
    scanf("%d", &i); 


    switch(i) { 
        case 1: 
            selsort(number); break; 
        case 2: 
            insort(number); break; 
        case 3: 
            bubsort(number); break; 
        default: 
            printf("選項錯誤(1..3)\n"); 
    } 


    return 0; 



void selsort(int number[]) { 
    int i, j, k, m; 


    for(i = 0; i < MAX-1; i++) { 
        m = i; 
        for(j = i+1; j < MAX; j++) 
            if(number[j] < number[m]) 
                m = j; 


        if( i != m) 
            SWAP(number[i], number[m]) 


        printf("第 %d 次排序:", i+1); 
        for(k = 0; k < MAX; k++) 
            printf("%d ", number[k]); 
        printf("\n"); 
    } 
 } 


 void insort(int number[]) { 
    int i, j, k, tmp; 


    for(j = 1; j < MAX; j++) { 
        tmp = number[j]; 
        i = j - 1; 
        while(tmp < number[i]) { 
            number[i+1] = number[i]; 
            i--; 
            if(i == -1) 
                break; 
        } 
        number[i+1] = tmp; 


        printf("第 %d 次排序:", j); 
        for(k = 0; k < MAX; k++) 
            printf("%d ", number[k]); 
        printf("\n"); 
    } 



void bubsort(int number[]) { 
    int i, j, k, flag = 1; 


    for(i = 0; i < MAX-1 && flag == 1; i++) { 
        flag = 0; 
        for(j = 0; j < MAX-i-1; j++) { 
            if(number[j+1] < number[j]) { 
                SWAP(number[j+1], number[j]); 
                flag = 1; 
            } 
        } 


        printf("第 %d 次排序:", i+1); 
        for(k = 0; k < MAX; k++) 
            printf("%d ", number[k]); 
        printf("\n"); 
    } 





34.Algorithm Gossip: Shell 排序法 - 改良的插入排序
 
說明
 插入排序法由未排序的後半部前端取出一個值,插入已排序前半部的適當位置,概念簡單但速度不快。


排序要加快的基本原則之一,是讓後一次的排序進行時,儘量利用前一次排序後的結果,以加快排序的速度,Shell排序法即是基於此一概念來改良插入排序法。
解法
 Shell排序法最初是D.L Shell於1959所提出,假設要排序的元素有n個,則每次進行插入排序時並不是所有的元素同時進行時,而是取一段間隔。


Shell首先將間隔設定爲n/2,然後跳躍進行插入排序,再來將間隔n/4,跳躍進行排序動作,再來間隔設定爲n/8、n/16,直到間隔爲1之後的最 後一次排序終止,由於上一次的排序動作都會將固定間隔內的元素排序好,所以當間隔越來越小時,某些元素位於正確位置的機率越高,因此最後幾次的排序動作將 可以大幅減低。


舉個例子來說,假設有一未排序的數字如右:89 12 65 97 61 81 27 2 61 98


數字的總數共有10個,所以第一次我們將間隔設定爲10 / 2 = 5,此時我們對間隔爲5的數字進行排序,如下所示:


畫線連結的部份表示 要一起進行排序的部份,再來將間隔設定爲5 / 2的商,也就是2,則第二次的插入排序對象如下所示:






再來間隔設定爲2 / 2 = 1,此時就是單純的插入排序了,由於大部份的元素都已大致排序過了,所以最後一次的插入排序幾乎沒作什麼排序動作了: 




將間隔設定爲n / 2是D.L Shell最初所提出,在教科書中使用這個間隔比較好說明,然而Shell排序法的關鍵在於間隔的選定,例如Sedgewick證明選用以下的間隔可以加 快Shell排序法的速度: 


其中4*(2j)2 + 3*(2j) + 1不可超過元素總數n值,使用上式找出j後代入4*(2j)2 + 3*(2j) + 1求得第一個間隔,然後將2j除以2代入求得第二個間隔,再來依此類推。 


後來還有人證明有其它的間隔選定法可以將Shell排序法的速度再加快;另外Shell排序法的概念也可以用來改良氣泡排序法。
實作
 

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void shellsort(int[]); 


int main(void) { 
    int number[MAX] = {0}; 
    int i;  


    srand(time(NULL)); 


    printf("排序前:"); 
    for(i = 0; i < MAX; i++) { 
        number[i] = rand() % 100; 
        printf("%d ", number[i]); 
    } 


    shellsort(number); 


    return 0; 



void shellsort(int number[]) { 
    int i, j, k, gap, t; 


    gap = MAX / 2; 


    while(gap > 0) { 
        for(k = 0; k < gap; k++) { 
            for(i = k+gap; i < MAX; i+=gap) { 
                for(j = i - gap; j >= k; j-=gap) { 
                    if(number[j] > number[j+gap]) { 
                        SWAP(number[j], number[j+gap]); 
                    } 
                    else 
                        break; 
                } 
            } 
        } 


        printf("\ngap = %d:", gap); 
        for(i = 0; i < MAX; i++) 
            printf("%d ", number[i]); 
        printf("\n"); 


        gap /= 2; 
    } 



















35.Algorithm Gossip: Shaker 排序法 - 改良的氣泡排序
 
說明
 請看看之前介紹過的氣泡排序法:
     for(i = 0; i < MAX-1 && flag == 1; i++) { 
        flag = 0; 
        for(j = 0; j < MAX-i-1; j++) { 
            if(number[j+1] < number[j]) { 
                SWAP(number[j+1], number[j]); 
                flag = 1; 
            } 
        } 
    } 
 


事實上這個氣泡排序法已經不是單純的氣泡排序了,它使用了旗標與右端左移兩個方法來改進排序的效能,而Shaker排序法使用到後面這個觀念進一步改良氣泡排序法。
解法
 在上面的氣泡排序法中,交換的動作並不會一直進行至陣列的最後一個,而是會進行至MAX-i-1,所以排序的過程中,陣列右方排序好的元素會一直增加,使得左邊排序的次數逐漸減少,如我們的例子所示:


排序前:95 27 90 49 80 58 6 9 18 50


27 90 49 80 58 6 9 18 50 [95] 95浮出 
27 49 80 58 6 9 18 50 [90 95] 90浮出 
27 49 58 6 9 18 50 [80 90 95] 80浮出 
27 49 6 9 18 50 [58 80 90 95] ...... 
27 6 9 18 49 [50 58 80 90 95] ...... 
6 9 18 27 [49 50 58 80 90 95] ...... 
6 9 18 [27 49 50 58 80 90 95] 


方括號括住的部份表示已排序完畢,Shaker排序使用了這個概念,如果讓左邊的元素也具有這樣的性質,讓左右兩邊的元素都能先排序完成,如此未排序的元素會集中在中間,由於左右兩邊同時排序,中間未排序的部份將會很快的減少。


方法就在於氣泡排序的雙向進行,先讓氣泡排序由左向右進行,再來讓氣泡排序由右往左進行,如此完成一次排序的動作,而您必須使用left與right兩個旗標來記錄左右兩端已排序的元素位置。


一個排序的例子如下所示:


排序前:45 19 77 81 13 28 18 19 77 11


往右排序:19 45 77 13 28 18 19 77 11 [81]
向左排序:[11] 19 45 77 13 28 18 19 77 [81]


往右排序:[11] 19 45 13 28 18 19 [77 77 81]
向左排序:[11 13] 19 45 18 28 19 [77 77 81]


往右排序:[11 13] 19 18 28 19 [45 77 77 81]
向左排序:[11 13 18] 19 19 28 [45 77 77 81]


往右排序:[11 13 18] 19 19 [28 45 77 77 81]
向左排序:[11 13 18 19 19] [28 45 77 77 81]


如上所示,括號中表示左右兩邊已排序完成的部份,當left > right時,則排序完成。 


實作

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 
void shakersort(int[]); 
int main(void) { 
    int number[MAX] = {0}; 
    int i;  


    srand(time(NULL)); 
36.排序法 - 改良的選擇排序
 
說明
 選擇排序法的概念簡單,每次從未排序部份選一最小值,插入已排序部份的後端,其時間主要花費於在整個未排序部份尋找最小值,如果能讓搜尋最小值的方式加 快,選擇排序法的速率也就可以加快,Heap排序法讓搜尋的路徑由樹根至最後一個樹葉,而不是整個未排序部份,因而稱之爲改良的選擇排序法。
解法
 Heap排序法使用Heap Tree(堆積樹),樹是一種資料結構,而堆積樹是一個二元樹,也就是每一個父節點最多隻有兩個子節點(關於樹的詳細定義還請見資料結構書籍),堆積樹的 父節點若小於子節點,則稱之爲最小堆積(Min Heap),父節點若大於子節點,則稱之爲最大堆積(Max Heap),而同一層的子節點則無需理會其大小關係,例如下面就是一個堆積樹: 


可以使用一維陣列來儲存堆積樹的所有元素與其順序,爲了計算方便,使用的起始索引是1而不是0,索引1是樹根位置,如果左子節點儲存在陣列中的索引爲s,則其父節點的索引爲s/2,而右子節點爲s+1,就如上圖所示,將上圖的堆積樹轉換爲一維陣列之後如下所示: 


首先必須知道如何建立堆積樹,加至堆積樹的元素會先放置在最後一個樹葉節點位置,然後檢查父節點是否小於子節點(最小堆積),將小的元素不斷與父節點交換,直到滿足堆積樹的條件爲止,例如在上圖的堆積加入一個元素12,則堆積樹的調整方式如下所示:




建立好堆積樹之後,樹根一定是所有元素的最小值,您的目的就是:
將最小值取出 
然後調整樹爲堆積樹 


不斷重複以上的步驟,就可以達到排序的效果,最小值的取出方式是將樹根與最後一個樹葉節點交換,然後切下樹葉節點,重新調整樹爲堆積樹,如下所示: 




調整完畢後,樹根節點又是最小值了,於是我們可以重覆這個步驟,再取出最小值,並調整樹爲堆積樹,如下所示:




如此重覆步驟之後,由於使用一維陣列來儲存堆積樹,每一次將樹葉與樹根交換的動作就是將最小值放至後端的陣列,所以最後陣列就是變爲已排序的狀態。 


其實堆積在調整的過程中,就是一個選擇的行爲,每次將最小值選至樹根,而選擇的路徑並不是所有的元素,而是由樹根至樹葉的路徑,因而可以加快選擇的過程, 所以Heap排序法纔會被稱之爲改良的選擇排序法。 


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void createheap(int[]); 
void heapsort(int[]); 


int main(void) { 
    int number[MAX+1] = {-1}; 
    int i, num;  


    srand(time(NULL)); 


    printf("排序前:"); 
    for(i = 1; i <= MAX; i++) { 
        number[i] = rand() % 100; 
        printf("%d ", number[i]); 
    } 


    printf("\n建立堆積樹:"); 
    createheap(number); 
    for(i = 1; i <= MAX; i++) 
        printf("%d ", number[i]); 
    printf("\n"); 


    heapsort(number); 


    printf("\n"); 


    return 0; 



void createheap(int number[]) { 
    int i, s, p; 
    int heap[MAX+1] = {-1}; 


    for(i = 1; i <= MAX; i++) { 
        heap[i] = number[i]; 
        s = i; 
        p = i / 2; 
        while(s >= 2 && heap[p] > heap[s]) { 
            SWAP(heap[p], heap[s]); 
            s = p; 
            p = s / 2; 
        } 
    } 


    for(i = 1; i <= MAX; i++) 
        number[i] = heap[i]; 
    



void heapsort(int number[]) { 
    int i, m, p, s; 


    m = MAX; 
    while(m > 1) { 
        SWAP(number[1], number[m]); 
        m--; 


        p = 1; 
        s = 2 * p; 


        while(s <= m) { 
            if(s < m && number[s+1] < number[s]) 
                s++; 
            if(number[p] <= number[s]) 
                break; 
            SWAP(number[p], number[s]); 
            p = s; 
            s = 2 * p; 
        } 


        printf("\n排序中:"); 
        for(i = MAX; i > 0; i--) 
            printf("%d ", number[i]); 
    } 





37.Algorithm Gossip: 快速排序法(一)
 
說明快速排序法(quick sort)是目前所公認最快的排序方法之一(視解題的對象而定),雖然快速排序法在最差狀況下可以達O(n2),但是在多數的情況下,快速排序法的效率表現是相當不錯的。
快速排序法的基本精神是在數列中找出適當的軸心,然後將數列一分爲二,分別對左邊與右邊數列進行排序,而影響快速排序法效率的正是軸心的選擇。
這邊所介紹的第一個快速排序法版本,是在多數的教科書上所提及的版本,因爲它最容易理解,也最符合軸心分割與左右進行排序的概念,適合對初學者進行講解。
解法這邊所介紹的快速演算如下:將最左邊的數設定爲軸,並記錄其值爲 s 
廻圈處理:
令索引 i 從數列左方往右方找,直到找到大於 s 的數 
令索引 j 從數列左右方往左方找,直到找到小於 s 的數 
如果 i >= j,則離開回圈 
如果 i < j,則交換索引i與j兩處的值 
將左側的軸與 j 進行交換 
對軸左邊進行遞迴 
對軸右邊進行遞迴 


透過以下演算法,則軸左邊的值都會小於s,軸右邊的值都會大於s,如此再對軸左右兩邊進行遞迴,就可以對完成排序的目的,例如下面的實例,*表示要交換的數,[]表示軸:
[41] 24 76* 11 45 64 21 69 19 36* 
[41] 24 36 11 45* 64 21 69 19* 76 
[41] 24 36 11 19 64* 21* 69 45 76 
[41] 24 36 11 19 21 64 69 45 76 
21 24 36 11 19 [41] 64 69 45 76 


在上面的例子中,41左邊的值都比它小,而右邊的值都比它大,如此左右再進行遞迴至排序完成。 


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void quicksort(int[], int, int); 


int main(void) { 
    int number[MAX] = {0}; 
    int i, num;  


    srand(time(NULL)); 


    printf("排序前:"); 
    for(i = 0; i < MAX; i++) { 
        number[i] = rand() % 100; 
        printf("%d ", number[i]); 
    } 


    quicksort(number, 0, MAX-1); 


    printf("\n排序後:"); 
    for(i = 0; i < MAX; i++) 
        printf("%d ", number[i]); 
    
    printf("\n"); 


    return 0; 



void quicksort(int number[], int left, int right) { 
    int i, j, s; 


    if(left < right) { 
        s = number[left]; 
        i = left; 
        j = right + 1; 


        while(1) { 
            // 向右找
            while(i + 1 < number.length && number[++i] < s) ;  
            // 向左找  
            while(j -1 > -1 && number[--j] > s) ;  
            if(i >= j) 
                break; 
            SWAP(number[i], number[j]); 
        } 


        number[left] = number[j]; 
        number[j] = s; 


        quicksort(number, left, j-1);   // 對左邊進行遞迴 
        quicksort(number, j+1, right);  // 對右邊進行遞迴 
    } 







38.Algorithm Gossip: 快速排序法(二)
說明在快速排序法(一)中,每次將最左邊的元素設爲軸,而之前曾經說過,快速排序法的加速在於軸的選擇,在這個例子中,只將軸設定爲中間的元素,依這個元素作基準進行比較,這可以增加快速排序法的效率。
解法在這個例子中,取中間的元素s作比較,同樣的先得右找比s大的索引 i,然後找比s小的索引 j,只要兩邊的索引還沒有交會,就交換 i 與 j 的元素值,這次不用再進行軸的交換了,因爲在尋找交換的過程中,軸位置的元素也會參與交換的動作,例如:
41 24 76 11 45 64 21 69 19 36


首先left爲0,right爲9,(left+right)/2 = 4(取整數的商),所以軸爲索引4的位置,比較的元素是45,您往右找比45大的,往左找比45小的進行交換:
41 24 76* 11 [45] 64 21 69 19 *36 
41 24 36 11 45* 64 21 69 19* 76 
41 24 36 11 19 64* 21* 69 45 76 
[41 24 36 11 19 21] [64 69 45 76] 


完成以上之後,再初別對左邊括號與右邊括號的部份進行遞迴,如此就可以完成排序的目的。 


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void quicksort(int[], int, int); 


int main(void) { 
    int number[MAX] = {0}; 
    int i, num;  
    srand(time(NULL)); 
    printf("排序前:"); 
    for(i = 0; i < MAX; i++) { 
        number[i] = rand() % 100; 
        printf("%d ", number[i]); 
    } 


    quicksort(number, 0, MAX-1); 
    printf("\n排序後:"); 
    for(i = 0; i < MAX; i++) 
        printf("%d ", number[i]); 
    
    printf("\n"); 
    return 0; 



void quicksort(int number[], int left, int right) { 
    int i, j, s; 
    if(left < right) { 
        s = number[(left+right)/2]; 
        i = left - 1; 
        j = right + 1; 


        while(1) { 
            while(number[++i] < s) ;  // 向右找 
            while(number[--j] > s) ;  // 向左找 
            if(i >= j) 
                break; 
            SWAP(number[i], number[j]); 
        } 


        quicksort(number, left, i-1);   // 對左邊進行遞迴 
        quicksort(number, j+1, right);  // 對右邊進行遞迴 
    } 

39.Algorithm Gossip: 快速排序法(三)
 
說明
 之前說過軸的選擇是快速排序法的效率關鍵之一,在這邊的快速排序法的軸選擇方式更加快了快速排序法的效率,它是來自演算法名書 Introduction to Algorithms 之中。
解法
 先說明這個快速排序法的概念,它以最右邊的值s作比較的標準,將整個數列分爲三個部份,一個是小於s的部份,一個是大於s的部份,一個是未處理的部份,如下所示 : 




在排序的過程中,i 與 j 都會不斷的往右進行比較與交換,最後數列會變爲以下的狀態: 




然後將s的值置於中間,接下來就以相同的步驟會左右兩邊的數列進行排序的動作,如下所示: 




整個演算的過程,直接摘錄書中的虛擬碼來作說明: 
QUICKSORT(A, p, r) 
    if p < r 
        then q <- PARTITION(A, p, r) 
                 QUICKSORT(A, p, q-1) 
                 QUICKSORT(A, q+1, r) 
end QUICKSORT 


PARTITION(A, p, r) 
    x <- A[r] 
    i <- p-1 
    for j <- p to r-1 
        do if A[j] <= x 
                 then  i <- i+1 
                         exchange A[i]<->A[j] 
    exchange A[i+1]<->A[r] 
    return i+1 
end PARTITION  


一個實際例子的演算如下所示:




#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


int partition(int[], int, int); 
void quicksort(int[], int, int); 


int main(void) { 
    int number[MAX] = {0}; 
    int i, num;  


    srand(time(NULL)); 


    printf("排序前:"); 
    for(i = 0; i < MAX; i++) { 
        number[i] = rand() % 100; 
        printf("%d ", number[i]); 
    } 


    quicksort(number, 0, MAX-1); 


    printf("\n排序後:"); 
    for(i = 0; i < MAX; i++) 
        printf("%d ", number[i]); 
    
    printf("\n"); 


    return 0; 



int partition(int number[], int left, int right) { 
    int i, j, s; 


    s = number[right]; 
    i = left - 1; 


    for(j = left; j < right; j++) { 
        if(number[j] <= s) { 
            i++; 
            SWAP(number[i], number[j]); 
        } 
    } 


    SWAP(number[i+1], number[right]); 
    return i+1; 



void quicksort(int number[], int left, int right) { 
    int q; 


    if(left < right) { 
        q = partition(number, left, right); 
        quicksort(number, left, q-1); 
        quicksort(number, q+1, right); 
    } 



40.Algorithm Gossip: 合併排序法
說明之前所介紹的排序法都是在同一個陣列中的排序,考慮今日有兩筆或兩筆以上的資料,它可能是不同陣列中的資料,或是不同檔案中的資料,如何爲它們進行排序?
解法可以使用合併排序法,合併排序法基本是將兩筆已排序的資料合併並進行排序,如果所讀入的資料尚未排序,可以先利用其它的排序方式來處理這兩筆資料,然後再將排序好的這兩筆資料合併。


有人問道,如果兩筆資料本身就無排序順序,何不將所有的資料讀入,再一次進行排序?排序的精神是儘量利用資料已排序的部份,來加快排序的效率,小筆資料的 排序較爲快速,如果小筆資料排序完成之後,再合併處理時,因爲兩筆資料都有排序了,所有在合併排序時會比單純讀入所有的資料再一次排序來的有效率。


那麼可不可以直接使用合併排序法本身來處理整個排序的動作?而不動用到其它的排序方式?答案是肯定的,只要將所有的數字不斷的分爲兩個等分,直到最後剩一個數字爲止,然後再反過來不斷的合併,就如下圖所示: 


不過基本上分割又會花去額外的時間,不如使用其它較好的排序法來排序小筆資料,再使用合併排序來的有效率。
下面這個程式範例,我們使用快速排序法來處理小筆資料排序,然後再使用合併排序法處理合並的動作。 
#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX1 10 
#define MAX2 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


int partition(int[], int, int); 
void quicksort(int[], int, int); 
void mergesort(int[], int, int[], int, int[]); 


int main(void) { 
    int number1[MAX1] = {0}; 
    int number2[MAX1] = {0}; 
    int number3[MAX1+MAX2] = {0}; 
    int i, num;  


    srand(time(NULL)); 
    printf("排序前:"); 
    printf("\nnumber1[]:"); 
    for(i = 0; i < MAX1; i++) { 
        number1[i] = rand() % 100; 
        printf("%d ", number1[i]); 
    } 


    printf("\nnumber2[]:"); 
    for(i = 0; i < MAX2; i++) { 
        number2[i] = rand() % 100; 
        printf("%d ", number2[i]); 
    } 


    // 先排序兩筆資料 
    quicksort(number1, 0, MAX1-1); 
    quicksort(number2, 0, MAX2-1); 
    printf("\n排序後:"); 
    printf("\nnumber1[]:"); 
    for(i = 0; i < MAX1; i++) 
        printf("%d ", number1[i]); 
    printf("\nnumber2[]:"); 
    for(i = 0; i < MAX2; i++) 
        printf("%d ", number2[i]); 


    // 合併排序 
    mergesort(number1, MAX1, number2, MAX2, number3); 
    printf("\n合併後:"); 
    for(i = 0; i < MAX1+MAX2; i++) 
        printf("%d ", number3[i]); 
    
    printf("\n"); 
    return 0; 



int partition(int number[], int left, int right) { 
    int i, j, s; 
    s = number[right]; 
    i = left - 1; 


    for(j = left; j < right; j++) { 
        if(number[j] <= s) { 
            i++; 
            SWAP(number[i], number[j]); 
        } 
    } 


    SWAP(number[i+1], number[right]); 
    return i+1; 



void quicksort(int number[], int left, int right) { 
    int q; 
    if(left < right) { 
        q = partition(number, left, right); 
        quicksort(number, left, q-1); 
        quicksort(number, q+1, right); 
    } 



void mergesort(int number1[], int M, int number2[], int N, int number3[]) { 
    int i = 0, j = 0, k = 0; 


    while(i < M && j < N) { 
        if(number1[i] <= number2[j]) 
            number3[k++] = number1[i++]; 
        else 
            number3[k++] = number2[j++]; 
    } 
    while(i < M) 
        number3[k++] = number1[i++]; 
    while(j < N) 
        number3[k++] = number2[j++]; 

41.Algorithm Gossip: 基數排序法
說明在之前所介紹過的排序方法,都是屬於「比較性」的排序法,也就是每次排序時 ,都是比較整個鍵值的大小以進行排序。
這邊所要介紹的「基數排序法」(radix sort)則是屬於「分配式排序」(distribution sort),基數排序法又稱「桶子法」(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些「桶」中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度爲O (nlog(r)m),其中r爲所採取的基數,而m爲堆數,在某些時候,基數排序法的效率高於其它的比較性排序法。
解法基數排序的方式可以採用LSD(Least sgnificant digital)或MSD(Most sgnificant digital),LSD的排序方式由鍵值的最右邊開始,而MSD則相反,由鍵值的最左邊開始。
以LSD爲例,假設原來有一串數值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根據個位數的數值,在走訪數值時將它們分配至編號0到9的桶子中:
0 1 2 3 4 5 6 7 8


81


65


39 




43 14 55

28






93










22 73









接下來將這些桶子中的數值重新串接起來,成爲以下的數列: 
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再進行一次分配,這次是根據十位數來分配:
0 1 2 3 4 5 6 7 8


28 39










14 22
43 55 65 73 81 93 


接下來將這些桶子中的數值重新串接起來,成爲以下的數列: 
14, 22, 28, 39, 43, 55, 65, 73, 81, 93 
這時候整個數列已經排序完畢;如果排序的對象有三位數以上,則持續進行以上的動作直至最高位數爲止。 
LSD的基數排序適用於位數小的數列,如果位數多的話,使用MSD的效率會比較好,MSD的方式恰與LSD相反,是由高位數爲基底開始進行分配,其他的演 算方式則都相同。 


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


int main(void) { 
    int data[10] = {73, 22, 93, 43, 55, 14, 28, 65, 39, 81}; 
    int temp[10][10] = {0}; 
    int order[10] = {0}; 
    int i, j, k, n, lsd; 
    k = 0; 
    n = 1; 
    printf("\n排序前: "); 
    for(i = 0; i < 10; i++) 
        printf("%d ", data[i]); 
    putchar('\n'); 
    while(n <= 10) { 
        for(i = 0; i < 10; i++) { 
            lsd = ((data[i] / n) % 10); 
            temp[lsd][order[lsd]] = data[i]; 
            order[lsd]++; 
        } 
        printf("\n重新排列: "); 
        for(i = 0; i < 10; i++) { 
            if(order[i] != 0) 
                for(j = 0; j < order[i]; j++) { 
                    data[k] = temp[i][j]; 
                    printf("%d ", data[k]); 
                    k++; 
                } 
            order[i] = 0; 
        } 
        n *= 10; 
        k = 0; 
    } 


    putchar('\n'); 
    printf("\n排序後: "); 
    for(i = 0; i < 10; i++) 
        printf("%d ", data[i]); 
    return 0; 

42.Algorithm Gossip: 循序搜尋法(使用衛兵)
 
說明
 搜尋的目的,是在「已排序的資料」中尋找指定的資料,而當中循序搜尋是最基本的搜尋法,只要從資料開頭尋找到最後,看看是否找到資料即可。
解法
 初學者看到循序搜尋,多數都會使用以下的方式來進行搜尋:
while(i < MAX) { 
    if(number[i] == k) { 
        printf("找到指定值"); 
        break; 
    } 
    i++; 

 


這個方法基本上沒有錯,但是可以加以改善,可以利用設定衛兵的方式,省去if判斷式,衛兵通常設定在數列最後或是最前方,假設設定在列前方好了(索引0的 位置),我們從數列後方向前找,如果找到指定的資料時,其索引值不是0,表示在數列走訪完之前就找到了,在程式的撰寫上,只要使用一個while迴圈就可 以了。


下面的程式爲了配合衛兵的設置,自行使用快速排序法先將產生的數列排序,然後才進行搜尋,若只是數字的話,通常您可以使用程式語言函式庫所提供的搜尋函式。 


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


int search(int[]); 
int partition(int[], int, int); 
void quicksort(int[], int, int); 


int main(void) { 
    int number[MAX+1] = {0}; 
    int i, find; 


    srand(time(NULL)); 


    for(i = 1; i <= MAX; i++) 
        number[i] = rand() % 100; 


    quicksort(number, 1, MAX); 


    printf("數列:"); 
    for(i = 1; i <= MAX; i++) 
        printf("%d ", number[i]); 
    
    printf("\n輸入搜尋值:"); 
    scanf("%d", &number[0]); 


    if(find = search(number)) 
        printf("\n找到數值於索引 %d ", find); 
    else 
        printf("\n找不到數值"); 


    printf("\n"); 


    return 0; 



int search(int number[]) { 
    int i, k; 
    
    k = number[0]; 
    i = MAX; 
    
    while(number[i] != k) 
        i--; 


    return i; 



int partition(int number[], int left, int right) { 
    int i, j, s; 


    s = number[right]; 
    i = left - 1; 


    for(j = left; j < right; j++) { 
        if(number[j] <= s) { 
            i++; 
            SWAP(number[i], number[j]); 
        } 
    } 


    SWAP(number[i+1], number[right]); 
    return i+1; 



void quicksort(int number[], int left, int right) { 
    int q; 


    if(left < right) { 
        q = partition(number, left, right); 
        quicksort(number, left, q-1); 
        quicksort(number, q+1, right); 
    } 







43.Algorithm Gossip: 二分搜尋法(搜尋原則的代表)
說明如果搜尋的數列已經有排序,應該儘量利用它們已排序的特性,以減少搜尋比對的次數,這是搜尋的基本原則,二分搜尋法是這個基本原則的代表。
解法在二分搜尋法中,從數列的中間開始搜尋,如果這個數小於我們所搜尋的數,由於數列已排序,則該數左邊的數一定都小於要搜尋的對象,所以無需浪費時間在左邊的數;如果搜尋的數大於所搜尋的對象,則右邊的數無需再搜尋,直接搜尋左邊的數。


所以在二分搜尋法中,將數列不斷的分爲兩個部份,每次從分割的部份中取中間數比對,例如要搜尋92於以下的數列,首先中間數索引爲(0+9)/2 = 4(索引由0開始):
[3 24 57 57 67 68 83 90 92 95]


由於67小於92,所以轉搜尋右邊的數列:
3 24 57 57 67 [68 83 90 92 95]
由於90小於92,再搜尋右邊的數列,這次就找到所要的數了:
3 24 57 57 67 68 83 90 [92 95] 


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void quicksort(int[], int, int); 
int bisearch(int[], int); 


int main(void) { 
    int number[MAX] = {0}; 
    int i, find; 
    srand(time(NULL)); 


    for(i = 0; i < MAX; i++) { 
        number[i] = rand() % 100; 
    } 


    quicksort(number, 0, MAX-1); 


    printf("數列:"); 
    for(i = 0; i < MAX; i++) 
        printf("%d ", number[i]); 


    printf("\n輸入尋找對象:"); 
    scanf("%d", &find); 


    if((i = bisearch(number, find)) >= 0) 
        printf("找到數字於索引 %d ", i); 
    else 
        printf("\n找不到指定數"); 
    
    printf("\n"); 


    return 0; 



int bisearch(int number[], int find) { 
    int low, mid, upper; 


    low = 0; 
    upper = MAX - 1; 


    while(low <= upper) { 
        mid = (low+upper) / 2; 
        if(number[mid] < find) 
            low = mid+1; 
        else if(number[mid] > find) 
            upper = mid - 1; 
        else 
            return mid; 
    } 


    return -1; 



void quicksort(int number[], int left, int right) { 
    int i, j, k, s; 


    if(left < right) { 
        s = number[(left+right)/2]; 
        i = left - 1; 
        j = right + 1; 


        while(1) { 
            while(number[++i] < s) ;  // 向右找 
            while(number[--j] > s) ;  // 向左找 
            if(i >= j) 
                break; 
            SWAP(number[i], number[j]); 
        } 


        quicksort(number, left, i-1);   // 對左邊進行遞迴 
        quicksort(number, j+1, right);  // 對右邊進行遞迴 
    } 

44.Algorithm Gossip: 插補搜尋法
 
說明
 如果卻搜尋的資料分佈平均的話,可以使用插補(Interpolation)搜尋法來進行搜尋,在搜尋的對象大於500時,插補搜尋法會比 二分搜尋法 來的快速。
解法
 插補搜尋法是以資料分佈的近似直線來作比例運算,以求出中間的索引並進行資料比對,如果取出的值小於要尋找的值,則提高下界,如果取出的值大於要尋找的 值,則降低下界,如此不斷的減少搜尋的範圍,所以其本原則與二分搜尋法是相同的,至於中間值的尋找是透過比例運算,如下所示,其中K是指定要尋找的對象, 而m則是可能的索引值: 




實作
 

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 10 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void quicksort(int[], int, int); 
int intsrch(int[], int); 


int main(void) { 
    int number[MAX] = {0}; 
    int i, find; 


    srand(time(NULL)); 


    for(i = 0; i < MAX; i++) { 
        number[i] = rand() % 100; 
    } 


    quicksort(number, 0, MAX-1); 


    printf("數列:"); 
    for(i = 0; i < MAX; i++) 
        printf("%d ", number[i]); 


    printf("\n輸入尋找對象:"); 
    scanf("%d", &find); 


    if((i = intsrch(number, find)) >= 0) 
        printf("找到數字於索引 %d ", i); 
    else 
        printf("\n找不到指定數"); 
    
    printf("\n"); 


    return 0; 



int intsrch(int number[], int find) { 
    int low, mid, upper; 


    low = 0; 
    upper = MAX - 1; 


    while(low <= upper) { 
        mid = (upper-low)* 
            (find-number[low])/(number[upper]-number[low]) 
            + low; 
        if(mid < low || mid > upper) 
            return -1; 


        if(find < number[mid]) 
            upper = mid - 1; 
        else if(find > number[mid]) 
            low = mid + 1; 
        else 
            return mid; 
     } 


     return -1;



void quicksort(int number[], int left, int right) { 
    int i, j, k, s; 


    if(left < right) { 
        s = number[(left+right)/2]; 
        i = left - 1; 
        j = right + 1; 


        while(1) { 
            while(number[++i] < s) ;  // 向右找 
            while(number[--j] > s) ;  // 向左找 
            if(i >= j) 
                break; 
            SWAP(number[i], number[j]); 
        } 


        quicksort(number, left, i-1);   // 對左邊進行遞迴 
        quicksort(number, j+1, right);  // 對右邊進行遞迴 
    } 









45.Algorithm Gossip: 費氏搜尋法
 
說明
 二分搜尋法每次搜尋時,都會將搜尋區間分爲一半,所以其搜尋時間爲O(log(2)n),log(2)表示以2爲底的log值,這邊要介紹的費氏搜尋,其利用費氏數列作爲間隔來搜尋下一個數,所以區間收斂的速度更快,搜尋時間爲O(logn)。
解法
 費氏搜尋使用費氏數列來決定下一個數的搜尋位置,所以必須先製作費氏數列,這在之前有提過;費氏搜尋會先透過公式計算求出第一個要搜尋數的位置,以及其代 表的費氏數,以搜尋對象10個數字來說,第一個費氏數經計算後一定是F5,而第一個要搜尋的位置有兩個可能,例如若在下面的數列搜尋的話(爲了計算方便, 通常會將索引0訂作無限小的數,而數列由索引1開始):


-infin; 1 3 5 7 9 13 15 17 19 20


如果要搜尋5的話,則由索引F5 = 5開始搜尋,接下來如果數列中的數小於指定搜尋值時,就往左找,大於時就向右,每次找的間隔是F4、F3、F2來尋找,當費氏數爲0時還沒找到,就表示尋找失敗,如下所示: 




由於第一個搜尋值索引F5 = 5處的值小於19,所以此時必須對齊數列右方,也就是將第一個搜尋值的索引改爲F5+2 = 7,然後如同上述的方式進行搜尋,如下所示:


至於第一個搜尋值是如何找到的?我們可以由以下這個公式來求得,其中n爲搜尋對象的個數:
Fx + m = n 
Fx <= n 
 


也就是說Fx必須找到不大於n的費氏數,以10個搜尋對象來說:
Fx + m = 10 
 


取Fx = 8, m = 2,所以我們可以對照費氏數列得x = 6,然而第一個數的可能位置之一並不是F6,而是第x-1的費氏數,也就是F5 = 5。


如果數列number在索引5處的值小於指定的搜尋值,則第一個搜尋位置就是索引5的位置,如果大於指定的搜尋值,則第一個搜尋位置必須加上m,也就是F5 + m = 5 + 2 = 7,也就是索引7的位置,其實加上m的原因,是爲了要讓下一個搜尋值剛好是數列的最後一個位置。


費氏搜尋看來難懂,但只要掌握Fx + m = n這個公式,自己找幾個實例算一次,很容易就可以理解;費氏搜尋除了收斂快速之外,由於其本身只會使用到加法與減法,在運算上也可以加快。 


#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#define MAX 15 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void createfib(void);     // 建立費氏數列 
int findx(int, int);          // 找x值 
int fibsearch(int[], int);  // 費氏搜尋 
void quicksort(int[], int, int);  // 快速排序 


int Fib[MAX] = {-999}; 


int main(void) { 
    int number[MAX] = {0}; 
    int i, find; 


    srand(time(NULL)); 


    for(i = 1; i <= MAX; i++) { 
        number[i] = rand() % 100; 
    } 


    quicksort(number, 1, MAX); 


    printf("數列:"); 
    for(i = 1; i <= MAX; i++) 
        printf("%d ", number[i]); 


    printf("\n輸入尋找對象:"); 
    scanf("%d", &find); 


    if((i = fibsearch(number, find)) >= 0) 
        printf("找到數字於索引 %d ", i); 
    else 
        printf("\n找不到指定數"); 
    
    printf("\n"); 


    return 0; 



// 建立費氏數列 
void createfib(void) { 
    int i; 


    Fib[0] = 0; 
    Fib[1] = 1; 


    for(i = 2; i < MAX; i++) 
        Fib[i] = Fib[i-1] + Fib[i-2]; 



// 找 x 值 
int findx(int n, int find) { 
    int i = 0; 


    while(Fib[i] <= n) 
        i++; 


    i--; 
    return i; 



// 費式搜尋 
int fibsearch(int number[], int find) { 
    int i, x, m; 


    createfib(); 


    x  = findx(MAX+1,find); 
    m = MAX - Fib[x]; 
    printf("\nx = %d, m = %d, Fib[x] = %d\n\n", 
                                     x, m, Fib[x]); 


    x--; 
    i = x; 


    if(number[i] < find) 
        i += m; 


    while(Fib[x] > 0) { 
        if(number[i] < find) 
            i += Fib[--x]; 
        else if(number[i] > find) 
            i -= Fib[--x]; 
        else 
            return i; 
    } 
    return -1; 



void quicksort(int number[], int left, int right) { 
    int i, j, k, s; 


    if(left < right) { 
        s = number[(left+right)/2]; 
        i = left - 1; 
        j = right + 1; 


        while(1) { 
            while(number[++i] < s) ;  // 向右找 
            while(number[--j] > s) ;  // 向左找 
            if(i >= j) 
                break; 
            SWAP(number[i], number[j]); 
        } 


        quicksort(number, left, i-1);   // 對左邊進行遞迴 
        quicksort(number, j+1, right);  // 對右邊進行遞迴 
    } 



46.Algorithm Gossip: 稀疏矩陣
 
說明
 如果在矩陣中,多數的元素並沒有資料,稱此矩陣爲稀疏矩陣(sparse matrix),由於矩陣在程式中常使用二維陣列表示,二維陣列的大小與使用的記憶體空間成正比,如果多數的元素沒有資料,則會造成記憶體空間的浪費,爲 此,必須設計稀疏矩陣的陣列儲存方式,利用較少的記憶體空間儲存完整的矩陣資訊。
解法
 在這邊所介紹的方法較爲簡單,陣列只儲存矩陣的行數、列數與有資料的索引位置及其值,在需要使用矩陣資料時,再透過程式運算加以還原,例如若矩陣資料如下 ,其中0表示矩陣中該位置沒有資料:
0 0 0 0 0 0 
0 3 0 0 0 0 
0 0 0 6 0 0 
0 0 9 0 0 0 
0 0 0 0 12 0


這個矩陣是5X6矩陣,非零元素有4個,您要使用的陣列第一列記錄其列數、行數與非零元素個數:
5 6 4 


陣列的第二列起,記錄其位置的列索引、行索引與儲存值:
1 1 3 
2 3 6 
3 2 9 
4 4 12 


所以原本要用30個元素儲存的矩陣資訊,現在只使用了15個元素來儲存,節省了不少記憶體的使用。 

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


int main(void) { 
    int num[5][3] = {{5, 6, 4}, 
                     {1, 1, 3}, 
                     {2, 3, 6}, 
                     {3, 2, 9}, 
                     {4, 4, 12}}; 
    int i, j, k = 1; 


    printf("sparse matrix:\n"); 
    for(i = 0; i < 5; i++) { 
        for(j = 0; j < 3; j++) { 
            printf("%4d", num[i][j]); 
        } 
        putchar('\n'); 
    } 


    printf("\nmatrix還原:\n"); 
    for(i = 0; i < num[0][0]; i++) { 
        for(j = 0; j < num[0][1]; j++) { 
            if(k < num[0][2] && 
               i == num[k][0] && j == num[k][1]) { 
                printf("%4d ", num[k][2]); 
                k++; 
            } 
            else 
                printf("%4d ", 0); 
        } 
        putchar('\n'); 
    } 


    return 0; 

















47.Algorithm Gossip: 多維矩陣轉一維矩陣
 
說明
 有的時候,爲了運算方便或資料儲存的空間問題,使用一維陣列會比二維或多維陣列來得方便,例如上三角矩陣、下三角矩陣或對角矩陣,使用一維陣列會比使用二維陣列來得節省空間。
解法
 以二維陣列轉一維陣列爲例,索引值由0開始,在由二維陣列轉一維陣列時,我們有兩種方式:「以列(Row)爲主」或「以行(Column)爲主」。由於 C/C++、Java等的記憶體配置方式都是以列爲主,所以您可能會比較熟悉前者(Fortran的記憶體配置方式是以行爲主)。


以列爲主的二維陣列要轉爲一維陣列時,是將二維陣列由上往下一列一列讀入一維陣列,此時索引的對應公式如下所示,其中row與column是二維陣列索引,loc表示對應的一維陣列索引:
loc = column + row*行數


以行爲主的二維陣列要轉爲一維陣列時,是將二維陣列由左往右一行一行讀入一維陣列,此時索引的對應公式如下所示:
loc = row + column*列數


公式的推導您畫圖看看就知道了,如果是三維陣列,則公式如下所示,其中i(個數u1)、j(個數u2)、k(個數u3)分別表示三維陣列的三個索引:
以列爲主:loc = i*u2*u3 + j*u3 + k
以行爲主:loc = k*u1*u2 + j*u1 + i


更高維度的可以自行依此類推,但通常更高維度的建議使用其它資料結構(例如物件包裝)會比較具體,也不易搞錯。


在C/C++中若使用到指標時,會遇到指標運算與記憶體空間位址的處理問題,此時也是用到這邊的公式,不過必須在每一個項上乘上資料型態的記憶體大小。 


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


int main(void) { 
    int arr1[3][4] = {{1, 2, 3, 4}, 
                     {5, 6, 7, 8}, 
                     {9, 10, 11, 12}}; 
    int arr2[12] = {0}; 
    int row, column, i; 


    printf("原二維資料:\n"); 
    for(row = 0; row < 3; row++) { 
        for(column = 0; column < 4; column++) { 
            printf("%4d", arr1[row][column]); 
        } 
        printf("\n"); 
    } 


    printf("\n以列爲主:"); 
    for(row = 0; row < 3; row++) { 
        for(column = 0; column < 4; column++) { 
            i = column + row * 4; 
            arr2[i] = arr1[row][column]; 
        } 
    } 
    for(i = 0; i < 12; i++) 
        printf("%d ", arr2[i]); 


    printf("\n以行爲主:"); 
    for(row = 0; row < 3; row++) { 
        for(column = 0; column < 4; column++) { 
            i = row + column * 3; 
            arr2[i] = arr1[row][column]; 
        } 
    } 
    for(i = 0; i < 12; i++) 
        printf("%d ", arr2[i]); 


    printf("\n"); 


    return 0; 













48.Algorithm Gossip: 上三角、下三角、對稱矩陣
說明
 上三角矩陣是矩陣在對角線以下的元素均爲0,即Aij = 0,i > j,例如:
1  2  3   4   5 
0  6  7   8   9 
0  0  10   11  12 
0  0  0   13  14 
0  0  0   0  15


下三角矩陣是矩陣在對角線以上的元素均爲0,即Aij = 0,i < j,例如:
 1  0  0  0  0 
 2  6  0  0  0 
 3  7  10 0  0 
 4  8  11 13 0 
 5  9  12 14 15 


對稱矩陣是矩陣元素對稱於對角線,例如:
 1  2  3  4  5 
 2  6  7  8  9 
 3  7  10 11 12 
 4  8  11 13 14 
 5  9  12 14 15 


上三角或下三角矩陣也有大部份的元素不儲存值(爲0),我們可以將它們使用一維陣列來儲存以節省儲存空間,而對稱矩陣因爲對稱於對角線,所以可以視爲上三角或下三角矩陣來儲存。
解法
 假設矩陣爲nxn,爲了計算方便,我們讓陣列索引由1開始,上三角矩陣化爲一維陣列,若以列爲主,其公式爲:loc = n*(i-1) - i*(i-1)/2 + j
化爲以行爲主,其公式爲:loc = j*(j-1)/2 + i


下三角矩陣化爲一維陣列,若以列爲主,其公式爲:loc = i*(i-1)/2 + j


若以行爲主,其公式爲:loc = n*(j-1) - j*(j-1)/2 + i
公式的導證其實是由等差級數公式得到,您可以自行繪圖並看看就可以導證出來,對於C/C++或Java等索引由0開始的語言來說,只要將i與j各加1,求得loc之後減1即可套用以上的公式。 


#include <stdio.h> 
#include <stdlib.h> 
#define N 5 


int main(void) { 
    int arr1[N][N] = { 
        {1, 2, 3,  4,   5}, 
        {0, 6, 7,  8,   9}, 
        {0, 0, 10, 11, 12}, 
        {0, 0, 0,  13, 14}, 
        {0, 0, 0,  0,  15}}; 


    int arr2[N*(1+N)/2] = {0}; 


    int i, j, loc = 0; 


    printf("原二維資料:\n"); 
    for(i = 0; i < N; i++) { 
        for(j = 0; j < N; j++) { 
            printf("%4d", arr1[i][j]); 
        } 
        printf("\n"); 
    } 


    printf("\n以列爲主:"); 
    for(i = 0; i < N; i++) { 
        for(j = 0; j < N; j++) { 
            if(arr1[i][j] != 0) 
                arr2[loc++] = arr1[i][j]; 
        } 
    } 
    for(i = 0; i < N*(1+N)/2; i++) 
        printf("%d ", arr2[i]); 


    printf("\n輸入索引(i, j):"); 
    scanf("%d, %d", &i, &j); 
    loc = N*i - i*(i+1)/2 + j; 
    printf("(%d, %d) = %d", i, j, arr2[loc]); 


    printf("\n"); 
    return 0; 

49.Algorithm Gossip: 奇數魔方陣
 
說明
 將1到n(爲奇數)的數字排列在nxn的方陣上,且各行、各列與各對角線的和必須相同,如下所示:


 解法
 
填魔術方陣的方法以奇數最爲簡單,第一個數字放在第一行第一列的正中央,然後向右(左)上填,如果右(左)上已有數字,則向下填,如下圖所示: 


一般程式語言的陣列索引多由0開始,爲了計算方便,我們利用索引1到n的部份,而在計算是向右(左)上或向下時,我們可以將索引值除以n值,如果得到餘數爲1就向下,否則就往右(左)上,原理很簡單,看看是不是已經在同一列上繞一圈就對了。 


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


#define N 5 


int main(void) { 
    int i, j, key; 
    int square[N+1][N+1] = {0}; 


    i = 0; 
    j = (N+1) / 2; 


    for(key = 1; key <= N*N; key++) { 
        if((key % N) == 1) 
            i++; 
        else { 
            i--; 
            j++; 
        } 


        if(i == 0) 
            i = N; 
        if(j > N) 
            j = 1; 


        square[i][j] = key; 
    } 


    for(i = 1; i <= N; i++) { 
        for(j = 1; j <= N; j++) 
            printf("%2d ", square[i][j]); 
    } 


    return 0; 









50.Algorithm Gossip: 4N 魔方陣
 
說明
 與 奇數魔術方陣 相同,在於求各行、各列與各對角線的和相等,而這次方陣的維度是4的倍數。
解法
 先來看看4X4方陣的解法: 




簡單的說,就是一個從左上由1依序開始填,但遇對角線不填,另一個由左上由16開始填,但只填在對角線,再將兩個合起來就是解答了;如果N大於2,則以 4X4爲單位畫對角線: 




至於對角線的位置該如何判斷,有兩個公式,有興趣的可以畫圖印證看看,如下所示:
左上至右下:j % 4 == i % 4
右上至左下:(j % 4 + i % 4) == 1 
#include <stdio.h> 
#include <stdlib.h> 
#define N 8 
int main(void) { 
    int i, j; 
    int square[N+1][N+1] = {0}; 


    for(j = 1; j <= N; j++) { 
        for(i = 1; i <= N; i++){ 
            if(j % 4 == i % 4 || (j % 4 + i % 4) == 1) 
                square[i][j] = (N+1-i) * N -j + 1; 
            else 
                square[i][j] = (i - 1) * N + j; 
        } 
    } 
    for(i = 1; i <= N; i++) { 
        for(j = 1; j <= N; j++) 
            printf("%2d ", square[i][j]); 
        printf("\n"); 
    } 
    return 0; 



51.Algorithm Gossip: 2(2N+1) 魔方陣
說明方陣的維度整體來看是偶數,但是其實是一個奇數乘以一個偶數,例如6X6,其中6=2X3,我們也稱這種方陣與單偶數方陣。
解法如果您會解奇數魔術方陣,要解這種方陣也就不難理解,首先我們令n=2(2m+1),並將整個方陣看作是數個奇數方陣的組合,如下所示: 


首先依序將A、B、C、D四個位置,依奇數方陣的規則填入數字,填完之後,方陣中各行的和就相同了,但列與對角線則否,此時必須在A-D與C- B之間,作一些對應的調換,規則如下:
將A中每一列(中間列除外)的頭m個元素,與D中對應位置的元素調換。 
將A的中央列、中央那一格向左取m格,並與D中對應位置對調 
將C中每一列的倒數m-1個元素,與B中對應的元素對調 
舉個實例來說,如何填6X6方陣,我們首先將之分解爲奇數方陣,並填入數字,如下所示: 




接下來進行互換的動作,互換的元素以不同顏色標示,如下:


由於m-1的數爲0,所以在這個例子中,C-B部份並不用進行對調。


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


#define N 6 
#define SWAP(x,y) {int t; t = x; x = y; y = t;} 


void magic_o(int [][N], int); 
void exchange(int [][N], int); 


int main(void) { 
    int square[N][N] = {0}; 
    int i, j; 


    magic_o(square, N/2); 
    exchange(square, N); 


    for(i = 0; i < N; i++) { 
        for(j = 0; j < N; j++) 
            printf("%2d ", square[i][j]); 
        printf("\n"); 
    } 


    return 0; 



void magic_o(int square[][N], int n) { 
    int count, row, column; 


    row = 0; 
    column = n / 2; 


    for(count = 1; count <= n*n; count++) { 
        square[row][column] = count;            // 填A 
        square[row+n][column+n] = count + n*n;  // 填B 
        square[row][column+n] = count + 2*n*n;  // 填C 
        square[row+n][column] = count + 3*n*n;  // 填D 
        if(count % n == 0) 
            row++; 
        else { 
            row = (row == 0) ? n - 1 : row - 1 ; 
            column = (column == n-1) ? 0 : column + 1; 
        } 
    } 



void exchange(int x[][N], int n) { 
    int i, j; 
    int m = n / 4; 
    int m1 = m - 1; 


    for(i = 0; i < n/2; i++) { 
        if(i != m)  {    
            for(j = 0; j < m; j++)          // 處理規則 1 
                SWAP(x[i][j], x[n/2+i][j]); 
            for(j = 0; j < m1; j++)         // 處理規則 2 
                SWAP(x[i][n-1-j], x[n/2+i][n-1-j]); 
        } 
        else {  // 處理規則 3 
            for(j = 1; j <= m; j++) 
                SWAP(x[m][j], x[n/2+m][j]); 
            for(j = 0; j < m1; j++) 
                SWAP(x[m][n-1-j], x[n/2+m][n-1-j]); 
        } 
    } 




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