Java经典算法大全

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

include

include

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;
}

  1. 巴斯卡三角形

include

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

include

include

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

include

include

include

include

include

include

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

include

include

include

include

include

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

include

include

include

include

include

include

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

include

include

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

include

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

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

include

include

include

include

include

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

include

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

include

include

include

include

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

include

include

include

include

include

include

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

include

include

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

include

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

include

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

include

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

include

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

include

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

include

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

include

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

include

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

include

include

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

include

include

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

include

include

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

include

include

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

include

include

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

include

include

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

include

include

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

include

include

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

include

include

include

include

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

include

include

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

include

include

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

include

include

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

include

include

include

include

include

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

include

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

include

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

include

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]);
}
}
}
  

  • 2
    点赞
  • 12
    收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 2
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值