1 .河内之塔.. 2 .Algorithm Gossip: 费式数列. 3 . 巴斯卡三角形 4 .Algorithm Gossip: 三色棋 5 .Algorithm Gossip: 老鼠走迷官(一) 6 .Algorithm Gossip: 老鼠走迷官(二) 7 .Algorithm Gossip: 骑士走棋盘 8 .Algorithm Gossip: 八皇后 9 .Algorithm Gossip: 八枚银币. 10 .Algorithm Gossip: 生命游戏. 11 .Algorithm Gossip: 字串核对 12 .Algorithm Gossip: 双色、三色河内塔 13 .Algorithm Gossip: 背包问题(Knapsack Problem 14 .Algorithm Gossip: 蒙地卡罗法求 PI 15 .Algorithm Gossip: Eratosthenes 筛选求质数 16 .Algorithm Gossip: 超长整数运算(大数运算). 17 .Algorithm Gossip: 长 PI. 18 .Algorithm Gossip: 最大公因数、最小公倍数、因式分解 19 .Algorithm Gossip: 完美数... 20 .Algorithm Gossip: 阿姆斯壮数. 21 .Algorithm Gossip: 最大访客数.... 22 .Algorithm Gossip: 中序式转后序式(前序式)... 23 .Algorithm Gossip: 后序式的运算. 24 .Algorithm Gossip: 洗扑克牌(乱数排列) 25 .Algorithm Gossip: Craps 赌博游戏. 26 .Algorithm Gossip: 约瑟夫问题(Josephus Problem) 27 .Algorithm Gossip: 排列组合. 28 .Algorithm Gossip: 格雷码(Gray Code 29 .Algorithm Gossip: 产生可能的集合 30 .Algorithm Gossip: m 元素集合的 n 个元素子集 31 .Algorithm Gossip: 数字拆解 32 .Algorithm Gossip: 得分排行 33 .Algorithm Gossip: 选择、插入、气泡排序 34 .Algorithm Gossip: Shell 排序法 - 改良的插入排序 35 .Algorithm Gossip: Shaker 排序法 - 改良的气泡排序 36 .排序法 - 改良的选择 37 .Algorithm Gossip:速排序法(一) 38 .Algorithm Gossip: 快速排序法(二) 39 .Algorithm Gossip: 快速排序法(三) 40 .Algorithm Gossip: 合并排序法 41 .Algorithm Gossip: 基数排序法. 42 .Algorithm Gossip: 循序搜寻法(使用卫兵) 43 .Algorithm Gossip: 二分搜寻法(搜寻原则的代表) 44 .Algorithm Gossip: 插补搜寻法. 45 .Algorithm Gossip: 费氏搜寻法. 46 .Algorithm Gossip: 稀疏矩阵 47 .Algorithm Gossip: 多维矩阵转一维矩阵 48 .Algorithm Gossip: 上三角、下三角、对称矩阵 49 .Algorithm Gossip: 奇数魔方阵 50 .Algorithm Gossip: 4N 魔方阵. 51 .Algorithm Gossip: 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 时,则 64 如果对这数字没什幺概念,就假设每秒钟搬一个盘子好了,也要约 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 - 1 = 18446744073709551615 为 5 .05390248594782e+ 16 年,也就是约 5000 世纪 2 .Algorithm Gossip: 费式数列 说明 Fibonacci为 1200 年代的欧洲数学家,在他的着作中曾经提到:「若有一只免子每个月生一只小免 子,一个月后小免子也开始生产。起初只有一只免子,一个月后就有两只免子,二个月后有三 只免子,三个月后有五只免子(小免子投入生产)......。 如果不太理解这个例子的话,举个图就知道了,注意新生的小免子需一个月成长期才会投入生 产,类似的道理也可以用于植物的生长,这就是Fibonacci数列,一般习惯称之为费氏数列,例 如以下: 1 、 1 、 2 、 3 、 5 、 8 、 13 、 21 、 34 、 55 、 89 ...... 解法 依说明,我们可以将费氏数列定义为以下: fn = fn- 1 + fn- 2 fn = n if n > 1 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++) } else { printf(" "); 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 1 2 3 李子 苹果 橘子 草莓 4KG 5KG 2KG 1KG NT$ 4500 NT$ 5700 NT$ 2250 NT$ 1100 4 甜瓜 6KG NT$ 6700 解法 背包问题是关于最佳化的问题,要解最佳化问题可以使用「动态规划」(Dynamic programming),从空集合开始,每增加一个元素就先求出该阶段的最佳解,直到所有的元素加 入至集合中,最后得到的就是最佳解。 以背包问题为例,我们使用两个阵列value与item,value表示目前的最佳解所得之总价,item表 示最后一个放至背包的水果,假设有负重量 1 ~ 8 的背包 8 个,并对每个背包求其最佳解。 逐步将水果放入背包中,并求该阶段的最佳解: 放入李子 背 1 2 3 4 5 6 7 8 包 负 重 valu 0 0 0 450 450 450 450 900 e 0 0 0 0 0 item - - - 0 0 0 0 0 放入苹果 背 1 2 3 4 5 6 7 8 包 负 重 valu 0 0 0 450 570 570 570 900 e 0 0 0 0 0 item - - - 0 1 1 1 0 放入橘子 背 1 2 3 4 5 6 7 8 包 负 重 valu e item 0 - 225 0 2 225 0 2 450 0 0 570 0 1 675 0 2 795 0 2 900 0 0 放入草莓 背 1 2 3 4 5 6 7 8 包 负 重 valu e item 110 0 3 225 0 2 335 0 3 450 0 0 570 0 1 680 0 3 795 0 2 905 0 3 放入甜瓜 背 1 2 3 4 5 6 7 8 包 负 重 valu e item 110 0 3 225 0 2 335 0 3 450 0 0 570 0 1 680 0 3 795 0 2 905 0 3 由最后一个表格,可以得知在背包负重 8 公斤时,最多可以装入 9050 元的水果,而最后一个装入 的 水果是 3 号,也就是草莓,装入了草莓,背包只能再放入 7 公斤( 8 - 1 )的水果,所以必须看 背包负重 7 公斤时的最佳解,最后一个放入的是 2 号,也就 是橘子,现在背包剩下负重量 5 公斤 ( 7 - 2 ),所以看负重 5 公斤的最佳解,最后放入的是 1 号,也就是苹果,此时背包负重量剩下 0 公 斤( 5 - 5 ), 无 法 再放入水果,所以求出最佳解为放入草莓、橘子与苹果,而总价为 9050 元。 实作 C #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 的检查动作可以减少。 实作 C #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的圆周率公式: 3 5 7 3 5 7 可以将这个公式整理为: 3 3 5 5 也就是说第n项,若为奇数则为正数,为偶数则为负数,而项数表示方式为: [ 16 / 5 2 *n- 1 2 *n- 1 2 *n- 1 2 *n- 1 2 *n- 1 2 *n- 1 来的 大,具有决定性,所以表示至少必须计算至第n项: [ 16 / 5 2 *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项: 2 2 这个公式的演算法配合大数运算函式的演算法为: 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 等式后面可以化为: 0 1 2 0 1 所以只要求出因式分解,就可以利用回圈求得等式后面的值,将该值除以 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: 阿姆斯壮数 说明 3 3 3 程式找出所有的三位数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 ( a + b ) * ( c + d ) - STACK ( ( (+ (+ - * *( *( *(+ *(+ * - OUTPUT - a a ab ab+ ab+ ab+ ab+c ab+c ab+cd ab+cd+ ab+cd+* 如果要将中序式转为前序式,则在读取中序式时是由后往前读取,而左右括号的处理方式相反, 其余不变,但输出之前必须先置入堆叠,待转换完成后再将堆叠中的 值由上往下读出,如此就 是前序表示式。 实作 C #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 + 3 4 + * #include <stdio.h> #include <stdlib.h> void evalPf( char *); double cal( double , char , double ); int main( void ) { char input[ 80 ]; 堆叠 1 12 3 // 1+2 后存回 33 334 3 7 // 3+4 后存回 21 // 3 * 7 后存回 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 的乱数,将产生的乱数当作索引取出阵列值,并与目前阵列走访到的值相交换 , 如此就不用担心乱数重复的问题了,阵列走访完毕后,所有的数字也就重新排列了。 至于如何判断花色?这只是除法的问题而已,取商数判断花色,取余数判断数字,您可以直接 看程式比较清楚。 实作 C #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 010 011 100 101 110 111 {} { 3 } { 2 } { 2 , 3 } { 1 } { 1 , 3 } { 1 , 2 } { 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维持不变。 实作 C #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 实作 C #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)这三个 排序方式是初学排序所必须知道的三个基本排序方式,它们由于速度不快而不实用(平均与最 2 解法 选择排序 将要排序的对象分作两部份,一个是已排序的,一个是未排序的,从后端未排序部份选择一个 最小值,并放入前端已排序部份的最后一个,例如: 排序前: 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排序法的速度: j 2 j j 2 j j 后来还有人证明有其它的间隔选定法可以将Shell排序法的速度再加快;另外Shell排序法的概念 也可以用来改良气泡排序法。 实作 C #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时,则排序完成。 实作 C #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)是目前所公认最快的排序方法之一(视解题的对象而定),虽然 2 错的。 快速排序法的基本精神是在数列中找出适当的轴心,然后将数列一分为二,分别对左边与右边 数列进行排序,而影响快速排序法效率的正是轴心的选择。 这边所介绍的第一个快速排序法版本,是在多数的教科书上所提及的版本,因为它最容易理解, 也最符合轴心分割与左右进行排序的概念,适合对初学者进行讲解。 解法这边所介绍的快速演算如下:将最左边的数设定为轴,并记录其值为 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 41 24 36 11 41 24 36 11 [ 41 24 36 11 [ 45 ] 64 21 69 19 * 36 45 * 64 21 69 19 * 76 19 64 * 21 * 69 45 76 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 9 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 9 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则是可能的索引值: 实作 C #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 个元素来储存,节省了不少记忆体的 使用。 C #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 0 0 2 6 0 3 7 10 4 8 5 9 11 12 0 0 0 13 14 0 0 0 0 15 下三角矩阵是矩阵在对角线以上的元素均为 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 对称矩阵是矩阵元素对称于对角线,例如: 1 2 3 2 6 7 4 5 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 , { 0 , 6 , 7 , 8 , 5 }, 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++) SWAP(x[i][j], x[n/ 2 +i][j]); for (j = 0 ; j < m1; j++) // 处理规则 1 // 处理规则 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]); } } } |
发表评论 取消回复