biancheng-算法教程

目录http://c.biancheng.net/algorithm/

1算法是什么
2时间复杂度和空间复杂度
3递归算法
4斐波那契数列
5分治算法
6找数组的最大值和最小值
7汉诺塔问题
8贪心算法
9部分背包问题
10动态规划算法
1101背包问题
12回溯算法
13迷宫问题
14N皇后问题
15冒泡排序算法
16插入排序算法
17选择排序算法
18希尔排序算法
19归并排序算法
20快速排序算法
21计数排序算法
22基数排序算法
23桶排序算法
24稳定排序算法
25顺序查找算法
26二分查找算法
27插值查找算法
28哈希查找算法
29最小生成树
30普里姆算法
31克鲁斯卡尔算法
32最短路径算法
33迪杰斯特拉算法
34弗洛伊德算法

 

时间复杂度和空间复杂度

算法本身是不分“好坏”的,所谓“最好”的算法,指的是最适合当前场景的算法。挑选算法时,主要考虑以下两方面因素:

  • 执行效率:根据算法所编写的程序,执行时间越短,执行效率就越高;
  • 占用的内存空间:不同算法编写出的程序,运行时占用的内存空间也不相同。如果实际场景中仅能使用少量的内存空间,就应该优先选择占用空间最少的算法。

如果一个算法的执行时间最终估算为O(n),那么该算法的时间复杂度就是O(n)。如下列举了常用的几种时间复杂度以及它们之间的大小关系:

O(1)< O(logn) < O(n) < O(n2) < O(n3) < O(2n)

与时间复杂度的表示方法一样,空间复杂度也采用大 O 记法表示。算法空间复杂度的估算方法是:

  • 如果算法中额外申请的内存空间不受用户输入值的影响(是一个固定值),那么该算法的空间复杂度用 O(1) 表示;
  • 如果随着输入值 n 的增大,算法申请的存储空间成线性增长,则程序的空间复杂度用 O(n) 表示;
  • 如果随着输入值 n 的增大,程序申请的存储空间成 n2 关系增长,则程序的空间复杂度用 O(n2) 表示;
  • 如果随着输入值 n 的增大,程序申请的存储空间成 n3 关系增长,则程序的空间复杂度用 O(n3) 表示;

递归算法

编程语言中,我们习惯将函数(方法)调用自身的过程称为递归,调用自身的函数称为递归函数,用递归方式解决问题的算法称为递归算法。

函数(方法)调用自身的实现方式有 2 种,分别是:
1) 直接调用自身,例如:

  1. int funciton(/*...*/) {
  2. //......
  3. //调用自身
  4. function(/*...*/);
  5. //......
  6. }

2) 间接调用自身,例如:

  1. int funciton1(/*...*/) {
  2. //......
  3. //调用另一个函数
  4. function2(/*...*/);
  5. //......
  6. }
  7. int function2(/*...*/) {
  8. //......
  9. //调用function1()函数
  10. funciton1(/*...*/);
  11. //......
  12. }

程序中,function1() 函数内部调用了 function2() 函数,而 function2() 函数内部又调用了 function1() 函数。也就是说,function1() 函数间接调用了自身。

设计递归函数时,我们必须为它设置一个结束递归的“出口”,否则函数会一直调用自身(死循环),直至运行崩溃。接下来我们以“用递归方式求 n! ”为例,给大家展示一个正确的递归函数。

n! 指的是求 1*2*3*...*n 的值,如下 C 语言程序中的 factorial() 就是实现求 n! 的递归函数:

  1. #include <stdio.h>
  2. int factorial(int n) {
  3. //递归的出口
  4. if (n == 1 || n == 0) {
  5. return 1;
  6. }
  7. //函数调用自身
  8. return n * factorial(n - 1);
  9. }
  10. int main()
  11. {
  12. int n;
  13. scanf("%d", &n);
  14. printf("%d! = %d", n,factorial(n));
  15. return 0;
  16. }

除非变量 n 的值为 1 或者 0,否则 factorial() 函数会一直调用自身。

递归的底层实现机制

为了方便讲解,当一个函数直接或间接调用自身时,我们将这个函数称作调用者,将直接或间接调用的自身称作被调用者。

递归函数执行时,调用者会将执行代码的权力移交给被调用者,同时还可能会向被调用者传输一些数据。此后,调用者将暂停执行,直至被调用者执行完成并将执行代码的权力交换给调用者后,它才能继续执行。

例如在求 n! 的递归函数中,factorial(n) 是调用者,函数内部的 factorial(n-1) 是被调用者。当 n 的值不为 1 和 0 时,调用者会将执行代码的权值移交给被调用者,同时会将 n-1 的值传递给被调用者。此后,factorial(n) 会暂停执行,直至 factorial(n-1) 执行完毕后,factorial(n) 才能继续执行。

为了确保调用者能够从暂停状态继续执行,当发生递归调用时,多数编程语言都使用栈结构保存调用者的状态信息,包括暂停时局部变量的值、寄存器中保存的数据等等。

图 1 展示了函数递归调用的底层实现过程:

 

 

如图 1 所示,F(n) 调用了 F(n-1),因此 F(n) 会暂停执行,将执行代码的权力移交给 F(n-1),F(n) 的暂停状态也会入栈保存;同样的道理,F(n-1) 中调用了 F(n-2),所以 F(n-1) 暂停执行,暂停状态入栈保存,执行代码的权力移交给 F(n-2)......直至递归停止,执行代码的权力会移交给栈中最顶部的函数,该函数的暂停状态信息会从栈中取出并恢复,该函数继续执行,执行完成后再将执行权力移交给栈顶的函数。直至 F(n) 出栈并执行完成,整个递归过程才算完成。

斐波那契数列(递归+源码+注释)

递归生成斐波那契数列

如下是一个伪代码形式的递归函数(方法),它可以输出斐波那契数列中指定位置处的数字:

  1. fibonacci(n): // n 表示求数列中第 n 个位置上的数的值
  2. if n == 1: // 设置结束递归的限制条件
  3. return 1
  4. if n == 2: // 设置结束递归的限制条件
  5. return 1
  6. return fibonacci(n-1) + fibonacci(n-2) // F(n) = F(n-1) + F(n-2)


如果我们想输出长度为 L 的斐波那契数列,需要调用 L 次 fibonacci() 函数。如下是输出斐波那契数列的 C 语言程序:

  1. #include <stdio.h>
  2. // index 表示求数列中第 index 个位置上的数的值
  3. int fibonacci(int index) {
  4. // 设置结束递归的限制条件
  5. if (index == 1 || index == 2) {
  6. return 1;
  7. }
  8. // F(index) = F(index-1) + F(index-2)
  9. return fibonacci(index - 1) + fibonacci(index - 2);
  10. }
  11. int main()
  12. {
  13. int i;
  14. // 输出前 10 个数
  15. for (i = 1; i <= 10; i++) {
  16. printf("%d ", fibonacci(i));
  17. }
  18. return 0;
  19. }


如下是输出斐波那契数列的 Java 程序:

  1. public class Demo {
  2. // index 表示求数列中第 index 个位置上的数的值
  3. public static int fibonacci(int index) {
  4. // 设置结束递归的限制条件
  5. if (index == 1 || index == 2) {
  6. return 1;
  7. }
  8. // F(index) = F(index-1) + F(index-2)
  9. return fibonacci(index - 1) + fibonacci(index - 2);
  10. }
  11. public static void main(String[] args) {
  12. // 输出前 10 个数
  13. for (int i = 1; i <= 10; i++) {
  14. System.out.print(fibonacci(i) + " ");
  15. }
  16. }
  17. }


如下是输出斐波那契数列的 Python 程序:

  1. # index 表示求数列中第 index 个位置上的数的值
  2. def fibonacci(index) :
  3. # 设置结束递归的限制条件
  4. if index == 1 or index == 2:
  5. return 1
  6. # F(index) = F(index - 1) + F(index - 2)
  7. return fibonacci(index - 1) + fibonacci(index - 2)
  8. # 输出前 10 个数
  9. for i in range(1, 10) :
  10. print(fibonacci(i), end = " ")


以上程序的执行结果都是:

1 1 2 3 5 8 13 21 34 55

分治算法


举一个简单的例子,设计一个排序算法实现对 1000 个整数进行排序。对于很多刚刚接触算法的初学者来说,直接实现对 1000 个整数进行排序是非常困难的。而同样的问题,如果转换成对 2 个整数进行排序,解决起来就很容易。

分治算法中,“分治”即“分而治之”的意思。分治算法解决问题的思路是:先将整个问题拆分成多个相互独立且数据量更少的小问题,通过逐一解决这些简单的小问题,最终找到解决整个问题的方案。

所谓问题间相互独立,简单理解就是每个问题都可以单独处理,不存在“谁先处理,谁后处理”的次序问题。

分治算法解决问题的过程如图 1 所示:

 

 

如图 1 所示,分治算法解决问题的过程需要经历 3 个阶段,分别是:

  1. 分:将整个问题划分成多个相对独立、涉及数据量更少的小问题,有些小问题还可以划分成很多更小的问题,直至每个问题都不可再分;
  2. 治:逐个解决所有的小问题;
  3. 合并:将所有小问题的解决方案合并到一起,找到解决整个问题的方案。

分治算法的利弊

使用分治算法解决的问题都具备这样的特征,当需要处理的数据量很少时,问题很容易就能解决,随着数据量增多,问题的解决难度也随之增大。分治算法通过将问题“分而治之”,每个小问题只需要处理少量的数据,每个小问题都很容易解决,最终就可以解决整个问题。

分治算法中,“分而治之”的小问题之间是相互独立的,处理次序不分先后,因此可以采用“并行”的方式让计算机同时处理多个小问题,提高问题的解决效率。

分治算法的弊端也很明显,该算法经常和递归算法搭配使用,整个解决问题的过程会耗费较多的时间和内存空间,严重时还可能导致程序运行崩溃。

分治算法的应用场景

分治算法解决的经典问题有很多,包括汉诺塔问题、寻找数列中最大值和最小值的问题等等。


分治算法还和其它算法搭配使用,比如二分查找算法、归并排序算法、快速排序算法等,后续章节会给大家一一进行讲解。

找数组的最大值和最小值

查找数组(序列)中最大值或最小值的算法有很多,接下来我们以 {3,7,2,1} 序列为例讲解两种查找最值的算法,一种是普通算法,另一种是借助分治算法解决。

普通算法

普通算法的解决思路是:创建两个变量 max 和 min 分别记录数组中的最大值和最小值,它们的初始值都是数组中的第一个数字。从第 2 个数字开始遍历数组,每遇到一个比 max 大的数字,就将它存储到 max 变量中;每遇到一个比 min 小的数字,就将它存储到 min 变量中。直到遍历完整个数组,max 记录的就是数组中的最大值,min 记录的就是数组中的最小值。

下面的动画,演示了找最大值的过程:


图 1 数组中找最大值的过程

找最小值的过程和图 1 类似,这里不再给出具体的动画演示。

如下是普通算法对应的伪代码:

输入 num[1...n]              // 输入 n 个数字
max <- num[1]                // 将第 1 个数字赋值给 max(表示最大值)
min <- num[1]                // 将第 1 个数字赋值给 min(表示最小值)
for i <- 2 to n:             // 从第 2 个数字开始遍历
    if num[i] > max:         // 如果 max 小于遍历到的数字,则更新 max 的值
        max <- num[i]
    if num[i] < min:         // 如果 min 小于遍历到的数字,则更新 min 的值
        min <- num[i]
Print max , min              // 输出 max 和 min 的值

实现过程非常简单,感兴趣的读者可以自行编写对应的 C、Java 或者 Python 代码。

分治算法

下图展示了用分治算法查找 {3, 7, 2, 1} 中最大值的实现过程:


图 2 分治算法找最大值


分治算法的实现思路是:不断地等分数组中的元素,直至各个分组中元素的个数 ≤2。由于每个分组内的元素最多有 2 个,很容易就可以找出其中的最值(最大值或最小值),然后这些最值再进行两两比较,最终找到的最值就是整个数组中的最值。

如图 2 所示,借助“分而治之”的思想,我们将“找 {3, 7, 2, 1} 中最值”的问题转换成了:先找出 {3 , 7]、[2 , 1} 中各自的最值,找出的最值再进行两两比较,最终就可以找到整个数组中的最值。

如下是分治算法求数组中最大值的伪代码:

输入 arr[1...n]           // 输入 n 个数字
arr_max(x , y) :          // 设计一个递归函数,[x , y] 用来限定查找最大数的范围
    if y-x ≤ 1 :         // 如果 y-x 的值小于等于 1,则比较 arr[x] 和 arr[y] 的值,大的就是最大值 
        return max(arr[x] , arr[y])
    else :
        // 将 [x , y] 区域划分为 [x , ⌊(x+y)/2⌋ ] 和 [ ⌊(x+y)/2+1⌋ , y] 两个区域,求出两个区域内各自的最大值
        max1 = arr_max(x , ⌊(x+y)/2⌋ )    
        max2 = arr_max( ⌊(x+y)/2+1⌋ , y)
    return max(max1 , max2)   // 比较两个区域的最大值,最终找出 [x , y] 中的最大值


分治算法实现“求数组中最大值”的 C 语言程序如下:

  1. #include <stdio.h>
  2. //自定义函数,其中 [left,right] 表示 arr 数组中查找最大值的范围
  3. int get_max(int* arr, int left, int right) {
  4. int max_left = 0, max_right = 0, middle = 0;
  5. //如果数组不存在
  6. if (arr == NULL) {
  7. return -1;
  8. }
  9. //如果查找范围中仅有一个数字
  10. if (right - left == 0) {
  11. return arr[left];
  12. }
  13. //如果查找范围中有 2 个数字,直接比较即可
  14. if (right - left <= 1) {
  15. if (arr[left] >= arr[right]) {
  16. return arr[left];
  17. }
  18. return arr[right];
  19. }
  20. //等量划分成 2 个区域
  21. middle = (right - left) / 2 + left;
  22. //得到左侧区域中的最大值
  23. max_left = get_max(arr, left, middle);
  24. //得到右侧区域中的最大值
  25. max_right = get_max(arr, middle + 1, right);
  26. //比较左、右两侧的最大值,找到 [left,right] 整个区域的最大值
  27. if (max_left >= max_right) {
  28. return max_left;
  29. }
  30. else {
  31. return max_right;
  32. }
  33. }
  34. int main() {
  35. int arr[4] = { 3,7,2,1 };
  36. int max = get_max(arr, 0, 3);
  37. printf("最大值:%d", max);
  38. return 0;
  39. }


分治算法实现“求数组中最大值”的 Java 程序如下:

  1. public class Demo {
  2. public static int get_max(int [] arr,int left,int right) {
  3. //如果数组不存在或者数组内没有元素
  4. if (arr == null || arr.length == 0) {
  5. return -1;
  6. }
  7. //如果查找范围中仅有 2 个数字,则直接比较即可
  8. if(right - left <=1) {
  9. if(arr[left] >= arr[right]) {
  10. return arr[left];
  11. }
  12. return arr[right];
  13. }
  14. //等量划分成 2 个区域
  15. int middle = (right-left)/2 + left;
  16. int max_left = get_max(arr,left,middle);
  17. int max_right = get_max(arr,middle+1,right);
  18. if(max_left >= max_right) {
  19. return max_left;
  20. }else {
  21. return max_right;
  22. }
  23. }
  24. public static void main(String[] args) {
  25. int [] arr = new int[] { 3,7,2,1 };
  26. int max = get_max(arr,0,3);
  27. System.out.println("最大值:"+max);
  28. }
  29. }


分治算法实现“求数组中最大值”的 Python 程序如下:

  1. def get_max(arr,left,right):
  2. #列表中没有数据
  3. if len(arr) == 0:
  4. return -1
  5. #如果查找范围中仅有 2 个数字,则直接比较即可
  6. if right - left <= 1:
  7. if arr[left] >= arr[right]:
  8. return arr[left]
  9. return arr[right]
  10. #等量划分成 2 个区域
  11. middle = int((right-left)/2 + left)
  12. max_left = get_max(arr,left,middle)
  13. max_right = get_max(arr,middle+1,right)
  14. if max_left >= max_right:
  15. return max_left
  16. else:
  17. return max_right
  18. arr = [3,7,2,1]
  19. max = get_max(arr,0,3)
  20. print("最大值:",max,sep='')


以上程序的输出结果均为:

最大值:7

汉诺塔问题(分治+源码+动画演示)

分治算法解决汉诺塔问题

为了方便讲解,我们将 3 个柱子分别命名为起始柱、目标柱和辅助柱。实际上,解决汉诺塔问题是有规律可循的:
1) 当起始柱上只有 1 个圆盘时,我们可以很轻易地将它移动到目标柱上;

2) 当起始柱上有 2 个圆盘时,移动过程如下图所示:


图 3 移动两个圆盘


移动过程是:先将起始柱上的 1 个圆盘移动到辅助柱上,然后将起始柱上遗留的圆盘移动到目标柱上,最后将辅助柱上的圆盘移动到目标柱上。

3) 当起始柱上有 3 个圆盘时,移动过程如图 2 所示,仔细观察会发现,移动过程和 2 个圆盘的情况类似:先将起始柱上的 2 个圆盘移动到辅助柱上,然后将起始柱上遗留的圆盘移动到目标柱上,最后将辅助柱上的圆盘移动到目标柱上。

通过分析以上 3 种情况的移动思路,可以总结出一个规律:对于 n 个圆盘的汉诺塔问题,移动圆盘的过程是:

  1. 将起始柱上的 n-1 个圆盘移动到辅助柱上;
  2. 将起始柱上遗留的 1 个圆盘移动到目标柱上;
  3. 将辅助柱上的所有圆盘移动到目标柱上。


由此,n 个圆盘的汉诺塔问题就简化成了 n-1 个圆盘的汉诺塔问题。按照同样的思路,n-1 个圆盘的汉诺塔问题还可以继续简化,直至简化为移动 3 个甚至更少圆盘的汉诺塔问题。

如下为分治算法解决汉诺塔问题的伪代码:

// num 表示移动圆盘的数量,source、target、auxiliary 分别表示起始柱、目标柱和辅助柱
hanoi(num , source , target , auxiliary): 
    if num == 1:     // 如果圆盘数量仅有 1 个,则直接从起始柱移动到目标柱
        print(从 source 移动到 target)
    else:
        // 递归调用 hanoi 函数,将 num-1 个圆盘从起始柱移动到辅助柱上,整个过程的实现可以借助目标柱
        hanoi(num-1 , source , auxiliary , target)
        // 将起始柱上剩余的最后一个大圆盘移动到目标柱上
        print(从 source 移动到 target) 
        // 递归调用 hanoi 函数,将辅助柱上的 num-1 圆盘移动到目标柱上,整个过程的实现可以借助起始柱               
        hanoi(n-1 , auxiliary , target , source)

汉诺塔问题的代码实现

根据伪代码,我们为大家编写好了相应的 C 语言、Java 以及 Python 程序。

如下是解决汉诺塔问题的 C 语言程序:

  1. #include <stdio.h>
  2. void hanoi(int num, char sou, char tar,char aux) {
  3. //统计移动次数
  4. static int i = 1;
  5. //如果圆盘数量仅有 1 个,则直接从起始柱移动到目标柱
  6. if (num == 1) {
  7. printf("第%d次:从 %c 移动至 %c\n", i, sou, tar);
  8. i++;
  9. }
  10. else {
  11. //递归调用 hanoi() 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
  12. hanoi(num - 1, sou, aux, tar);
  13. //将起始柱上剩余的最后一个大圆盘移动到目标柱上
  14. printf("第%d次:从 %c 移动至 %c\n", i, sou, tar);
  15. i++;
  16. //递归调用 hanoi() 函数,将辅助柱上的 num-1 圆盘移动到目标柱上
  17. hanoi(num - 1, aux, tar, sou);
  18. }
  19. }
  20. int main()
  21. {
  22. //以移动 3 个圆盘为例,起始柱、目标柱、辅助柱分别用 A、B、C 表示
  23. hanoi(3, 'A', 'B', 'C');
  24. return 0;
  25. }


如下是解决汉诺塔问题的 Java 程序:

  1. public class Demo {
  2. // 统计移动次数
  3. public static int i = 1;
  4. public static void hanoi(int num, char sou, char tar, char sux) {
  5. // 如果圆盘数量仅有 1 个,则直接从起始柱移动到目标柱
  6. if (num == 1) {
  7. System.out.println("第" + i + "次:从" + sou + "移动到" + tar);
  8. i++;
  9. } else {
  10. // 递归调用 hanoi() 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
  11. hanoi(num - 1, sou, sux, tar);
  12. // 将起始柱上剩余的最后一个大圆盘移动到目标柱上
  13. System.out.println("第" + i + "次:从" + sou + "移动到" + tar);
  14. i++;
  15. // 递归调用 hanoi() 函数,将辅助柱上的 num-1 圆盘移动到目标柱上
  16. hanoi(num - 1, sux, tar, sou);
  17. }
  18. }
  19. public static void main(String[] args) {
  20. // 以移动 3 个圆盘为例,起始柱、目标柱、辅助柱分别用 A、B、C 表示
  21. hanoi(3, 'A', 'B', 'C');
  22. }
  23. }


如下是解决汉诺塔问题的 Python 程序:

  1. #记录移动次数
  2. i = 1
  3. def hanoi(num,sou,tar,aux):
  4. global i
  5. if num==1:
  6. print("第%d次:从 %c 移动至 %c" % (i, sou, tar))
  7. i=i+1
  8. else:
  9. #递归调用 hanoi() 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
  10. hanoi(num - 1, sou, aux, tar)
  11. #将起始柱上剩余的最后一个大圆盘移动到目标柱上
  12. print("第%d次:从 %c 移动至 %c" % (i, sou, tar))
  13. i=i+1
  14. #递归调用 hanoi() 函数,将辅助柱上的 num-1 圆盘移动到目标柱上
  15. hanoi(num - 1, aux, tar, sou)
  16. #以移动 3 个圆盘为例,起始柱、目标柱、辅助柱分别用 A、B、C 表示
  17. hanoi(3, 'A', 'B', 'C');


以上程序的执行结果均为:

第1次:从 A 移动至 B
第2次:从 A 移动至 C
第3次:从 B 移动至 C
第4次:从 A 移动至 B
第5次:从 C 移动至 A
第6次:从 C 移动至 B
第7次:从 A 移动至 B

贪心算法

举个例子,假设有 1、2、5、10 这 4 种面值的纸币,要求在不限制各种纸币使用数量的情况下,用尽可能少的纸币拼凑出的总面值为 18。贪心算法的解决方案如下:

  • 率先选择一张面值为 10 的纸币,可以最大程度上减少需要拼凑的数额(剩余 8);
  • 选择一张面值为 5 的纸币,需要拼凑的数额降为 3;
  • 选择一张面值为 2 的纸币,需要拼凑的数额降为 1;
  • 选择一张面值为 1 的纸币,完成拼凑。


可以看到,每一步都力求最大限度地解决问题,每一步都选择的是当前最优的解决方案,这种解决问题的算法就是贪心算法。

注意,虽然贪心算法每一步都是最优的解决方案,但整个算法并不一定是最优的。仍以选纸币为例,假设有 1、7、10 这 3 种面值的纸币,每种纸币使用的数量不限,要求用尽可能少的纸币拼凑出的总面值为 15,贪心算法的解决方案为:

  • 选择一张面值为 10 的纸币,需要拼凑的数额降为 5;
  • 选择一张面值为 1 的纸币,需要拼凑的数额降为 4;
  • 选择一张面值为 1 的纸币,需要拼凑的数额降为 3;
  • 选择一张面值为 1 的纸币,需要拼凑的数额降为 2;
  • 选择一张面值为 1 的纸币,需要拼凑的数额降为 1;
  • 选择一张面值为 1 的纸币,完成拼凑。


最终贪心算法给出的解决方案是 10+1+1+1+1+1 共 6 张纸币,但是通过思考,最优的解决方案应该是只需要用 3 张纸币(7+7+1)。

部分背包问题

贪心算法解决部分背包问题

假设商店中有 3 种商品,它们各自的重量和收益是:

  • 商品 1:重量 10 斤,收益 60 元;
  • 商品 2:重量 20 斤,收益 100 元;
  • 商品 3:重量 30 斤,收益 120 元。


对于每件商品,顾客可以购买商品的一部分(可再分)。一个小偷想到商店行窃,他的背包最多只能装 50 斤的商品,如何选择才能获得最大的收益呢?

贪心算法解决此问题的思路是:计算每个商品的收益率(收益/重量),优先选择收益率最大的商品,直至所选商品的总重量达到 50 斤。

如下是使用贪心算法解决此问题的伪代码:

// w 存储各个商品的重量,p 存储各个商品的收益,W 表示背包的承重
fractional_knapsack(w[] , p[] , W):
    sort(w , p)                   //根据收益率对商品进行排序
    i <- 0
    while W > 0:                  //只要背包还有剩余空间,就一直装
        temp = min(W , w[i])      //判断该商品能否全部装入
        result[i] <- temp/w[i]    //将实际装入到背包中的商品量以百分比的方式存储起来
        W <- W - temp             //计算背包的剩余容量,为装后续商品做准备
        i <- i + 1
    return result                 //返回统计装入信息的 result


如下是根据伪代码编写的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 3 //设定商品数量
  3. //根据收益率,对记录的商品进行从大到小排序
  4. void Sort(float w[], float p[]) {
  5. int i,j;
  6. float temp;
  7. float v[N] = { 0 };
  8. //用v[]存商品的收益率
  9. for (i = 0; i < N; i++)
  10. v[i] = p[i] / w[i];
  11. //根据 v 数组记录的各个商品收益率的大小,同时对 w 和 p 数组进行排序
  12. for (i = 0; i < N; i++) {
  13. for (j = i + 1; j < N; j++) {
  14. if (v[i] < v[j]) {
  15. temp = v[i];
  16. v[i] = v[j];
  17. v[j] = temp;
  18. temp = w[i];
  19. w[i] = w[j];
  20. w[j] = temp;
  21. temp = p[i];
  22. p[i] = p[j];
  23. p[j] = temp;
  24. }
  25. }
  26. }
  27. }
  28. /*贪心算法解决部分背包问题
  29. w:记录各个商品的总重量
  30. p:记录各个商品的总价值
  31. result:记录各个商品装入背包的比例
  32. W:背包的容量
  33. */
  34. void fractional_knapsack(float w[], float p[], float result[], float W) {
  35. float temp = 0;
  36. int i = 0;
  37. //根据收益率,重新商品进行排序
  38. Sort(w, p);
  39. //从收益率最高的商品开始装入背包,直至背包装满为止
  40. while (W > 0) {
  41. temp = W > w[i] ? w[i] : W;
  42. result[i] = temp / w[i];
  43. W -= temp;
  44. i++;
  45. }
  46. }
  47. int main() {
  48. int i;
  49. //统计背包中商品的总收益
  50. float values = 0;
  51. //各个商品的重量
  52. float w[N] = { 10,30,20 };
  53. //各个商品的收益
  54. float p[N] = { 60,100,120 };
  55. float result[N] = { 0 };
  56. //调用解决部分背包问题的函数
  57. fractional_knapsack(w, p, result, 50);
  58. //根据 result 数组中记录的数据,决定装入哪些商品
  59. for (i = 0; i < N; i++) {
  60. if (result[i] == 1) {
  61. printf("总重量为 %f,总价值为 %f 的商品全部装入\n", w[i], p[i]);
  62. values += p[i];
  63. }
  64. else if (result[i] == 0)
  65. printf("总重量为 %f,总价值为 %f 的商品不装\n", w[i], p[i]);
  66. else {
  67. printf("总重量为 %f,总价值为 %f 的商品装入 %f%%\n", w[i], p[i], result[i] * 100);
  68. values += p[i] * result[i];
  69. }
  70. }
  71. printf("最终收获的商品价值为 %.2f\n", values);
  72. return 0;
  73. }


如下是根据伪代码编写的 Java 程序:

  1. public class Demo {
  2. //根据收益率,对记录的商品进行从大到小排序
  3. public static void sort(float [] w, float [] p) {
  4. int length = w.length;
  5. //用v[]存商品的收益率
  6. float [] v = new float[length];
  7. for (int i=0;i<length;i++) {
  8. v[i] = p[i]/w[i];
  9. }
  10. //根据 v 数组记录的各个商品收益率的大小,同时对 w 和 p 数组进行排序
  11. for (int i = 0; i < length; i++) {
  12. for (int j = i + 1; j < length; j++) {
  13. if (v[i] < v[j]) {
  14. float temp = v[i];
  15. v[i] = v[j];
  16. v[j] = temp;
  17. temp = w[i];
  18. w[i] = w[j];
  19. w[j] = temp;
  20. temp = p[i];
  21. p[i] = p[j];
  22. p[j] = temp;
  23. }
  24. }
  25. }
  26. }
  27. /*贪心算法解决部分背包问题
  28. w:记录各个商品的总重量
  29. p:记录各个商品的总价值
  30. result:记录各个商品装入背包的比例
  31. W:背包的容量
  32. */
  33. public static void fractional_knapsack(float []w, float []p, float []result, float W) {
  34. //根据收益率,重新对商品进行排序
  35. sort(w, p);
  36. int i=0;
  37. //从收益率最高的商品开始装入背包,直至背包装满为止
  38. while (W > 0) {
  39. float temp = W > w[i]?w[i]:W;
  40. result[i] = temp / w[i];
  41. W -= temp;
  42. i++;
  43. }
  44. }
  45. public static void main(String[] args) {
  46. //设定背包的容量
  47. float W = 50;
  48. //各个商品的重量
  49. float [] w = { 10,30,20 };
  50. //各个商品的价值
  51. float [] p = { 60,100,120 };
  52. //统计背包中商品的总收益
  53. float [] result = {0,0,0};
  54. //调用解决部分背包问题的函数
  55. fractional_knapsack(w,p,result,W);
  56. //统计背包中商品的总收益
  57. float values = 0;
  58. //根据 result 数组中记录的数据,决定装入哪些商品
  59. for (int i = 0; i < w.length; i++) {
  60. if (result[i] == 1) {
  61. System.out.println("总重量为"+w[i]+",总价值为"+p[i]+"的商品全部装入");
  62. values += p[i];
  63. }
  64. else if (result[i] == 0)
  65. System.out.println("总重量为"+w[i]+",总价值为"+p[i]+"的商品不装");
  66. else {
  67. System.out.println("总重量为"+w[i]+",总价值为"+p[i]+"的商品装入"+result[i]*100+"%");
  68. values += p[i] * result[i];
  69. }
  70. }
  71. System.out.print("最终收获的商品价值为"+values);
  72. }
  73. }


如下是根据伪代码编写的 Python 程序:

  1. #设定背包的容量
  2. W = 50
  3. #各个商品的重量
  4. w = [10,30,20]
  5. #各个商品的价值
  6. p = [60,100,120]
  7. #根据收益率,对记录的商品进行从大到小排序
  8. def sort():
  9. #用v列表存商品的收益率
  10. v = []
  11. length = len(w)
  12. for i in range(length):
  13. v.append(p[i]/w[i])
  14. #根据 v 列表记录的各个商品收益率的大小,同时对 w 和 p 数组进行排序
  15. for i in range(length):
  16. for j in range(i+1,length):
  17. if v[i] < v[j]:
  18. v[i],v[j] = v[j],v[i]
  19. w[i],w[j] = w[j],w[i]
  20. p[i],p[j] = p[j],p[i]
  21. result = [0,0,0]
  22. def fractional_knapsack():
  23. global W
  24. #根据收益率,重新对商品进行排序
  25. sort()
  26. i = 0
  27. #从收益率最高的商品开始装入背包,直至背包装满为止
  28. while(W>0):
  29. temp = min(W,w[i])
  30. result[i] = temp/w[i]
  31. W = W - temp
  32. i = i + 1
  33. fractional_knapsack()
  34. #统计背包中商品的总收益
  35. values = 0
  36. #根据 result 列表中记录的数据,决定装入哪些商品
  37. for i in range(len(result)):
  38. if result[i] ==1:
  39. print("总重量为 %f,总价值为 %f 的商品全部装入"%(w[i],p[i]))
  40. values = values + p[i]
  41. elif result[i]==0:
  42. print("总重量为 %f,总价值为 %f 的商品不装"%(w[i],p[i]))
  43. else:
  44. print("总重量为 %f,总价值为 %f 的商品装入%f%%"%(w[i],p[i],result[i]*100))
  45. values = values + p[i]*result[i]
  46. print("最终收获的商品价值为:%f" %(values))


以上程序的输出结果均为:

总重量为 10.000000,总价值为 60.000000 的商品全部装入
总重量为 20.000000,总价值为 120.000000 的商品全部装入
总重量为 30.000000,总价值为 100.000000 的商品装入66.666667%
最终收获的商品价值为:246.666667

动态规划算法

动态规划算法解决问题的过程和分治算法类似,也是先将问题拆分成多个简单的小问题,通过逐一解决这些小问题找到整个问题的答案。不同之处在于,分治算法拆分出的小问题之间是相互独立的,而动态规划算法拆分出的小问题之间相互关联,例如要想解决问题 A,必须先解决问题 B 和 C。

动态规划解决01背包问题

虚拟一个场景,商店中拥有 5 件商品,它们各自的重量和收益分别是:

  • 商品 1:重量 1 斤,收益 1 元;
  • 商品 2:重量 2 斤,收益 6 元;
  • 商品 3:重量 5 斤,收益 18 元;
  • 商品 4:重量 6 斤,收益 22 元;
  • 商品 5:重量 7 斤,收益 28 元。

所有商品不可再分,顾客要么“整件”购买商品,要么放弃购买。一个小偷想窃取商品,他的背包只能装 11 斤商品,如何选择商品才能获得最大的收益呢?

动态规划算法解决此问题的核心思想是:背包承重 1 斤时所能获得的最大收益是很容易计算的,在此基础上,可以推算出背包承重 2 斤、3斤、...、14斤、15斤时所能获得的最大收益。建立如下这张表格,依次将各个商品装入不同承重的背包中,计算出它们所能获得的最大收益。

结合表 5,当背包承重为 11 斤时,所能获得的最大收益为 40 元。如下以伪代码的形式给大家总结了以上推理的整个过程:

输入 N    // 指定商品种类
输入 W    // 指定背包载重量
//w[] 记录各个商品的载重量,v[] 记录各个商品对应的收益
knapsack01(w[] , v[]):
    //逐个遍历每个商品
    for i <- 1 to N:
        //求出从 1 到 W 各个载重量对应的最大收益
        for j <- 1 to W:
            //如果背包载重量小于商品总重量,则商品无法放入背包,收益不变
            if  j < w[i]:
                result[i][j] = result[i-1][j]
            else:
                //比较装入该商品和不装该商品,哪种情况获得的收益更大,记录最大收益值
                result[i][j] = max(result[i-1][j] , v[i]+result[i-1][j-w[i]])
    return result 

01背包问题的具体实现

 结合伪代码,如下是用动态规划算法解决 01 背包问题的 C 语言程序:

  1. #include<stdio.h>
  2. #define N 5 //商品的种类
  3. #define W 11 //背包的最大承重
  4. /*
  5. 动态规划算法解决01背包问题
  6. result[N + 1][W + 1]:存储最终的结果
  7. w[N + 1]:存储各商品的重量
  8. v[N + 1]:存储各商品的价值
  9. */
  10. void knapsack01(int result[N + 1][W + 1], int w[N + 1], int v[N + 1]) {
  11. int i, j;
  12. //逐个遍历每个商品
  13. for (i = 1; i <= N; i++) {
  14. //求出从 1 到 W 各个载重对应的最大收益
  15. for (j = 1; j <= W; j++) {
  16. //如果背包载重小于商品总重量,则该商品无法放入背包,收益不变
  17. if (j < w[i])
  18. result[i][j] = result[i - 1][j];
  19. else
  20. //比较装入该商品和不装该商品,哪种情况获得的收益更大,记录最大收益值
  21. result[i][j] = result[i - 1][j] > (v[i] + result[i - 1][j - w[i]]) ? result[i - 1][j] : (v[i] + result[i - 1][j - w[i]]);
  22. }
  23. }
  24. }
  25. //追溯选中的商品
  26. void select(int result[N + 1][W + 1], int w[N + 1], int v[N + 1]) {
  27. int n = N;
  28. int bagw = W;
  29. //逐个商品进行判断
  30. while (n > 0) {
  31. //如果在指定载重量下,该商品对应的收益和上一个商品对应的收益相同,则表明未选中
  32. if (result[n][bagw] == result[n - 1][bagw]) {
  33. n--;
  34. }
  35. else {
  36. //输出被选用商品的重量和价值
  37. printf("(%d,%d) ", w[n], v[n]);
  38. //删除被选用商品的承重,以便继续遍历
  39. bagw = bagw - w[n];
  40. n--;
  41. }
  42. }
  43. }
  44. int main()
  45. {
  46. int w[N + 1] = { 0,1 , 2 , 5 , 6 , 7 }; //商品的承重
  47. int v[N + 1] = { 0,1 , 6 , 18 , 22 , 28 }; //商品的价值
  48. int result[N + 1][W + 1] = { 0 }; //记录统计数据
  49. knapsack01(result, w, v);
  50. printf("背包承重为 %d,最大收益为 %d\n", W, result[N][W]);
  51. printf("选择了:");
  52. select(result, w, v);
  53. return 0;
  54. }


如下为用动态规划算法解决 01 背包问题的 Java 程序:

  1. public class Demo {
  2. static int N = 5;//商品的种类
  3. static int W = 11;//背包的承重
  4. //动态规划算法解决01背包问题
  5. public static void knapsack01(int [][] result , int [] w,int []v) {
  6. //逐个遍历每个商品
  7. for(int i=1;i<=N;i++) {
  8. //求出从 1 到 W 各个承重对应的最大收益
  9. for ( int j=1;j<=W;j++) {
  10. //如果背包承重小于商品总重量,则该商品无法放入背包,收益不变
  11. if(j<w[i]) {
  12. result[i][j] = result[i-1][j];
  13. }else {
  14. //比较装入该商品和不装该商品,哪种情况获得的收益更大,记录最大收益值
  15. result[i][j] = result[i - 1][j] > (v[i] + result[i - 1][j - w[i]]) ? result[i - 1][j] : (v[i] + result[i - 1][j - w[i]]);
  16. }
  17. }
  18. }
  19. }
  20. //追溯选中的商品
  21. public static void select(int [][] result , int [] w,int []v) {
  22. int n = N;
  23. int bagw = W;
  24. //逐个商品进行判断
  25. while(n>0) {
  26. //如果在指定承重下,该商品对应的收益和上一个商品对应的收益相同,则表明未选中
  27. if (result[n][bagw] == result[n - 1][bagw]) {
  28. n--;
  29. }
  30. else {
  31. //输出被选用商品的重量和价值
  32. System.out.print("("+w[n]+","+v[n]+") ");
  33. //删除被选用商品的承重,以便继续遍历
  34. bagw = bagw - w[n];
  35. n--;
  36. }
  37. }
  38. }
  39. public static void main(String[] args) {
  40. int [] w= {0,1 , 2 , 5 , 6 , 7}; //商品的重量
  41. int [] v ={0,1 , 6 , 18 , 22 , 28}; //商品的价值
  42. int [][] result = new int[N+1][W+1];
  43. knapsack01(result, w, v);;
  44. System.out.println("背包可容纳重量为 "+W+",最大收益为 "+result[N][W]);
  45. System.out.print("选择了");
  46. select(result, w,v);
  47. }
  48. }


如下为用动态规划算法解决 01 背包问题的 Python 程序:

  1. N = 5 #商品的种类
  2. W = 11 #背包的承重
  3. w = [0,1,2,5,6,7] #商品的承重,不使用 w[0]
  4. v = [0,1,6,18,22,28] #商品的价值,不使用 v[0]
  5. #二维列表,记录统计数据
  6. result = [[0]*(W+1),[0]*(W+1),[0]*(W+1),[0]*(W+1),[0]*(W+1),[0]*(W+1)]
  7. #动态规划算法解决01背包问题
  8. def knapsack01():
  9. #逐个遍历每个商品
  10. for i in range(1,N+1):
  11. #求出从 1 到 W 各个承重对应的最大收益
  12. for j in range(1,W+1):
  13. #如果背包承重小于商品总重量,则该商品无法放入背包,收益不变
  14. if j<w[i]:
  15. result[i][j] = result[i-1][j];
  16. else:
  17. #比较装入该商品和不装该商品,哪种情况获得的收益更大,记录最大收益值
  18. result[i][j] = max(result[i-1][j],v[i]+result[i-1][j-w[i]])
  19. knapsack01()
  20. print("背包可容纳重量为 %d,最大收益为 %d"%(W, result[N][W]))
  21. #追溯选中的商品
  22. def select():
  23. n = N
  24. bagw = W
  25. #逐个商品进行判断
  26. while n > 0:
  27. #如果在指定承重下,该商品对应的收益和上一个商品对应的收益相同,则表明未选中
  28. if result[n][bagw] == result[n-1][bagw]:
  29. n = n - 1
  30. else:
  31. #输出被选用商品的重量和价值
  32. print("(%d,%d) "%(w[n],v[n]))
  33. #删除被选用商品的承重,以便继续遍历
  34. bagw = bagw - w[n]
  35. n = n - 1
  36. print("所选商品为:")
  37. select()


以上程序的输出结果均为:

背包可容纳重量为 11,最大收益为 40
选择了(6,22) (5,18)

回溯算法

在图 1 中找到从 A 到 K 的行走路线,一些读者会想到用穷举算法(简称穷举法),即简单粗暴地将从 A 出发的所有路线罗列出来,然后逐一筛选,最终找到正确的路线。

 

回溯算法的应用场景

回溯算法经常以递归的方式实现,用来解决以下 3 类问题:

  • 决策问题:从众多选择中找到一个可行的解决方案;
  • 优化问题:从众多选择中找到一个最佳的解决方案;
  • 枚举问题:找出能解决问题的所有方案。

 

回溯算法解决迷宫问题

以图 1 所示的迷宫为例,回溯算法解决此问题的具体思路是:

  1. 从当前位置开始,分别判断是否可以向 4 个方向(上、下、左、右)移动:
  2. 选择一个方向并移动到下个位置。判断此位置是否为终点,如果是就表示找到了一条移动路线;如果不是,在当前位置继续判断是否可以向 4 个方向移动;
  3. 如果 4 个方向都无法移动,则回退至之前的位置,继续判断其它的方向;
  4. 重复 2、3 步,最终要么成功找到可行的路线,要么回退至起点位置,表明所有的路线都已经判断完毕。


程序中,我们可以用特殊的字符表示迷宫中的不同区域。例如,用 1 表示可以移动的白色区域,用 0 表示不能移动的黑色区域,图 1 的迷宫可以用如下的 0-1 矩阵来表示:

1 0 1 1 1
1 1 1 0 1
1 0 0 1 1
1 0 0 1 0
1 0 0 1 1

如下是用回溯算法解决迷宫问题的伪代码:

输入 maze[ROW][COL]   //输入迷宫地图,0 表示黑色区域,1 表示可行走区域
//(row,col) 表示起点,(outrow,outcol)表示终点
maze_puzzle(maze,row,col,outrow,outcol):
    //回溯过程中,行走的每一区域都设为 Y,表示已经进行了判断
    maze[row][col] <- 'Y'
    //如果行走至终点,表明有从起点到终点的路线
    if row == outrow && col == outcol:
        Print maze  // 输出行走路线
        return
    //判断是否可以向上移动
    if canMove(maze,row-1,col):
        maze_puzzle(maze,row-1,col,outrow,outcol)
    //判断是否可以向左移动
    if canMove(maze,row,col-1):
        maze_puzzle(maze,row,col-1,outrow,outcol)
    //判断是否可以向下移动
    if canmove(maze,row+1,col):
        maze_puzzle(maze,row+1,col,outrow,outcol)
    //判断是否可以向右移动
    if canmove(maze,row,col+1):
        maze_puzzle(maze,row,col+1,outrow,outcol)


结合伪代码,如下为解决迷宫问题的 C 语言程序:

  1. #include <stdio.h>
  2. typedef enum { false, true } bool;
  3. #define ROW 5
  4. #define COL 5
  5. //假设当前迷宫中没有起点到终点的路线
  6. bool find = false;
  7. //回溯算法查找可行路线
  8. void maze_puzzle(char maze[ROW][COL], int row, int col, int outrow, int outcol);
  9. //判断 (row,col) 区域是否可以移动
  10. bool canMove(char maze[ROW][COL], int row, int col);
  11. //输出行走路线
  12. void printmaze(char maze[ROW][COL]);
  13. int main()
  14. {
  15. char maze[ROW][COL] = {
  16. {'1','0','1','1','1'},
  17. {'1','1','1','0','1'},
  18. {'1','0','0','1','1'},
  19. {'1','0','0','1','0'},
  20. {'1','0','0','1','1'} };
  21. maze_puzzle(maze, 0, 0, ROW - 1, COL - 1);
  22. if (find == false) {
  23. printf("未找到可行线路");
  24. }
  25. return 0;
  26. }
  27. //(row,col) 表示起点,(outrow,outcol)表示终点
  28. void maze_puzzle(char maze[ROW][COL], int row, int col, int outrow, int outcol) {
  29. maze[row][col] = 'Y'; // 将各个走过的区域标记为 Y
  30. //如果行走至终点,表明有从起点到终点的路线
  31. if (row == outrow && col == outcol) {
  32. find = true;
  33. printf("成功走出迷宫,路线图为:\n");
  34. printmaze(maze);
  35. return;
  36. }
  37. //尝试向上移动
  38. if (canMove(maze, row - 1, col)) {
  39. maze_puzzle(maze, row - 1, col, outrow, outcol);
  40. //如果程序不结束,表明此路不通,恢复该区域的标记
  41. maze[row - 1][col] = '1';
  42. }
  43. //尝试向左移动
  44. if (canMove(maze, row, col - 1)) {
  45. maze_puzzle(maze, row, col - 1, outrow, outcol);
  46. //如果程序不结束,表明此路不通,恢复该区域的标记
  47. maze[row][col - 1] = '1';
  48. }
  49. //尝试向下移动
  50. if (canMove(maze, row + 1, col)) {
  51. maze_puzzle(maze, row + 1, col, outrow, outcol);
  52. //如果程序不结束,表明此路不通,恢复该区域的标记
  53. maze[row + 1][col] = '1';
  54. }
  55. //尝试向右移动
  56. if (canMove(maze, row, col + 1)) {
  57. maze_puzzle(maze, row, col + 1, outrow, outcol);
  58. //如果程序不结束,表明此路不通,恢复该区域的标记
  59. maze[row][col + 1] = '1';
  60. }
  61. }
  62. //判断 (row,col) 区域是否可以移动
  63. bool canMove(char maze[ROW][COL], int row, int col) {
  64. //如果目标区域位于地图内,不是黑色区域,且尚未行走过,返回 true:反之,返回 false
  65. return row >= 0 && row <= ROW - 1 && col >= 0 && col <= COL - 1 && maze[row][col] != '0' && maze[row][col] != 'Y';
  66. }
  67. //输出可行的路线
  68. void printmaze(char maze[ROW][COL]) {
  69. int i, j;
  70. for (i = 0; i < ROW; i++) {
  71. for (j = 0; j < COL; j++) {
  72. printf("%c ", maze[i][j]);
  73. }
  74. printf("\n");
  75. }
  76. }


如下为解决迷宫问题的 Java 程序:

  1. public class Demo {
  2. static boolean find = false;
  3. static int ROW = 5;
  4. static int COL = 5;
  5. //(row,col) 表示起点,(outrow,outcol)表示终点
  6. public static void maze_puzzle(char [][] maze, int row, int col, int outrow, int outcol) {
  7. maze[row][col] = 'Y'; // 将各个走过的区域标记为 Y
  8. //如果行走至终点,表明有从起点到终点的路线
  9. if (row == outrow && col == outcol) {
  10. find = true;
  11. System.out.println("成功走出迷宫,路线图为:");
  12. printmaze(maze);
  13. return ;
  14. }
  15. //尝试向上移动
  16. if (canMove(maze, row - 1, col)) {
  17. maze_puzzle(maze, row - 1, col, outrow, outcol);
  18. //如果程序不结束,表明此路不通,恢复该区域的标记
  19. maze[row - 1][col] = '1';
  20. }
  21. //尝试向左移动
  22. if (canMove(maze, row, col - 1)) {
  23. maze_puzzle(maze, row, col - 1, outrow, outcol);
  24. //如果程序不结束,表明此路不通,恢复该区域的标记
  25. maze[row][col - 1] = '1';
  26. }
  27. //尝试向下移动
  28. if (canMove(maze, row + 1, col)) {
  29. maze_puzzle(maze, row + 1, col, outrow, outcol);
  30. //如果程序不结束,表明此路不通,恢复该区域的标记
  31. maze[row + 1][col] = '1';
  32. }
  33. //尝试向右移动
  34. if (canMove(maze, row, col + 1)) {
  35. maze_puzzle(maze, row, col + 1, outrow, outcol);
  36. //如果程序不结束,表明此路不通,恢复该区域的标记
  37. maze[row][col + 1] = '1';
  38. }
  39. }
  40. //判断(row,col)区域是否可以移动
  41. public static boolean canMove(char [][] maze, int row, int col) {
  42. //如果目标区域位于地图内,不是黑色区域,且尚未移动过,返回 true:反之,返回 false
  43. return row >= 0 && row <= ROW - 1 && col >= 0 && col <= COL - 1 && maze[row][col] != '0' && maze[row][col] != 'Y';
  44. }
  45. //输出行走路线
  46. public static void printmaze(char [][] maze) {
  47. for(int i=0;i<ROW;i++) {
  48. for(int j=0;j<COL;j++) {
  49. System.out.print(maze[i][j]+" ");
  50. }
  51. System.out.println();
  52. }
  53. }
  54. public static void main(String[] args) {
  55. char [][]maze = new char[][]{
  56. {'1','0','1','1','1'},
  57. {'1','1','1','0','1'},
  58. {'1','0','0','1','1'},
  59. {'1','0','0','1','0'},
  60. {'1','0','0','1','1'} };
  61. maze_puzzle(maze, 0, 0, ROW - 1, COL - 1);
  62. if (find == false) {
  63. System.out.print("未找到可行线路");
  64. }
  65. }
  66. }


如下为解决迷宫问题的 Python 程序:

  1. #指定地图的行数和列数
  2. ROW = 5
  3. COL = 5
  4. #初始化地图
  5. maze =[['1','0','1','1','1'],
  6. ['1','1','1','0','1'],
  7. ['1','0','0','1','1'],
  8. ['1','0','0','1','0'],
  9. ['1','0','0','1','1']]
  10. #假设当前迷宫中没有起点到终点的路线
  11. find = False
  12. #回溯算法查找可行路线
  13. def maze_puzzle(maze,row,col,outrow,outcol):
  14. global find
  15. maze[row][col] = 'Y'
  16. if row == outrow and col == outcol:
  17. find = True
  18. print("成功走出迷宫,路线图为:")
  19. printmaze(maze)
  20. return
  21. if canMove(maze,row-1,col):
  22. maze_puzzle(maze, row - 1, col, outrow, outcol)
  23. #如果程序不结束,表明此路不通,恢复该区域的标记
  24. maze[row - 1][col] = '1'
  25. if canMove(maze, row, col - 1):
  26. maze_puzzle(maze, row, col - 1, outrow, outcol)
  27. #如果程序不结束,表明此路不通,恢复该区域的标记
  28. maze[row][col - 1] = '1'
  29. #尝试向下移动
  30. if canMove(maze, row + 1, col):
  31. maze_puzzle(maze, row + 1, col, outrow, outcol)
  32. #如果程序不结束,表明此路不通,恢复该区域的标记
  33. maze[row + 1][col] = '1'
  34. #尝试向右移动
  35. if canMove(maze, row, col + 1):
  36. maze_puzzle(maze, row, col + 1, outrow, outcol)
  37. #如果程序不结束,表明此路不通,恢复该区域的标记
  38. maze[row][col + 1] = '1'
  39. #判断(row,col)区域是否可以移动
  40. def canMove(maze,row,col):
  41. return row >= 0 and row <= ROW - 1 and col >= 0 and col <= COL - 1 and maze[row][col] != '0' and maze[row][col] != 'Y'
  42. #输出行走路线
  43. def printmaze(maze):
  44. for i in range(0,ROW):
  45. for j in range(0,COL):
  46. print(maze[i][j],end=" ")
  47. print()
  48. maze_puzzle(maze,0,0,ROW-1,COL-1)
  49. if find == False:
  50. print("未找到可行路线")


以上程序的执行结果均为:

成功走出迷宫,路线图为:
Y 0 Y Y Y
Y Y Y 0 Y
1 0 0 Y Y
1 0 0 Y 0
1 00 Y Y 

N皇后问题

回溯算法解决N皇后问题

要想使 N 个皇后不相互攻击,应将它们放置在不同的行、不同的列、还不能位于同一条 45°(或 135°)角的斜线上。

回溯算法解决N皇后问题的具体思路是:将 N 个皇后逐一放置在不同的行,以“回溯”的方式逐一测试出每行皇后所在行的具体位置,最终确定所有皇后的位置。

如下为回溯算法解决 N 皇后问题的伪代码:

输入 N      // 输入皇后的个数
q[1...N]    //存储每行的皇后的具体位置(列标)
n_queens(k , n):    // 确定第 k 行皇后的位置
    if k > n:             // 递归的出口
        Print q          // 输出各个皇后的位置
    else:
        for j <- 1 to n:      // 从第 k 行第 1 列开始,判断各个位置是否可行
            if isSafe(k , j):    // 如果可行,继续判断下一行
                q[k] <- j        // 将第 k 行皇后放置的位置 j 记录下来
                n_queens(k+1 , n)    // 继续判断下一行皇后的位置


借助伪代码,如下是解决N皇后问题的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 20 //皇后的数量
  3. int q[N]; //各行皇后所在的列
  4. int count = 0; //统计N皇后问题解的个数
  5. //输出 N 皇后问题的解决方案
  6. void print(int n)
  7. {
  8. int i, j;
  9. count++;
  10. printf("第%d个解:\n", count);
  11. for (i = 1; i <= n; i++) //行
  12. {
  13. for (j = 1; j <= n; j++) //列
  14. {
  15. if (q[i] != j)
  16. printf("x");
  17. else
  18. printf("Q");
  19. }
  20. printf("\n");
  21. }
  22. printf("\n");
  23. }
  24. //检验第k行第j列上是否可以摆放皇后
  25. int isSafe(int k, int j)
  26. {
  27. int i;
  28. for (i = 1; i < k; i++) {
  29. //如果有其它皇后位置同一列上,或者位于该位置的斜线位置上,则该位置无法使用
  30. if (q[i] == j || abs(i - k) == abs(q[i] - j))
  31. return 0;
  32. }
  33. return 1;
  34. }
  35. //放置皇后到棋盘上
  36. void n_queens(int k, int n)
  37. {
  38. int j;
  39. if (k > n) //递归的出口
  40. print(n);
  41. else
  42. {
  43. for (j = 1; j <= n; j++) //试探第k行的每一列,找到符合要求的列
  44. {
  45. if (isSafe(k, j))
  46. {
  47. q[k] = j;
  48. n_queens(k + 1, n); //在确认第 k 行皇后位置的前提下,继续测试下一行皇后的位置
  49. }
  50. }
  51. }
  52. }
  53. int main()
  54. {
  55. int n;
  56. printf("请输入皇后个数:");
  57. scanf("%d", &n);
  58. n_queens(1, n);
  59. printf("共有 %d 种不同的排列", count);
  60. return 0;
  61. }


如下为解决 N 皇后问题的 Java 程序:

  1. import java.util.Scanner;
  2. public class Demo {
  3. static int[] q = new int[20];
  4. static int count = 0;
  5. public static void n_queens(int k, int n) {
  6. int j;
  7. if (k > n)
  8. print(n);
  9. else {
  10. for (j = 1; j <= n; j++) // 试探第k行的每一列,找到符合要求的列
  11. {
  12. if (isSafe(k, j)) {
  13. q[k] = j;
  14. n_queens(k + 1, n); // 在确认第 k 行皇后位置的前提下,继续测试下一行皇后的位置
  15. }
  16. }
  17. }
  18. }
  19. public static boolean isSafe(int k, int j) {
  20. int i;
  21. for (i = 1; i < k; i++) {
  22. // 如果有其它皇后位置同一列上,或者位于该位置的斜线位置上,则该位置无法使用
  23. if (q[i] == j || Math.abs(i - k) == Math.abs(q[i] - j))
  24. return false;
  25. }
  26. return true;
  27. }
  28. // 输出 N 皇后问题的解决方案
  29. public static void print(int n) {
  30. int i, j;
  31. count++;
  32. System.out.println("第 " + count + " 个解:");
  33. for (i = 1; i <= n; i++) // 行
  34. {
  35. for (j = 1; j <= n; j++) // 列
  36. {
  37. if (q[i] != j)
  38. System.out.print("x");
  39. else
  40. System.out.print("Q");
  41. }
  42. System.out.println();
  43. }
  44. System.out.println();
  45. }
  46. public static void main(String[] args) {
  47. System.out.println("请输入皇后个数:");
  48. Scanner sc = new Scanner(System.in);
  49. int n = sc.nextInt();
  50. n_queens(1, n);
  51. System.out.println("共有 " + count + " 种摆放方式");
  52. }
  53. }


如下为解决 N 皇后问题的 Python 程序:

  1. count = 0 #统计解决方案的个数
  2. q = [0]*20 #记录各个皇后的放置位置,最多放置 20 个皇后
  3. #输出 N 皇后问题的解决方案
  4. def display(n):
  5. global count
  6. count = count + 1
  7. print("输出第%d个解:" % (count))
  8. for i in range(1 , n + 1):
  9. for j in range(1 , n + 1):
  10. if q[i] != j:
  11. print("x",end=" ");
  12. else:
  13. print("Q",end=" ");
  14. print()
  15. print()
  16. #检验第k行的第j列是否可以摆放皇后
  17. def isSafe(k , j):
  18. for i in range(1 , k):
  19. #如果有其它皇后位置同一列上,或者位于该位置的斜线位置上,则该位置无法使用
  20. if q[i] == j or abs(i - k) == abs(q[i] - j):
  21. return False
  22. return True
  23. #放置皇后到棋盘上
  24. def n_queens(k , n):
  25. if k > n: #递归的出口
  26. display(n)
  27. else:
  28. for j in range(1 , n + 1): #试探第k行的每一列,找到符合要求的列
  29. if isSafe(k , j):
  30. q[k] = j
  31. n_queens(k + 1 , n); #在确认第 k 行皇后位置的前提下,继续测试下一行皇后的位置
  32. print("请输入皇后个数:")
  33. n = int(input());
  34. n_queens(1,n)
  35. print("共有 %d 种不同的排列" % (count))


假设皇后的总个数为 4,以上程序的输出结果均为:

请输入皇后个数:
4
输出第1个解:
x Q x x
x x x Q
Q x x x
x x Q x

输出第2个解:
x x Q x
Q x x x
x x x Q
x Q x x

共有 2 种不同的排列

冒泡排序算法

冒泡排序算法的具体实现

如下是冒泡排序算法实现升序排序的伪代码:

Bubble_sort(list):                         // list 表示待排序序列
    for i <- 0 to length(list)-1:          // 对于元素个数为 n 的 list 序列,需遍历 n-1 次,这里用 [0,length(list)-1) 表示。
        for j <- 1 to length(list) - i:    // 从第 1 个元素开始遍历,遍历区间为 [1,length(list)-i)。
            if list[j] > list[j+1]:        // 若进行降序排序,则改成 < 小于号
                 swap(list[j] , list[j+1]) // 交换 2 个相邻元素的位置
    return list                            // 返回排好序的序列

根据伪代码,冒泡排序算法的时间复杂度为O(n2)

如下是用冒泡排序算法对 {14, 33, 27, 35, 10} 完成升序排序的 C 语言程序:

  1. #include<stdio.h>
  2. #define N 5 //设定待排序序列中的元素个数
  3. //实现冒泡升序排序算法的函数,list[N] 为待排序数组
  4. void Bubble_sort(int list[N]) {
  5. int i, j;
  6. int temp = 0;
  7. // N 个元素,遍历 N-1 次
  8. for (i = 0; i < N - 1; i++) {
  9. // 从第 1 个元素开始遍历,遍历至 N-1-i
  10. for (j = 0; j < N - 1 - i; j++) {
  11. //比较 list[j] 和 list[j+1] 的大小
  12. if (list[j] > list[j + 1]) {
  13. //交换 2 个元素的位置
  14. temp = list[j];
  15. list[j] = list[j + 1];
  16. list[j + 1] = temp;
  17. }
  18. }
  19. }
  20. }
  21. int main() {
  22. int i = 0;
  23. int list[N] = { 14,33,27,35,10 };
  24. Bubble_sort(list);
  25. //输出已排好序的序列
  26. for (i = 0; i < N; i++) {
  27. printf("%d ", list[i]);
  28. }
  29. return 0;
  30. }


如下是用冒泡排序算法对 {14, 33, 27, 35, 10} 完成升序排序的 Java 程序:

  1. public class Demo {
  2. public static void Bubble_sort(int[] list) {
  3. int length = list.length;
  4. // length 个元素,遍历 length-1 次
  5. for (int i = 0; i < length-1; i++) {
  6. // 从第 1 个元素开始遍历,遍历至 length-1-i
  7. for (int j = 0; j < length - 1 - i; j++) {
  8. // 比较 list[j] 和 list[j++] 的大小
  9. if (list[j] > list[j + 1]) {
  10. // 交换 2 个元素的位置
  11. int temp = list[j];
  12. list[j] = list[j + 1];
  13. list[j + 1] = temp;
  14. }
  15. }
  16. }
  17. }
  18. public static void main(String[] args) {
  19. int[] list = { 14, 33, 27, 35, 10 };
  20. Bubble_sort(list);
  21. // 输出已排好序的序列
  22. for (int i = 0; i < list.length; i++) {
  23. System.out.print(list[i] + " ");
  24. }
  25. }
  26. }


如下是用冒泡排序算法对 {14, 33, 27, 35, 10} 完成升序排序的 Python 程序:

  1. #待排序序列
  2. list = [14,33,27,35,10]
  3. def Bubble_sort():
  4. #序列中有 n 个元素,就遍历 n-1 遍
  5. for i in range(len(list-1)):
  6. #从第 1 个元素开始遍历,比那里至 len(list)-1-i
  7. for j in range(len(list)-1-i):
  8. #比较两个相邻元素的大小
  9. if list[j] > list[j+1]:
  10. #交换 2 个元素的位置
  11. list[j],list[j+1] = list[j+1],list[j]
  12. Bubble_sort()
  13. for i in list:
  14. print(i,end=" ")

插入排序算法

插入排序算法的具体实现

实现插入排序算法的伪代码如下:

// list 为待排序序列
insertion_sort(list):
    // 从第 2 个元素开始遍历序列
    for i <- 2 to length(list):
        //记录要插入的目标元素
        insert_elem = list[i]
        //记录目标元素所在的位置
        position = i
        //从 position 所在位置向前遍历,直至找到一个比目标元素小的元素,目标元素插入到该元素之后的位置
        while position > 0 and list[position-1] > insert_elem:      // 此为升序排序,实现降序排序改为 list[position-1] < insert_elem
            //移动前一个元素的位置,将其向后移动一个位置
            list[position] = list[position-1]
            position = position - 1
        if(position != i):
            list[position] = insert_elem
    return list


结合伪代码,如下是用插入排序算法对 {14, 33, 27, 10, 35, 19, 42, 44} 实现升序排序的 C 语言程序:

  1. #include <stdio.h>
  2. #define MAX 8 //设定待排序序列中的元素个数
  3. //list[MAX]为待排序序列
  4. void insertion_sort(int list[MAX]) {
  5. int insert_elem;
  6. int position;
  7. int i;
  8. //从第 2 个元素(下标为 1)开始遍历
  9. for (i = 1; i < MAX; i++) {
  10. // 记录要插入的目标元素
  11. insert_elem = list[i];
  12. // 记录目标元素所在的位置,从此位置向前开始遍历
  13. position = i;
  14. // 从 position 向前遍历,找到目标元素的插入位置
  15. while (position > 0 && list[position - 1] > insert_elem) {
  16. //position 处的元素向后移动一个位置
  17. list[position] = list[position - 1];
  18. position--;
  19. }
  20. //将目标元素插入到指定的位置
  21. if (position != i) {
  22. list[position] = insert_elem;
  23. }
  24. }
  25. }
  26. int main() {
  27. int i;
  28. int list[MAX] = { 14, 33, 27, 10, 35, 19, 42, 44 };
  29. insertion_sort(list);
  30. //输出 list 数组中已排好序的序列
  31. for (i = 0; i < MAX; i++) {
  32. printf("%d ", list[i]);
  33. }
  34. }


如下是用插入排序算法对 {14, 33, 27, 10, 35, 19, 42, 44} 实现升序排序的 Java 程序:

  1. public class Demo {
  2. public static void insertion_sort(int[] list) {
  3. int length = list.length;
  4. // 从第 2 个元素(下标为 1)开始遍历
  5. for (int i = 1; i < length; i++) {
  6. // 记录要插入的目标元素
  7. int insert_elem = list[i];
  8. // 记录目标元素所在的位置,从此位置向前开始遍历
  9. int position = i;
  10. // 从 position 向前遍历,找到目标元素的插入位置
  11. while (position > 0 && list[position - 1] > insert_elem) {
  12. // position 处的元素向后移动一个位置
  13. list[position] = list[position - 1];
  14. position--;
  15. }
  16. // 将目标元素插入到指定的位置
  17. if (position != i) {
  18. list[position] = insert_elem;
  19. }
  20. }
  21. }
  22. public static void main(String[] args) {
  23. int[] list = { 10, 14, 19, 27, 33, 35, 42, 44 };
  24. insertion_sort(list);
  25. // 输出已排好序的序列
  26. for (int i = 0; i < list.length; i++) {
  27. System.out.print(list[i] + " ");
  28. }
  29. }
  30. }


如下是用插入排序算法对 {14, 33, 27, 10, 35, 19, 42, 44} 实现升序排序的 Python 程序:

  1. #待排序序列
  2. list = [10, 14, 19, 27, 33, 35, 42, 44]
  3. def insertion_sort():
  4. length = len(list)
  5. # 从第 2 个元素(下标为 1)开始遍历
  6. for i in range(1,length):
  7. # 记录要插入的目标元素
  8. insert_elem = list[i];
  9. # 记录目标元素所在的位置,从此位置向前开始遍历
  10. position = i
  11. # 从 position 向前遍历,找到目标元素的插入位置
  12. while position > i and list[position - 1] > insert_elem:
  13. # position 处的元素向后移动一个位置
  14. list[position] = list[position - 1]
  15. position = position - 1
  16. # 将目标元素插入到指定的位置
  17. if position != i:
  18. list[position] = insert_elem
  19. insertion_sort()
  20. # 输出已排好序的序列
  21. for i in list:
  22. print(i,end=" ")


以上程序的输出结果均为:

10 14 19 27 33 35 42 44 

选择排序算法

选择排序算法的具体实现

如下是描述选择排序算法的伪代码:

selection_sort(list):   //list 为待排序序列
    n <- length(list)   //记录序列中的元素个数
    for i <- 1 to n-1:  //从第 1 个元素一直遍历至倒数第 2 个元素
        min <- i        //初始化最小值为第 i 个元素
        for j <- i+1 to n: // 从第 i+1 个元素开始开始遍历序列
            if list[j] < list[min]:  //查找待排序序列中的最小值
                min = j
        if min != i:     //如果最小值所在的位置不为 i,交换最小值和第 i 个元素的位置
            swap list[min] , list[i]
    return list


结合伪代码,如下为使用选择排序算法对 {14, 33, 27, 10, 35, 19, 42, 44} 实现升序排序的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 8 //设定待排序序列中的元素个数
  3. //list[N] 为存储待排序序列的数组
  4. void selection_sort(int list[N]) {
  5. int i, j;
  6. int min,temp;
  7. //从第 1 个元素开始遍历,直至倒数第 2 个元素
  8. for (i = 0; i < N-1; i++) {
  9. min = i; //事先假设最小值为第 i 个元素
  10. //从第 i+1 个元素开始遍历,查找真正的最小值
  11. for (j = i + 1; j < N; j++) {
  12. if (list[j] < list[min]) {
  13. min = j;
  14. }
  15. }
  16. //如果最小值所在位置不为 i,交换最小值和第 i 个元素的位置
  17. if (min != j) {
  18. temp = list[min];
  19. list[min] = list[i];
  20. list[i] = temp;
  21. }
  22. }
  23. }
  24. int main() {
  25. int i;
  26. int list[N] = { 14,33,27,10,35,19,42,44 };
  27. //对待排序序列做选择排序
  28. selection_sort(list);
  29. //输出已排序序列中的各个元素
  30. for (i = 0; i < N; i++) {
  31. printf("%d ", list[i]);
  32. }
  33. }


如下为使用选择排序算法对 {14, 33, 27, 10, 35, 19, 42, 44} 实现升序排序的 Java 程序:

  1. public class Demo {
  2. // list[N] 为存储待排序序列的数组
  3. public static void selection_sort(int[] list) {
  4. int length = list.length;
  5. int i, j;
  6. // 从第 1 个元素开始遍历,直至倒数第 2 个元素
  7. for (i = 0; i < length - 1; i++) {
  8. int min = i; // 事先假设最小值为第 i 个元素
  9. // 从第 i+1 个元素开始遍历,查找真正的最小值
  10. for (j = i + 1; j < length; j++) {
  11. if (list[j] < list[min]) {
  12. min = j;
  13. }
  14. }
  15. // 如果最小值所在位置不为 i,交换最小值和第 i 个元素的位置
  16. if (min != j) {
  17. int temp = list[min];
  18. list[min] = list[i];
  19. list[i] = temp;
  20. }
  21. }
  22. }
  23. public static void main(String[] args) {
  24. int[] list = { 14, 33, 27, 10, 35, 19, 42, 44 };
  25. selection_sort(list);
  26. // 输出已排好序的序列
  27. for (int i = 0; i < list.length; i++) {
  28. System.out.print(list[i] + " ");
  29. }
  30. }
  31. }


如下为使用选择排序算法对 {14, 33, 27, 10, 35, 19, 42, 44} 实现升序排序的 Python 程序:

  1. #待排序序列
  2. list = [14,33,27,10,35,19,42,44]
  3. def selection_sort():
  4. length = len(list)
  5. #从第 1 个元素开始遍历,直至倒数第 2 个元素
  6. for i in range(length-1):
  7. min = i #事先假设最小值为第 i 个元素
  8. #从第 i+1 个元素开始遍历,查找真正的最小值
  9. for j in range(i+1,length):
  10. if list[j] < list[min]:
  11. min = j
  12. #如果最小值所在位置不为 i,交换最小值和第 i 个元素的位置
  13. if min != j:
  14. list[min],list[i] = list[i],list[min]
  15. selection_sort()
  16. # 输出已排好序的序列
  17. for i in list:
  18. print(i,end=" ")


以上程序的输出结果均为:

10 14 19 27 33 35 42 44

希尔排序算法

序列的划分方法

待排序序列如何进行划分,划分多少次,都会影响到希尔排序算法的执行效率。

希尔排序算法没有固定的划分标准,这里给大家推荐一种常用的方法,套用如下伪代码:

输入 list           //输入待排序序列
interval <- 1    // 初始值为 1
while interval < length(list) / 3:    // length(list) 表示待排序序列的长度
    interval = interval * 3 + 1

经过计算得出的 interval 的值,就是首次划分序列采用的标准。

后续划分整个序列,套用如下公式:

interval = (interval-1)/3

比如说计算第二次划分序列的标准,只需将第一次划分序列时计算得到的 interval 代入公式,求出的新 interval 值就是第二次采用的划分标准。

希尔排序算法的具体实现

实现希尔排序算法的伪代码如下:

// list 为待排序序列
shell_sort(list):
    len <- length(list)  // 记录 list 序列中的元素个数
    //初始化间隔数为 1
    interval <- 1
    //计算最大间隔数
    while interval < len/3:
        interval <- interval * 3 + 1
    //根据间隔数,不断划分序列,并对各子序列排序
    while interval > 0:
        //对各个子序列做直接插入排序
        for i <- interval to len:
            temp <- list[i]
            j <- i
            while j > interval - 1 && list[j - interval] ≥ temp:
                list[j] <- list[j - interval]
                j <- j - interval
            if j != i:
                list[j] <- temp
        //计算新的间隔数,继续划分序列
        interval <- (interval - 1)/3
return  list


结合伪代码,如下是用希尔排序算法对 {35, 33, 42, 10, 14, 19, 27, 44} 实现升序排序的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 8 //设定待排序序列中的元素个数
  3. //list[N] 为存储待排序序列的数组
  4. void shell_sort(int list[N]) {
  5. int temp, i, j;
  6. //初始化间隔数为 1
  7. int interval = 1;
  8. //计算最大间隔
  9. while (interval < N / 3) {
  10. interval = interval * 3 + 1;
  11. }
  12. //根据间隔数,不断划分序列,并对各子序列排序
  13. while (interval > 0) {
  14. //对各个子序列做直接插入排序
  15. for (i = interval; i < N; i++) {
  16. temp = list[i];
  17. j = i;
  18. while (j > interval - 1 && list[j - interval] >= temp) {
  19. list[j] = list[j - interval];
  20. j -= interval;
  21. }
  22. if(j != i){
  23. list[j] = temp;
  24. }
  25. }
  26. //计算新的间隔数,继续划分序列
  27. interval = (interval - 1) / 3;
  28. }
  29. }
  30. int main() {
  31. int i;
  32. int list[N] = { 35,33,42,10,14,19,27,44 };
  33. //对待排序序列做希尔排序
  34. shell_sort(list);
  35. //输出已排序序列
  36. for (i = 0; i < N; i++) {
  37. printf("%d ", list[i]);
  38. }
  39. }


如下是用希尔排序算法对 {35, 33, 42, 10, 14, 19, 27, 44} 实现升序排序的 Java 程序:

  1. public class Demo {
  2. // list[N] 为存储待排序序列的数组
  3. public static void shell_sort(int[] list) {
  4. int length = list.length;
  5. // 初始化间隔数为 1
  6. int interval = 1;
  7. // 计算最大间隔
  8. while (interval < length / 3) {
  9. interval = interval * 3 + 1;
  10. }
  11. // 根据间隔数,不断划分序列,并对各子序列排序
  12. while (interval > 0) {
  13. // 对各个子序列做直接插入排序
  14. for (int i = interval; i < length; i++) {
  15. int temp = list[i];
  16. int j = i;
  17. while (j > interval - 1 && list[j - interval] >= temp) {
  18. list[j] = list[j - interval];
  19. j -= interval;
  20. }
  21. if (j != i) {
  22. list[j] = temp;
  23. }
  24. }
  25. // 计算新的间隔数,继续划分序列
  26. interval = (interval - 1) / 3;
  27. }
  28. }
  29. public static void main(String[] args) {
  30. int[] list = { 35, 33, 42, 10, 14, 19, 27, 44 };
  31. shell_sort(list);
  32. // 输出已排好序的序列
  33. for (int i = 0; i < list.length; i++) {
  34. System.out.print(list[i] + " ");
  35. }
  36. }
  37. }


如下是用希尔排序算法对 {35, 33, 42, 10, 14, 19, 27, 44} 实现升序排序的 Python 程序:

  1. #待排序序列
  2. list = [35,33,42,10,14,19,27,44]
  3. def shell_sort():
  4. length = len(list)
  5. # 初始化间隔数为 1
  6. interval = 1
  7. # 计算最大间隔
  8. while interval < (int)(length / 3):
  9. interval = interval * 3 + 1
  10. # 根据间隔数,不断划分序列,并对各子序列排序
  11. while interval > 0:
  12. # 对各个子序列做直接插入排序
  13. for i in range(interval , length):
  14. temp = list[i]
  15. j = i
  16. while j > interval - 1 and list[j - interval] >= temp:
  17. list[j] = list[j - interval]
  18. j = j - interval
  19. if j != i:
  20. list[j] = temp
  21. # 计算新的间隔数,继续划分序列
  22. interval = (int)((interval - 1)/3)
  23. # 对待排序序列做希尔排序
  24. shell_sort()
  25. # 输出已排好序的序列
  26. for i in list:
  27. print(i,end=" ")


以上程序的输出结果均为:

10 14 19 27 33 35 42 44

归并排序算法

归并排序算法的具体实现

对比图 1 和图 2 很容易联想到,归并排序算法可以借助递归的思想实现,对应的伪代码如下:

输入 arr[n]                                // 输入要排序的序列
merge_sort(arr[n] , p , q):                // [p , q] 表示对第 p ~ q 区域内的元素进行归并排序
    if p < q :                             // 对 [p , q] 区域不断采用对半分割的方式,最终将整个区域划分为多个仅包含 1 个元素(p==q)的序列
        mid = ⌊(p+q)/2⌋
        merge_sort(arr , p , mid)
        merge_sort(arr , mid+1 , q)
        merge(arr , p , mid , q)          // 调用实现归并过程的代码模块

merge_sort() 用于将整个序列分割成多个子序列,merge() 用来合并这些子序列,合并的实现方式为:

  1. 从 [p, mid] 和 [mid+1, q] 两个区域的元素分别拷贝到 leftarr 和 rightarr 区域。
  2. 从 leftarr 和 rightarr 区域中各个取出第一个元素,比较它们的大小;
  3. 将较小的元素拷贝到 [p, q] 区域,然后从较小元素所在的区域内取出下一个元素,继续进行比较;
  4. 重复执行第 3 步,直至 leftarr 和 rightarr 内的元素全部拷贝到 [p, q] 为止。如果 leftarr 或者 rightarr 有一方为空,则直接将另一方的所有元素依次拷贝到 [p, q] 区域。


对应的伪代码如下:

merge(arr[n] , p , mid , q):                          // 该算法表示将 [p , mid] 和 [mid+1 , q] 做归并操作
    leftnum <- mid - p + 1                            // 统计 [p , mid] 区域内的元素个数
    rightnum <- q - mid                               // 统计 [mid+1 , q] 区域内的元素个数
    leftarr[leftnum] <- arr[p ... mid]                // 分别将两个区域内的元素各自拷贝到另外两个数组中
    rightarr[rightnum] <- arr[mid+1 ... q]
    i <- 1 , j <- 1
    for k <- p to q :             // 从 leftarr 和 rightarr 数组中第 1 个元素开始,比较它们的大小,将较小的元素拷贝到 arr 数组的 [p , q] 区域
        if leftarr[i] ≤ rightarr[j] :
            arr[k] = leftarr[i]
            i <- i+1
        else :
            arr[k] = right[j]
            j <- j+1


结合伪代码,如下是用归并排序算法对 {7, 5, 2, 4, 1, 6, 3, 0} 进行升序排序的 C 语言程序:

  1. #include <stdio.h>
  2. //实现分割操作的函数
  3. void merge_sort(int* arr, int p, int q);
  4. //实现归并操作的函数
  5. void merge(int* arr, int p, int mid, int q);
  6. int main() {
  7. int i = 0;
  8. int arr[8] = { 7,5,2,4,1,6,3,0 };
  9. //对 arr 数组中第 1 至 8 个元素进行归并排序
  10. merge_sort(arr, 1, 8);
  11. while (i < 8)
  12. {
  13. printf("%d ", arr[i]);
  14. i++;
  15. }
  16. return 0;
  17. }
  18. //实现分割操作的函数,[p,q] 用于指定归并排序的区域范围,
  19. void merge_sort(int* arr, int p, int q) {
  20. int mid;
  21. if (arr == NULL || p > q || p == q) {
  22. return ;
  23. }
  24. mid = (p + q) / 2;
  25. //将 [p,q] 分为[p,mid] 和 [mid+1,q] 区域
  26. merge_sort(arr, p, mid);
  27. merge_sort(arr, mid + 1, q);
  28. //对分好的 [p,mid] 和 [mid,q] 进行归并操作
  29. merge(arr, p, mid, q);
  30. }
  31. //实现归并操作的函数,归并的 2 个区域分别为 [p,mid] 和 [mid+1,q]
  32. void merge(int* arr, int p, int mid, int q) {
  33. int i,j,k;
  34. int leftarr[100], rightarr[100];
  35. int numL = mid - p + 1;
  36. int numR = q - mid;
  37. //将 arr 数组中 [p,mid] 区域内的元素逐一拷贝到 leftarr 数组中
  38. for (i = 0; i < numL; i++) {
  39. leftarr[i] = arr[p - 1 + i];
  40. }
  41. //将 arr 数组中 [mid+1,q] 区域内的元素逐一拷贝到 rightarr 数组中
  42. leftarr[i] = 2147483647;
  43. for (i = 0; i < numR; i++) {
  44. rightarr[i] = arr[mid + i];
  45. }
  46. rightarr[i] = 2147483647;
  47. i = 0;
  48. j = 0;
  49. //逐一比较 leftarr 和 rightarr 中的元素,每次将较小的元素拷贝到 arr 数组中的 [p,q] 区域内
  50. for (k = p; k <= q; k++) {
  51. if (leftarr[i] <= rightarr[j]) {
  52. arr[k - 1] = leftarr[i];
  53. i++;
  54. }
  55. else {
  56. arr[k - 1] = rightarr[j];
  57. j++;
  58. }
  59. }
  60. }


如下是用归并排序算法对 {7, 5, 2, 4, 1, 6, 3, 0} 进行升序排序的 Java 程序:

  1. public class Demo {
  2. //实现归并排序算法的分割操作
  3. public static void merge_sort(int[] arr, int p, int q) {
  4. // 如果数组不存在或者 [p.q] 区域不合理
  5. if (arr == null || p >= q) {
  6. return;
  7. }
  8. //对[p,q]区域进行分割
  9. int mid = (p + q) / 2;
  10. merge_sort(arr, p, mid);
  11. merge_sort(arr, mid + 1, q);
  12. //对分割的 [p,mid] 和 [mid+1,q] 区域进行归并
  13. merge(arr, p, mid, q);
  14. }
  15. //实现归并排序算法的归并操作
  16. public static void merge(int[] arr, int p, int mid, int q) {
  17. int numL = mid - p + 1;
  18. int numR = q - mid;
  19. //创建 2 个数组,分别存储 [p,mid] 和 [mid+1,q]区域内的元素
  20. int[] leftarr = new int[numL + 1];
  21. int[] rightarr = new int[numR + 1];
  22. int i;
  23. for (i = 0; i < numL; i++) {
  24. leftarr[i] = arr[p - 1 + i];
  25. }
  26. //将 leftarr 数组中最后一个元素设置为足够大的数。
  27. leftarr[i] = 2147483647;
  28. for (i = 0; i < numR; i++) {
  29. rightarr[i] = arr[mid + i];
  30. }
  31. //将 rightarr 数组中最后一个元素设置为足够大的数。
  32. rightarr[i] = 2147483647;
  33. int j = 0;
  34. i = 0;
  35. //对 leftarr 和 rightarr 数组中存储的 2 个区域的元素做归并操作
  36. for (int k = p; k <= q; k++) {
  37. if (leftarr[i] <= rightarr[j]) {
  38. arr[k - 1] = leftarr[i];
  39. i++;
  40. } else {
  41. arr[k - 1] = rightarr[j];
  42. j++;
  43. }
  44. }
  45. }
  46. public static void main(String[] args) {
  47. int[] arr = new int[] { 7, 5, 2, 4, 1, 6, 3, 0 };
  48. //对 arr 数组中第 1 至 8 个元素进行归并排序
  49. merge_sort(arr, 1, 8);
  50. for (int i : arr) {
  51. System.out.print(i + " ");
  52. }
  53. }
  54. }


如下是用归并排序算法对 {7, 5, 2, 4, 1, 6, 3, 0} 进行升序排序的 Python 程序:

  1. #实现归并排序算法中的分割操作,[p,q]为指定分割区域
  2. def merge_sort(arr,p,q):
  3. #列表中没有数据,或者 [p,q]区域不存在
  4. if len(arr) == 1 or p >= q:
  5. return
  6. #对 [p,q] 区域进行分割
  7. mid = int( (p + q) / 2 )
  8. merge_sort(arr,p,mid)
  9. merge_sort(arr,mid+1,q)
  10. #归并 [p,mid] 和 [mid+1,q] 区域
  11. merge(arr,p,mid,q)
  12. #实现归并排序算法中的归并操作,归并区域为 [p.mid] 和 [mid+1,q]
  13. def merge(arr,p,mid,q):
  14. numL = mid - p + 1;
  15. numR = q - mid;
  16. #分别将 [p,mid] 和 [mid+1,q] 区域内的元素拷贝到 leftarr 和 rightarr 列表中
  17. leftarr = arr[p-1:p+numL-1]
  18. rightarr = arr[mid:mid+numR]
  19. # 2 个列表末尾添加一个足够大的数
  20. leftarr.append(float('inf'))
  21. rightarr.append(float('inf'))
  22. i=0
  23. j=0
  24. k=p
  25. #逐个比较 leftarr 和 rightarr 列表中的元素,每次将较小的元素添加到 arr 列表中的 [p,q] 区域内
  26. while k <= q:
  27. if leftarr[i] <= rightarr[j]:
  28. arr[k-1] = leftarr[i]
  29. i = i + 1
  30. else:
  31. arr[k-1] = rightarr[j]
  32. j = j + 1
  33. k = k + 1
  34. arr = [7, 5, 2, 4, 1, 6, 3, 0]
  35. #对 arr 数组中第 1 至 8 个元素做归并排序操作
  36. merge_sort(arr, 1, 8)
  37. print(arr)


以上程序的输出结果均为:

0 1 2 3 4 5 6 7

快速排序算法

可以看到,最终元素 31 左侧的元素都比它小,右侧的元素都比它大。如下是实现 partition() 函数的伪代码:

partition(arr[] , p , q):              // [p , q] 为要分割的区域
    lo <- p                            // lo、hi 准备遍历 [p , q-1] 区域
    hi <- q-1
    pivot <- arr[q]                    // 以 [p , q] 区域中最后一个元素作为中间值
    while true:                       // 一直循环,直到执行 end while
        while arr[lo] < pivot:         // lo 从左往右遍历,直至找到一个不小于 pivot 的元素
            lo <- lo+1
        while hi>0 and arr[hi] > pivot:   // hi 从右往左遍历,直至找到一个不小于 pivot 的元素
            hi <- hi-1
        if lo ≥ hi:                      // 如果 lo 大于等于 hi,退出循环
            end while
        else:
            swap arr[lo] , arr[hi]        // 交换 arr[lo] 和 arr[hi] 的值
            lo <- lo+1                    // 分别将 lo 和 hi 向前移动一步,准备遍历后续的元素
            hi <- hi-1
    swap arr[lo] , arr[q]                 // 跳出循环后,交换 arr[lo] 和 arr[q] 的值
    return lo                             // 返回 lo 的值,也就是中间值所在序列中的位置

快速排序算法的实现

结合伪代码,如下是用快速排序算法对 {35, 33, 42, 10, 14, 19, 27, 44, 26, 31} 完成升序排序的 C 语言程序:

  1. #include <stdio.h>
  2. // arr 为待排序数组,[p,q] 用于指定排序区域
  3. int partition(int* arr, int p, int q) {
  4. int temp = 0;
  5. // lo、hi分别表示指向首个元素和倒数第 2 个元素的指针
  6. int lo = p;
  7. int hi = q - 1;
  8. //pivot 表示选中的中间值
  9. int pivot = arr[q];
  10. while (1)
  11. {
  12. //lo从左往右遍历,直至找到一个不小于 pivot 的元素
  13. while (arr[lo] < pivot) {
  14. lo++;
  15. };
  16. //hi从右往左遍历,直至找到一个不大于 pivot 的元素
  17. while (hi > 0 && arr[hi] > pivot) {
  18. hi--;
  19. }
  20. //如果 lo≥hi,退出循环
  21. if (lo >= hi)
  22. {
  23. break;
  24. }
  25. else {
  26. //交换 arr[lo] 和 arr[hi] 的值
  27. temp = arr[lo];
  28. arr[lo] = arr[hi];
  29. arr[hi] = temp;
  30. // lo 和 hi 都向前移动一个位置,准备继续遍历
  31. lo++;
  32. hi--;
  33. }
  34. }
  35. //交换 arr[lo] 和 arr[q] 的值
  36. temp = arr[lo];
  37. arr[lo] = pivot;
  38. arr[q] = temp;
  39. //返回中间值所在序列中的位置
  40. return lo;
  41. }
  42. void quick_sort(int* arr, int p, int q) {
  43. int par;
  44. //如果待排序序列不存在,或者仅包含 1 个元素,则不再进行分割
  45. if (q - p <= 0) {
  46. return;
  47. }
  48. else {
  49. //调用 partition() 函数,分割 [p,q] 区域
  50. par = partition(arr, p, q);
  51. //以 [p,par-1]作为新的待排序序列,继续分割
  52. quick_sort(arr, p, par - 1);
  53. //以[par+1,q]作为新的待排序序列,继续分割
  54. quick_sort(arr, par + 1, q);
  55. }
  56. }
  57. int main()
  58. {
  59. int i = 0;
  60. int arr[10] = { 35,33,42,10,14,19,27,44,26,31 };
  61. //对于 arr 数组中所有元素进行快速排序
  62. quick_sort(arr, 0, 9);
  63. for (; i < 10; i++) {
  64. printf("%d ", arr[i]);
  65. }
  66. return 0;
  67. }


如下是用快速排序算法对 {35, 33, 42, 10, 14, 19, 27, 44, 26, 31} 完成升序排序的 Java 程序:

  1. public class Demo {
  2. public static int partition(int[] arr, int p, int q) {
  3. int temp = 0;
  4. // lo、hi分别表示指向首个元素和倒数第 2 个元素的指针
  5. int lo = p;
  6. int hi = q - 1;
  7. // pivot 表示选中的中间值
  8. int pivot = arr[q];
  9. while (true) {
  10. // lo从左往右遍历,直至找到一个不小于 pivot 的元素
  11. while (arr[lo] < pivot) {
  12. lo++;
  13. }
  14. // hi从右往左遍历,直至找到一个不大于 pivot 的元素
  15. while (hi > 0 && arr[hi] > pivot) {
  16. hi--;
  17. }
  18. // 如果 lo≥hi,退出循环
  19. if (lo >= hi) {
  20. break;
  21. } else {
  22. // 交换 arr[lo] 和 arr[hi] 的值
  23. temp = arr[lo];
  24. arr[lo] = arr[hi];
  25. arr[hi] = temp;
  26. // lo 和 hi 都向前移动一个位置,准备继续遍历
  27. lo++;
  28. hi--;
  29. }
  30. }
  31. // 交换 arr[lo] 和 arr[q] 的值
  32. temp = arr[lo];
  33. arr[lo] = pivot;
  34. arr[q] = temp;
  35. // 返回中间值所在序列中的位置
  36. return lo;
  37. }
  38. public static void quick_sort(int[] arr, int p, int q) {
  39. //如果待排序序列不存在,或者仅包含 1 个元素,则不再进行分割
  40. if (q - p <= 0) {
  41. return;
  42. } else {
  43. //调用 partition() 函数,分割 [p,q] 区域
  44. int par = partition(arr, p, q);
  45. //以 [p,par-1]作为新的待排序序列,继续分割
  46. quick_sort(arr, p, par - 1);
  47. //以[par+1,q]作为新的待排序序列,继续分割
  48. quick_sort(arr, par + 1, q);
  49. }
  50. }
  51. public static void main(String[] args) {
  52. int[] arr = new int[] { 35, 33, 42, 10, 14, 19, 27, 44, 26, 31 };
  53. // 对于 arr 数组中所有元素进行快速排序
  54. quick_sort(arr, 0, 9);
  55. for (int i = 0; i < arr.length; i++) {
  56. System.out.print(arr[i]+" ");
  57. }
  58. }
  59. }


如下是用快速排序算法对 {35, 33, 42, 10, 14, 19, 27, 44, 26, 31} 完成升序排序的 Python 程序:

  1. def partition(arr,p,q):
  2. #lo、hi分别表示指向首个元素和倒数第 2 个元素的索引
  3. lo = p
  4. hi = q-1
  5. #pivot 表示选中的中间值
  6. pivot = arr[q]
  7. while True:
  8. #lo从左往右遍历,直至找到一个不小于 pivot 的元素
  9. while arr[lo] < pivot:
  10. lo = lo + 1
  11. #hi从右往左遍历,直至找到一个不大于 pivot 的元素
  12. while hi > 0 and arr[hi] > pivot:
  13. hi = hi - 1
  14. #如果 lo≥hi,退出循环
  15. if lo >= hi:
  16. break
  17. else:
  18. #交换 arr[lo] 和 arr[hi] 的值
  19. arr[lo],arr[hi] = arr[hi],arr[lo]
  20. #lo 和 hi 都向前移动一个位置,准备继续遍历
  21. lo = lo + 1
  22. hi = hi - 1
  23. #交换 arr[lo] 和 arr[q] 的值
  24. arr[lo],arr[q] = arr[q],arr[lo]
  25. #返回中间值所在序列中的位置
  26. return lo
  27. def quick_sort(arr,p,q):
  28. #如果待排序序列不存在,或者仅包含 1 个元素,则不再进行分割
  29. if q - p <= 0:
  30. return
  31. #调用 partition() 函数,分割 [p,q] 区域
  32. par = partition(arr,p,q)
  33. #以 [p,par-1]作为新的待排序序列,继续分割
  34. quick_sort(arr,p,par-1)
  35. #以[par+1,q]作为新的待排序序列,继续分割
  36. quick_sort(arr,par+1,q)
  37. arr=[35,33,42,10,14,19,27,44,26,31]
  38. #对于 arr 列表中所有元素进行快速排序
  39. quick_sort(arr,0,9)
  40. print(arr)


以上程序的输出结果均为:

10 14 19 26 27 31 33 35 42 44

计数排序算法

计数排序算法的具体实现

实现计数排序算法的伪代码如下:

//计数排序算法,list 为待排序序列
countingSort(list)
    size <- len(list)      // 获取 list 序列中的元素个数=
    max <- getMax(list)    // 找到 list 序列中的最大值
    array[0...max+1] <- 0    // 定义一个长度为 max+1 的数组,
    for j <- 0 to size      // 创建 array[max+1] 并统计各个元素的出现次数
        array[list[j]] <- array[list[j]] + 1
    for i <- 1 to max       // 对 array[max+1] 存储的元素做累加操作
        array[i] <- array[i] + array[i - 1];
    for j <- size to 0 // 根据 array[max+1] 中的累加值,找到各个元素排序后的具体位置
        output[array[list[i]] - 1] = list[i]; // output存储有序序列
        array[list[i]] <- array[list[i]] - 1  // 确定一个元素的位置后,array[max+1] 中相应位置的数值要减 1
    return output[size]


结合伪代码,如下是采用计数排序算法对 {4, 2, 2, 8, 3, 3, 1} 进行升序排序的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 7 //待排序序列中的元素个数
  3. #define MAX 100 //待排序序列中的最大值不能超过 100
  4. //找到数组中的最大值
  5. int getMax(int list[]) {
  6. int i, max = list[0];
  7. for (i = 1; i < N; i++) {
  8. if (list[i] > max)
  9. max = list[i];
  10. }
  11. return max;
  12. }
  13. void countingSort(int list[]) {
  14. int i;
  15. //第 1 步,找到序列中的最大值
  16. int max = getMax(list);
  17. //第 2 步,创建一个数组,长度至少为 max+1,并初始化为 0
  18. int array[MAX] = { 0 };
  19. int output[N] = { 0 };
  20. //第 3 步,统计各个元素的出现次数,并存储在相应的位置上
  21. for (i = 0; i < N; i++) {
  22. array[list[i]]++;
  23. }
  24. //第 4 步,累加 array 数组中的出现次数
  25. for (i = 1; i <= max; i++) {
  26. array[i] += array[i - 1];
  27. }
  28. //第 5 步,根据 array 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
  29. for (i = N - 1; i >= 0; i--) {
  30. output[array[list[i]] - 1] = list[i];
  31. //第 6 步,数组相应位置上的值减1
  32. array[list[i]]--;
  33. }
  34. // 将 output 数组中的数据原封不动地拷贝到 list 数组中
  35. for (i = 0; i < N; i++) {
  36. list[i] = output[i];
  37. }
  38. }
  39. void printlist(int list[]) {
  40. int i;
  41. for (i = 0; i < N; ++i) {
  42. printf("%d ", list[i]);
  43. }
  44. }
  45. int main() {
  46. int list[] = { 4, 2, 2, 8, 3, 3, 1 };
  47. //进行计数排序
  48. countingSort(list);
  49. printlist(list);
  50. }


如下是采用计数排序算法对 {4, 2, 2, 8, 3, 3, 1} 进行升序排序的 Java 程序:

  1. public class Demo {
  2. //找到数组中的最大值
  3. public static int getMax(int[] list) {
  4. int max = list[0];
  5. for (int i = 1; i < list.length; i++) {
  6. if (list[i] > max) {
  7. max = list[i];
  8. }
  9. }
  10. return max;
  11. }
  12. public static void countingSort(int[] list) {
  13. int length = list.length;
  14. //第 1 步,找到序列中的最大值
  15. int max = getMax(list);
  16. //第 2 步,初始化一个 array[max+1]
  17. int[] array = new int[max + 1];
  18. int[] output = new int[length];
  19. //第 3 步,统计各个元素的出现次数,并存储在相应的位置上
  20. for (int i = 0; i < length; i++) {
  21. array[list[i]]++;
  22. }
  23. // 第 4 步,累加 array 数组中的出现次数
  24. for (int i = 1; i <= max; i++) {
  25. array[i] += array[i - 1];
  26. }
  27. // 第 5 步,根据 array 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
  28. for (int i = length - 1; i >= 0; i--) {
  29. output[array[list[i]] - 1] = list[i];
  30. array[list[i]]--;
  31. }
  32. // 将 output 数组中的数据原封不动地拷贝到 list 数组中
  33. for (int i = 0; i < length; i++) {
  34. list[i] = output[i];
  35. }
  36. }
  37. public static void printList(int[] list) {
  38. for (int i = 0; i < list.length; i++) {
  39. System.out.print(list[i] + " ");
  40. }
  41. }
  42. public static void main(String[] args) {
  43. // 待排序序列
  44. int[] list = new int[] { 4, 2, 2, 8, 3, 3, 1 };
  45. //进行计数排序
  46. countingSort(list);
  47. printList(list);
  48. }
  49. }


如下是采用计数排序算法对 {4, 2, 2, 8, 3, 3, 1} 进行升序排序的 Python 程序:

  1. list = [4, 2, 2, 8, 3, 3, 1]
  2. length = len(list)
  3. #找到数组中的最大值
  4. def getMax(list):
  5. max = list[0]
  6. for i in range(1,length):
  7. if list[i] > max:
  8. max = list[i]
  9. return max
  10. #实现计数排序算法
  11. def countingSort(list):
  12. #第 1 步,找到序列中的最大值
  13. max = getMax(list)
  14. #第 2 步,初始化一个 array[max+1]
  15. array = [0]*(max+1)
  16. output = [0]*length
  17. #第 3 步,统计各个元素的出现次数,并存储在相应的位置上
  18. for i in range(length):
  19. array[list[i]] = array[list[i]]+1
  20. #第 4 步,累加 array 数组中的出现次数
  21. for i in range(1,max+1):
  22. array[i] = array[i] + array[i-1]
  23. #第 5 步,根据 array 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
  24. for i in range(length):
  25. output[array[list[i]]-1] = list[i];
  26. array[list[i]] = array[list[i]]-1;
  27. #将 output 数组中的数据原封不动地拷贝到 list 数组中
  28. for i in range(length):
  29. list[i] = output[i];
  30. def printlist(list):
  31. for i in range(length):
  32. print(list[i],end=' ')
  33. countingSort(list)
  34. printlist(list)


以上程序的输出结果均为:

1 2 2 3 3 4 8 

基数排序算法

基数排序算法的代码实现

如下是实现基数排序算法的伪代码:

//基数排序算法,array 为待排序序列
radixSort(array):
    max = getMax(array)   // 查找 array 序列中的最大值
    place <- 1            // 默认从个位开始排序
    while max/place > 0 : // 将最大值的位数作为循环次数
        countingSort(array, place)  // 调用计数排序算法,根据所选数位对各个元素进行排序
        place = place * 10

//计数排序算法,array 为待排序序列,place 指排序所依照的数位
countingSort(array, place)
    size <- len(array)       // 获取 array 序列中的元素个数
    // 根据 place,找到相应数位值最大的元素
    max <- (array[0] / place) % 10    
    for i <- 1 to size:
        if (array[i] / place) % 10 > max:
            max <- array[i]
    // 创建 count[max+1],统计各个元素的出现次数
    for j <- 0 to size     
        count[(array[i] / place) % 10] <- count[(array[i] / place) % 10] + 1
    // 对 count[max+1] 存储的元素做累加操作
    for i <- 1 to max
        count[i] <- count[i] + count[i - 1];
    // 根据 count[max+1] 中的累加值,找到各个元素排序后的具体位置
    for j <- size down to 0
        output[count[(array[i] / place) % 10] - 1] <- array[i];
        // 确定一个元素的位置后,count[max+1] 中相应位置的数值要减 1
        count[(array[i] / place) % 10] <- count[(array[i] / place) % 10] - 1 
    return output[size]


结合伪代码,如下是采用基数排序算法对 {121, 432, 564, 23, 1, 45, 788} 进行升序排序的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 7
  3. #define MAX 100 //限制各个元素各数位上的值不能超过 100
  4. //计数排序算法,place 表示以指定数位为准,对序列中的元素进行排序
  5. void countingSort(int array[], int place) {
  6. int i, output[N];
  7. //初始化一个数组,继续各个元素的出现次数
  8. int count[MAX] = { 0 };
  9. //假设第一个元素指定数位上的值最大
  10. int max = (array[0] / place) % 10;
  11. //找到真正数位上值最大的元素
  12. for (i = 1; i < N; i++) {
  13. if (((array[i] / place) % 10) > max)
  14. max = array[i];
  15. }
  16. //统计各个元素出现的次数
  17. for (i = 0; i < N; i++)
  18. count[(array[i] / place) % 10]++;
  19. //累加 count 数组中的出现次数
  20. for (i = 1; i < 10; i++)
  21. count[i] += count[i - 1];
  22. //根据 count 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
  23. for (i = N - 1; i >= 0; i--) {
  24. output[count[(array[i] / place) % 10] - 1] = array[i];
  25. count[(array[i] / place) % 10]--;
  26. }
  27. //将 output 数组中的数据原封不动地拷贝到 array 数组中
  28. for (i = 0; i < N; i++)
  29. array[i] = output[i];
  30. }
  31. //找到整个序列中的最大值
  32. int getMax(int array[]) {
  33. int i, max = array[0];
  34. for (i = 1; i < N; i++)
  35. if (array[i] > max)
  36. max = array[i];
  37. return max;
  38. }
  39. //基数排序算法
  40. void radixSort(int array[]) {
  41. //找到序列中的最大值
  42. int place, max = getMax(array);
  43. //根据最大值具有的位数,从低位依次调用计数排序算法
  44. for (place = 1; max / place > 0; place *= 10)
  45. countingSort(array, place);
  46. }
  47. //输出 array 数组中的数据
  48. void printArray(int array[]) {
  49. int i;
  50. for (i = 0; i < N; ++i) {
  51. printf("%d ", array[i]);
  52. }
  53. }
  54. int main() {
  55. int array[N] = { 121, 432, 564, 23, 1, 45, 788 };
  56. radixSort(array);
  57. printArray(array);
  58. }


如下是采用基数排序算法对 {121, 432, 564, 23, 1, 45, 788} 进行升序排序的 Java 程序:

  1. public class Demo {
  2. // 计数排序算法,place 表示以指定数位为准,对序列中的元素进行排序
  3. public static void countingSort(int array[], int place) {
  4. int size = array.length;
  5. int[] output = new int[size];
  6. // 假设第一个元素指定数位上的值最大
  7. int max = (array[0] / place) % 10;
  8. // 找到真正数位上值最大的元素
  9. for (int i = 1; i < size; i++) {
  10. if (((array[i] / place) % 10) > max)
  11. max = array[i];
  12. }
  13. // 创建并初始化 count 数组
  14. int[] count = new int[max + 1];
  15. for (int i = 0; i < max; ++i)
  16. count[i] = 0;
  17. // 统计各个元素出现的次数
  18. for (int i = 0; i < size; i++)
  19. count[(array[i] / place) % 10]++;
  20. // 累加 count 数组中的出现次数
  21. for (int i = 1; i < 10; i++)
  22. count[i] += count[i - 1];
  23. // 根据 count 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
  24. for (int i = size - 1; i >= 0; i--) {
  25. output[count[(array[i] / place) % 10] - 1] = array[i];
  26. count[(array[i] / place) % 10]--;
  27. }
  28. // 将 output 数组中的数据原封不动地拷贝到 array 数组中
  29. for (int i = 0; i < size; i++)
  30. array[i] = output[i];
  31. }
  32. // 找到整个序列中的最大值
  33. public static int getMax(int array[]) {
  34. int size = array.length;
  35. int max = array[0];
  36. for (int i = 1; i < size; i++)
  37. if (array[i] > max)
  38. max = array[i];
  39. return max;
  40. }
  41. // 基数排序算法
  42. public static void radixSort(int array[]) {
  43. // 找到序列中的最大值
  44. int max = getMax(array);
  45. // 根据最大值具有的位数,从低位依次调用计数排序算法
  46. for (int place = 1; max / place > 0; place *= 10)
  47. countingSort(array, place);
  48. }
  49. public static void main(String args[]) {
  50. int[] data = { 121, 432, 564, 23, 1, 45, 788 };
  51. radixSort(data);
  52. System.out.println(Arrays.toString(data));
  53. }
  54. }


如下是采用基数排序算法对 {121, 432, 564, 23, 1, 45, 788} 进行升序排序的 Python 程序:

  1. array = [121, 432, 564, 23, 1, 45, 788]
  2. #计数排序算法,place 表示以指定数位为准,对序列中的元素进行排序
  3. def countingSort(array, place):
  4. size = len(array)
  5. output = [0] * size
  6. # 找到真正数位上值最大的元素
  7. max_element = int(array[0] // place) % 10
  8. for i in range(1,size):
  9. if (array[i] // place) % 10 > max_element:
  10. max_element = array[i]
  11. #创建一个列表,统计各个元素的出现次数
  12. count = [0] * (max_element+1)
  13. for i in range(0, size):
  14. count[(array[i]//place) % 10] += 1
  15. #累加 count 数组中的出现次数
  16. for i in range(1, max_element):
  17. count[i] += count[i - 1]
  18. #根据 count 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
  19. i = size - 1
  20. while i >= 0:
  21. output[count[(array[i]//place) % 10] - 1] = array[i]
  22. count[(array[i]//place) % 10] -= 1
  23. i -= 1
  24. #将 output 数组中的数据原封不动地拷贝到 array 数组中
  25. for i in range(0, size):
  26. array[i] = output[i]
  27. # 基数排序算法
  28. def radixSort(array):
  29. # 找到序列中的最大值
  30. max_element = max(array)
  31. # 根据最大值具有的位数,从低位依次调用计数排序算法
  32. place = 1
  33. while max_element//place :
  34. countingSort(array, place)
  35. place *= 10
  36. radixSort(array)
  37. print(array)


以上程序的输出结果均为:

1 23 45 121 432 564 788

桶排序算法

使用桶排序算法解决此问题的 C 语言程序如下:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #define N 7 // 待排序序列中的元素个数
  4. #define NBUCKET 6 // 桶的数量
  5. #define INTERVAL 10 // 每个桶能存放的元素个数
  6. //建立桶
  7. struct Node {
  8. float data;
  9. struct Node *next;
  10. };
  11. void BucketSort(float arr[]);
  12. struct Node *InsertionSort(struct Node *list);
  13. void print(float arr[]);
  14. int main() {
  15. float array[N] = { 0.42, 0.32, 0.23, 0.52, 0.25, 0.47, 0.51 };
  16. BucketSort(array);
  17. print(array);
  18. return 0;
  19. }
  20. // 桶排序,arr 为待排序序列
  21. void BucketSort(float arr[]) {
  22. int i, j;
  23. struct Node **buckets;
  24. // 创建所有桶
  25. buckets = (struct Node **)malloc(sizeof(struct Node *) * NBUCKET);
  26. // 设置每个桶为空桶
  27. for (i = 0; i < NBUCKET; ++i) {
  28. buckets[i] = NULL;
  29. }
  30. // 根据规定,将 arr 中的每个元素分散存储到各个桶中
  31. for (i = 0; i < N; ++i) {
  32. struct Node *current;
  33. int pos = arr[i] * 10; //根据规则,确定元素所在的桶
  34. //创建存储该元素的存储块,并连接到指定的桶中
  35. current = (struct Node *)malloc(sizeof(struct Node));
  36. current->data = arr[i];
  37. current->next = buckets[pos];
  38. buckets[pos] = current;
  39. }
  40. // 调用自定义的排序算法,对各个桶进行排序
  41. for (i = 0; i < NBUCKET; ++i) {
  42. buckets[i] = InsertionSort(buckets[i]);
  43. }
  44. // 合并所有桶内的元素
  45. for (j = 0, i = 0; i < NBUCKET; ++i) {
  46. struct Node *node;
  47. node = buckets[i];
  48. while (node) {
  49. arr[j++] = node->data;
  50. node = node->next;
  51. }
  52. }
  53. }
  54. // 自定义的排序算法,用于对各个桶内元素进行排序
  55. struct Node *InsertionSort(struct Node *list) {
  56. struct Node *k, *nodeList;
  57. if (list == NULL || list->next == NULL) {
  58. return list;
  59. }
  60. nodeList = list;
  61. k = list->next;
  62. nodeList->next = NULL;
  63. while (k != NULL) {
  64. struct Node *ptr;
  65. if (nodeList->data > k->data) {
  66. struct Node *tmp;
  67. tmp = k;
  68. k = k->next;
  69. tmp->next = nodeList;
  70. nodeList = tmp;
  71. continue;
  72. }
  73. for (ptr = nodeList; ptr->next != 0; ptr = ptr->next) {
  74. if (ptr->next->data > k->data)
  75. break;
  76. }
  77. if (ptr->next != 0) {
  78. struct Node *tmp;
  79. tmp = k;
  80. k = k->next;
  81. tmp->next = ptr->next;
  82. ptr->next = tmp;
  83. continue;
  84. }
  85. else {
  86. ptr->next = k;
  87. k = k->next;
  88. ptr->next->next = 0;
  89. continue;
  90. }
  91. }
  92. return nodeList;
  93. }
  94. void print(float ar[]) {
  95. int i;
  96. for (i = 0; i < N; ++i) {
  97. printf("%.2f ", ar[i]);
  98. }
  99. }


使用桶排序算法解决此问题的 Java 程序如下:

  1. import java.util.ArrayList;
  2. import java.util.Collections;
  3. public class BucketSort {
  4. public static void bucketSort(float[] arr) {
  5. int n = arr.length;
  6. if (n <= 0)
  7. return;
  8. @SuppressWarnings("unchecked")
  9. ArrayList<Float>[] bucket = new ArrayList[n];
  10. // 创建空桶
  11. for (int i = 0; i < n; i++)
  12. bucket[i] = new ArrayList<Float>();
  13. // 根据规则将序列中元素分散到桶中
  14. for (int i = 0; i < n; i++) {
  15. int bucketIndex = (int) arr[i] * n;
  16. bucket[bucketIndex].add(arr[i]);
  17. }
  18. // 对各个桶内的元素进行排序
  19. for (int i = 0; i < n; i++) {
  20. Collections.sort((bucket[i]));
  21. }
  22. // 合并所有桶内的元素
  23. int index = 0;
  24. for (int i = 0; i < n; i++) {
  25. for (int j = 0, size = bucket[i].size(); j < size; j++) {
  26. arr[index++] = bucket[i].get(j);
  27. }
  28. }
  29. }
  30. public static void main(String[] args) {
  31. float[] arr = { (float) 0.42, (float) 0.32, (float) 0.23, (float) 0.52, (float) 0.25, (float) 0.47,
  32. (float) 0.51 };
  33. bucketSort(arr);
  34. for (float i : arr)
  35. System.out.print(i + " ");
  36. }
  37. }


使用桶排序算法解决此问题的 Python 程序如下:

  1. #桶排序算法,array 为待排序序列
  2. def bucketSort(array):
  3. bucket = []
  4. # 创建空桶
  5. for i in range(len(array)):
  6. bucket.append([])
  7. # 根据规则将所有元素分散到各个桶中
  8. for j in array:
  9. index_b = int(10 * j)
  10. bucket[index_b].append(j)
  11. # 分别对各个桶进行排序
  12. for i in range(len(array)):
  13. bucket[i] = sorted(bucket[i])
  14. # 合并所有桶内的元素
  15. k = 0
  16. for i in range(len(array)):
  17. for j in range(len(bucket[i])):
  18. array[k] = bucket[i][j]
  19. k += 1
  20. return array
  21. array = [0.42, 0.32, 0.23, 0.52, 0.25, 0.47, 0.51]
  22. print(bucketSort(array))


以上程序的输出结果均为:

0.23, 0.25, 0.32, 0.42, 0.47, 0.51, 0.52

稳定排序算法有哪些

下表给大家列出了常用的排序算法以及它们的稳定性:

表 1 排序算法的稳定性
排序算法稳定排序算法
冒泡排序算法 稳定
插入排序算法 稳定
希尔排序算法 不稳定
选择排序算法 不稳定
归并排序算法 稳定
快速排序算法 不稳定
计数排序算法 不稳定
基数排序算法 不稳定
桶排序算法 不稳定

通过优化代码、改进实现思路等方式,某些 "不稳定" 的算法也可以变得 "稳定"。以桶排序算法为例,如果保证各个桶内存储相同元素时不改变它们的相对位置,且桶内排序时采用稳定的排序算法,那么桶排序算法就可以变得“稳定”。

【扩展】就地排序算法

除了稳定性,某些场景中还需要使用就地排序算法。

“就地排序”的含义是:排序算法在排序过程中,主要使用待排序序列占用的内存空间,最多额外创建几个辅助变量,不再申请过多的辅助空间。也就是说,就地排序算法指的是直接将待排序序列修改成有序序列的排序算法,而不是新创建一个有序序列。

就地排序算法的空间复杂度为 O(1)。

下表给大家罗列了哪些排序算法属于就地排序算法:

 

表 2 就地排序算法
排序算法就地排序算法
冒泡排序算法
插入排序算法
希尔排序算法
选择排序算法
归并排序算法 不是
快速排序算法
计数排序算法 不是
基数排序算法 不是
桶排序算法 不是

 

顺序查找算法

某些场景中,待查找序列可能包含多个目标元素,需要我们全部找到。这种情况下,顺序查找算法仍然适用,只需对实现过程做一下微调即可,对应的伪代码如下:

arr[1...N]       //待查找序列
index[1...N]     //存储目标元素的位置
j <- 1
linear_search(arr , value):       // value 表示要查找的目标元素
    for i <-1 to length(arr):     // 从 arr 序列中第一个元素开始遍历,直至最后一个元素
        if arr[i] == value:       // 如果成功找到一个元素和目标元素匹配,则返回该元素所处的位置
            index[j] <- i         // 将目标元素所在序列的位置存储到 index 中
            j <- j + 1            // j 自加,为下次在 index 中存储目标元素的位置做准备       
    return index

顺序查找算法的具体实现

结合伪代码,如下是使用顺序查找算法在  {10,14,19,26,27,31,33,35,42,44} 中查找 33 的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 10 //待查找序列的元素个数
  3. //实现顺序查找,arr[N] 为待查找序列,value 为要查找的目标元素
  4. int linear_search(int arr[N], int value) {
  5. int i;
  6. //从第 1 个元素开始遍历
  7. for (i = 0; i < N; i++) {
  8. //匹配成功,返回元素所处的位置下标
  9. if (arr[i] == value) {
  10. return i;
  11. }
  12. }
  13. //匹配失败,返回 -1
  14. return -1;
  15. }
  16. int main()
  17. {
  18. int arr[N] = { 10,14,19,26,27,31,33,35,42,44 };
  19. int add = linear_search(arr, 33);
  20. if (add != -1) {
  21. printf("查找成功,为序列中第 %d 个元素", add + 1);
  22. }
  23. else {
  24. printf("查找失败");
  25. }
  26. return 0;
  27. }


如下是使用顺序查找算法在  {10,14,19,26,27,31,33,35,42,44} 中查找 33 的 Java 程序:

  1. public class Demo {
  2. // 实现顺序查找,arr[N] 为待查找序列,value 为要查找的目标元素
  3. public static int linear_search(int[] arr, int value) {
  4. // 从第 1 个元素开始遍历
  5. for (int i = 0; i < arr.length; i++) {
  6. // 匹配成功,返回元素所处的位置下标
  7. if (arr[i] == value) {
  8. return i;
  9. }
  10. }
  11. // 匹配失败,返回 -1
  12. return -1;
  13. }
  14. public static void main(String[] args) {
  15. int[] arr = new int[] { 10, 14, 19, 26, 27, 31, 33, 35, 42, 44 };
  16. int add = linear_search(arr, 33);
  17. if (add != -1) {
  18. System.out.println("查找成功,为序列中第 " + (add + 1) + " 个元素");
  19. } else {
  20. System.out.println("查找失败");
  21. }
  22. }
  23. }


如下是使用顺序查找算法在  {10,14,19,26,27,31,33,35,42,44} 中查找 33 的 Python 程序:

  1. #待查找序列
  2. arr = [10,14,19,26,27,31,33,35,42,44]
  3. #实现顺序查找算法,value 为要查找的目标元素
  4. def linear_search(value):
  5. #遍历整个序列
  6. for i in range(len(arr)):
  7. if arr[i] == value:
  8. return i
  9. return -1
  10. add = linear_search(33)
  11. if add != -1:
  12. print("查找成功,为序列中第 %d 个元素" % (add + 1))
  13. else:
  14. print("查找失败")


以上程序的输出结果均为:

查找成功,为序列中第 7 个元 

二分查找算法(折半查找算法)

二分查找算法的具体实现

如下用伪代码给大家展示了二分查找算法的具体实现过程:

输入 arr[]                                // 输入有序序列
binary_search( arr , begin , end , ele):  // [begin,end] 指定搜索区域,ele 为要搜索的目标元素
    if begin > end:                       // [begin,end] 不存在时,返回一个错误值(比如 -1)
        return -1
    mid <- ⌊ begin+(end-begin)/2 ⌋        // 找到 [begin,end] 区域内中间元素所在位置的下标
    if ele == arr[mid]:                  // 递归的出口,即 ele 和中间元素的值相等
        return mid
    if ele < arr[mid]:                  // 比较 ele 和中间元素的值,进一步缩小搜索区域
        return binary_search(arr , begin , mid-1 , ele)
    else:
        return binary_search(arr , mid+1 , end , ele)


结合伪代码,如下是用二分查找算法在 {10, 14, 19, 26, 27, 31, 33, 35, 42, 44} 升序序列中查找元素 31 的 C 语言程序:

  1. #include <stdio.h>
  2. //实现二分查找算法,ele 表示要查找的目标元素,[p,q] 指定查找区域
  3. int binary_search(int *arr,int p,int q,int ele) {
  4. int mid = 0;
  5. //如果[p,q] 不存在,返回 -1
  6. if (p > q) {
  7. return -1;
  8. }
  9. // 找到中间元素所在的位置
  10. mid = p + (q - p) / 2;
  11. //递归的出口
  12. if (ele == arr[mid]) {
  13. return mid;
  14. }
  15. //比较 ele 和 arr[mid] 的值,缩小 ele 可能存在的区域
  16. if (ele < arr[mid]) {
  17. //新的搜索区域为 [p,mid-1]
  18. return binary_search(arr, p, mid - 1, ele);
  19. }
  20. else {
  21. //新的搜索区域为 [mid+1,q]
  22. return binary_search(arr, mid + 1, q, ele);
  23. }
  24. }
  25. int main()
  26. {
  27. int arr[10] = { 10,14,19,26,27,31,33,35,42,44 };
  28. //输出二叉查找元素 31 所在位置的下标
  29. printf("%d", binary_search(arr, 0, 9, 31));
  30. return 0;
  31. }


如下是用二分查找算法在 {10, 14, 19, 26, 27, 31, 33, 35, 42, 44} 升序序列中查找元素 31 的 Java 程序:

  1. public class Demo {
  2. // 实现二分查找算法,ele 表示要查找的目标元素,[p,q] 指定查找区域
  3. public static int binary_search(int[] arr, int p, int q, int ele) {
  4. // 如果[p,q] 不存在,返回 -1
  5. if (p > q) {
  6. return -1;
  7. }
  8. // 找到中间元素所在的位置
  9. int mid = p + (q - p) / 2;
  10. // 递归的出口
  11. if (ele == arr[mid]) {
  12. return mid;
  13. }
  14. // 比较 ele 和 arr[mid] 的值,缩小 ele 可能存在的区域
  15. if (ele < arr[mid]) {
  16. // 新的搜索区域为 [p,mid-1]
  17. return binary_search(arr, p, mid - 1, ele);
  18. } else {
  19. // 新的搜索区域为 [mid+1,q]
  20. return binary_search(arr, mid + 1, q, ele);
  21. }
  22. }
  23. public static void main(String[] args) {
  24. int[] arr = new int[] { 10, 14, 19, 26, 27, 31, 33, 35, 42, 44 };
  25. // 输出二叉查找元素 31 所在位置的下标
  26. int add = binary_search(arr, 0, 9, 31);
  27. System.out.print(add);
  28. }
  29. }


如下是用二分查找算法在 {10, 14, 19, 26, 27, 31, 33, 35, 42, 44} 升序序列中查找元素 31 的 Python 程序:

  1. #实现二分查找算法,ele 表示要查找的目标元素,[p,q] 指定查找区域
  2. def binary_search(arr,p,q,ele):
  3. #如果[p,q] 不存在,返回 -1
  4. if p > q:
  5. return -1
  6. #找到中间元素所在的位置
  7. mid = p + int( (q - p) / 2 )
  8. #递归的出口
  9. if ele == arr[mid]:
  10. return mid
  11. #比较 ele 和 arr[mid] 的值,缩小 ele 可能存在的区域
  12. if ele < arr[mid]:
  13. return binary_search(arr,p,mid-1,ele)
  14. else:
  15. return binary_search(arr,mid+1,q,ele)
  16. arr = [10, 14, 19, 26, 27, 31, 33, 35, 42, 44]
  17. #输出二叉查找元素 31 所在位置的下标
  18. add = binary_search(arr, 0, 9, 31);
  19. print(add)


以上程序的输出结果均为:

5

插值查找算法

插值查找算法的具体实现

如下用伪代码给大家展示了插值查找算法的具体实现过程:

输入 arr[]               // 输入有序序列
输入 ele                 // 输入查找的目标元素  
interpolation_search( arr , begin , end , ele):      // [begin,end] 指定搜索区域,ele 为要搜索的目标元素
    // [begin,end] 不存在时,返回一个错误值(比如 -1)
    if begin > end: 
        return -1
    // [begin,end] 只包含 1 个元素时,判断此元素是否为目标元素
    if begin == end:
        if ele == arr[begin]:
            return begin
        else:
            return -1
    // 找到 [begin,end] 区域“中间值”的下标
    mid <- begin + ( (end-begin)/(arr[end] - arr[begin]) * (ele - arr[begin]) )
    // 递归的出口,即 ele 和中间元素的值相等
    if ele == arr[mid]:                                   
        return mid
    if ele < arr[mid]:         // 比较 ele 和中间元素的值,进一步缩小搜索区域
        return binary_search(arr , begin , mid-1 , ele)
    else:
        return binary_search(arr , mid+1 , end , ele)


结合伪代码,如下是使用插值查找算法在 {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 序列中查找元素 2 的 C 语言程序:

  1. #include <stdio.h>
  2. //实现插值查找算法,ele 表示要查找的目标元素,[begin,end] 指定查找区域
  3. int interpolation_search(int* arr, int begin, int end, int ele) {
  4. int mid = 0;
  5. //如果[begin,end] 不存在,返回 -1
  6. if (begin > end) {
  7. return -1;
  8. }
  9. //如果搜索区域内只有一个元素,判断其是否为目标元素
  10. if (begin == end) {
  11. if (ele == arr[begin]) {
  12. return begin;
  13. }
  14. //如果该元素非目标元素,则查找失败
  15. return -1;
  16. }
  17. // 找到"中间元素"所在的位置
  18. mid = begin + ((end - begin) / (arr[end] - arr[begin]) * (ele - arr[begin]));
  19. //递归的出口
  20. if (ele == arr[mid]) {
  21. return mid;
  22. }
  23. //比较 ele 和 arr[mid] 的值,缩小 ele 可能存在的区域
  24. if (ele < arr[mid]) {
  25. //新的搜索区域为 [begin,mid-1]
  26. return interpolation_search(arr, begin, mid - 1, ele);
  27. }
  28. else {
  29. //新的搜索区域为 [mid+1,end]
  30. return interpolation_search(arr, mid + 1, end, ele);
  31. }
  32. }
  33. int main()
  34. {
  35. int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  36. //输出元素 2 所在位置的下标
  37. int pos = interpolation_search(arr, 0, 9, 2);
  38. if (pos != -1) {
  39. printf("%d", interpolation_search(arr, 0, 9, 2));
  40. }
  41. else {
  42. printf("查找失败");
  43. }
  44. return 0;
  45. }


如下是使用插值查找算法在 {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 序列中查找元素 2 的 Java 程序:

  1. public class Demo {
  2. // 实现插值查找算法,ele 表示要查找的目标元素,[begin,end] 指定查找区域
  3. public static int interpolation_search(int[] arr, int begin, int end, int ele) {
  4. // 如果[begin,end] 不存在,返回 -1
  5. if (begin > end) {
  6. return -1;
  7. }
  8. //如果搜索区域内只有一个元素,判断其是否为目标元素
  9. if (begin == end) {
  10. if (ele == arr[begin]) {
  11. return begin;
  12. }
  13. //如果该元素非目标元素,则查找失败
  14. return -1;
  15. }
  16. // 找到中间元素所在的位置
  17. int mid = begin + ((end - begin) / (arr[end] - arr[begin]) * (ele - arr[begin]));
  18. // 递归的出口
  19. if (ele == arr[mid]) {
  20. return mid;
  21. }
  22. // 比较 ele 和 arr[mid] 的值,缩小 ele 可能存在的区域
  23. if (ele < arr[mid]) {
  24. // 新的搜索区域为 [begin,mid-1]
  25. return interpolation_search(arr, begin, mid - 1, ele);
  26. } else {
  27. // 新的搜索区域为 [mid+1,end]
  28. return interpolation_search(arr, mid + 1, end, ele);
  29. }
  30. }
  31. public static void main(String[] args) {
  32. int[] arr = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
  33. // 输出目标元素 2 所在位置的下标
  34. int add = interpolation_search(arr, 0, 9, 2);
  35. if(add != -1) {
  36. System.out.print(add);
  37. }else {
  38. System.out.print("查找失败");
  39. }
  40. }
  41. }


如下是使用插值查找算法在 {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 序列中查找元素 2 的 Python 程序:

  1. #实现插值查找算法,ele 表示要查找的目标元素,[begin,end] 指定查找区域
  2. def interpolation_search(arr,begin,end,ele):
  3. #如果[begin,end] 不存在,返回 -1
  4. if begin > end:
  5. return -1
  6. if begin == end:
  7. if arr[begin] == ele:
  8. return begin
  9. return -1
  10. #找到中间元素所在的位置
  11. mid = int(begin + ((end - begin) / (arr[end] - arr[begin]) * (ele - arr[begin])))
  12. #递归的出口
  13. if ele == arr[mid]:
  14. return mid
  15. #比较 ele 和 arr[mid] 的值,缩小 ele 可能存在的区域
  16. if ele < arr[mid]:
  17. return interpolation_search(arr,begin,mid-1,ele)
  18. else:
  19. return interpolation_search(arr,mid+1,end,ele)
  20. arr = [1,2,3,4,5,6,7,8,9,10]
  21. #输出元素 2 所在位置的下标
  22. add = interpolation_search(arr, 0, 9, 2);
  23. if add != -1:
  24. print(add)
  25. else:
  26. print("查找失败")


以上程序的输出结果均为:

1

哈希查找算法

哈希查找算法

哈希查找算法就是利用哈希表查找目标元素的算法。对于给定的序列,该算法会先将整个序列存储到哈希表中,然后再查找目标元素。

例如,哈希查找算法查找 {5, 20, 30, 50, 55} 序列中是否有 50 这个元素,实现的伪代码如下:

N <- 10          // 指定哈希表的长度
输入 arr[]        //存储 {5, 20, 30, 50, 55} 待查找序列
//哈希函数
hash(value):
    return value%10
//创建哈希表,arr为原序列,hashArr为空的哈希表
createHash(arr, hashArr):
    for i <- 0 to 5:
        index <- hash(arr[i])
        while (hashArr[index % N] !=0):
            index <- index + 1
        hashArr[index] <- arr[i]
// 实现哈希查找算法,value 为要查找的目标元素
hash_serch(hashArr[] , value):           
    hashAdd = hash(value)            // 根据哈希函数,找到对应的索引值
    while hashArr[hashAdd] != value: // 如果哈希表中对应位置不是要查找的目标元(即发生了碰撞)
        hashAdd = (hashAdd + 1) % N  // 获取下一个索引值
        if hashArr[hashAdd] == 0 || hashAdd = hash(value):   // 如果索引值对应的存储位置为空(这里用 -1 表示),或者已经查找了一圈,仍为找到目标元素
            return -1                 // 查找失败(返回 -1 表示查找失败)
    return hashAdd                    // 返回目标元素所在的索引


结合伪代码,如下是使用哈希查找算法在 {5, 20, 30, 50, 55} 序列中查找 50 的 C 语言程序:

  1. #include <stdio.h>
  2. #define N 10 //指定哈希表的长度
  3. //自定义哈希函数
  4. int hash(int value) {
  5. return value % 10;
  6. }
  7. //创建哈希表
  8. void creatHash(int arr[5], int hashArr[N]) {
  9. int i,index;
  10. //将序列中每个元素存储到哈希表
  11. for (i = 0; i < 5; i++) {
  12. index = hash(arr[i]);
  13. while(hashArr[index % N] != 0) {
  14. index++;
  15. }
  16. hashArr[index] = arr[i];
  17. }
  18. }
  19. //实现哈希查找算法,hashArr 表示哈希表,value 为要查找的目标元素
  20. int hash_search(int* hashArr, int value) {
  21. int hashAdd = hash(value); //查找目标元素所在的索引
  22. while (hashArr[hashAdd] != value) { // 如果索引位置不是目标元素,则发生了碰撞
  23. hashAdd = (hashAdd + 1) % N; // 根据线性探测法,从索引位置依次向后探测
  24. //如果探测位置为空,或者重新回到了探测开始的位置(即探测了一圈),则查找失败
  25. if (hashArr[hashAdd] == 0 || hashAdd == hash(value)) {
  26. return -1;
  27. }
  28. }
  29. //返回目标元素所在的数组下标
  30. return hashAdd;
  31. }
  32. int main()
  33. {
  34. int hashAdd;
  35. int hashArr[N] = { 0 };
  36. int arr[5] = { };
  37. creatHash(arr, hashArr);
  38. hashAdd = hash_search(hashArr, 50);
  39. //如果返回值为 -1,表明查找失败,反之则返回目标元素所在的位置
  40. if (hashAdd == -1) {
  41. printf("查找失败\n");
  42. }
  43. else {
  44. printf("查找成功,目标元素所在哈希表中的下标为:%d", hashAdd);
  45. }
  46. return 0;
  47. }


如下是使用哈希查找算法在 {5, 20, 30, 50, 55} 序列中查找 50 的 Java 程序:

  1. public class Demo {
  2. //哈希函数
  3. public static int hash(int value) {
  4. return value % 10;
  5. }
  6. //创建哈希表
  7. public static void creatHash(int [] arr,int [] hashArr) {
  8. int i,index;
  9. //将序列中每个元素存储到哈希表
  10. for (i = 0; i < 5; i++) {
  11. index = hash(arr[i]);
  12. while(hashArr[index % 10] != 0) {
  13. index++;
  14. }
  15. hashArr[index] = arr[i];
  16. }
  17. }
  18. //实现哈希查找算法
  19. public static int hash_serach(int [] hashArr,int value) {
  20. //查找目标元素对应的索引值
  21. int hashAdd = hash(value);
  22. while (hashArr[hashAdd] != value) { // 如果索引位置不是目标元素,则发生了碰撞
  23. hashAdd = (hashAdd + 1) % 10; // 根据线性探测法,从索引位置依次向后探测
  24. //如果探测位置为空,或者重新回到了探测开始的位置(即探测了一圈),则查找失败
  25. if (hashArr[hashAdd] == 0 || hashAdd == hash(value)) {
  26. return -1;
  27. }
  28. }
  29. //返回目标元素所在的数组下标
  30. return hashAdd;
  31. }
  32. public static void main(String[] args) {
  33. int [] arr = new int[] {5, 20, 30, 50, 55};
  34. int[] hashArr = new int[10];
  35. //创建哈希表
  36. creatHash(arr,hashArr);
  37. // 查找目标元素 50 位于哈希表中的位置
  38. int hashAdd = hash_serach(hashArr,50);
  39. if(hashAdd == -1) {
  40. System.out.print("查找失败");
  41. }else {
  42. System.out.print("查找成功,目标元素所在哈希表中的下标为:" + hashAdd);
  43. }
  44. }
  45. }


如下是使用哈希查找算法在 {5, 20, 30, 50, 55} 序列中查找 50 的 Python 程序:

  1. # 自定义哈希函数
  2. def hash(hashArr,value):
  3. return value % len(hashArr)
  4. #创建哈希表
  5. def createHash(arr,hashArr):
  6. for ele in arr:
  7. index = hash(hashArr,ele)
  8. while hashArr[index % len(hashArr)] != 0:
  9. index = index + 1
  10. hashArr[index] = ele
  11. # 实现哈希查找算法,hashArr 表示哈希表,value 为要查找的目标元素
  12. def hash_search(hashArr,value):
  13. hashAdd = hash(hashArr,value) # 查找目标元素所在的索引
  14. while hashArr[hashAdd] != value: # 如果索引位置不是目标元素,则发生了碰撞
  15. hashAdd = (hashAdd + 1) % len(hashArr) # 根据线性探测法,从索引位置依次向后探测
  16. #如果探测位置为空,或者重新回到了探测开始的位置(即探测了一圈),则查找失败
  17. if (hashArr[hashAdd] == 0) or (hashAdd == hash(hashArr,value)):
  18. return -1
  19. return hashAdd # 返回目标元素所在的数组下标
  20. #待查找序列
  21. arr = [5, 20, 30, 50, 55]
  22. #构建哈希表
  23. hashArr = [0]*10
  24. createHash(arr,hashArr)
  25. # 查找元素 50 的位置
  26. hashAdd = hash_search(hashArr,50)
  27. if hashAdd == -1:
  28. print("查找失败\n")
  29. else:
  30. print("查找成功,目标元素所在哈希表中的下标为 %d" % (hashAdd))


以上程序的输出结果均为:

查找成功,目标元素所在哈希表中的下标为 2

最小生成树

普里姆算法的具体实现

接下来,我们将给出实现普里姆算法的 C、Java、Python 程序,程序中有详尽的注释,您可以借助编译器一边运行程序一边观察程序的执行过程,彻底搞清楚普里姆算法是如何找到最小生成树的。

如下是使用普里姆算法在图 1 所示的连通网中查找最小生成树的 C 语言程序:

  1. #include<stdio.h>
  2. #define V 6 // 记录图中顶点的个数
  3. typedef enum { false, true } bool;
  4. //查找权值最小的、尚未被选择的顶点,key 数组记录了各顶点之间的权值数据,visited数组记录着各个顶点是否已经被选择的信息
  5. int min_Key(int key[], bool visited[])
  6. {
  7. int min = 2147483647, min_index; //遍历 key 数组使用,min 记录最小的权值,min_index 记录最小权值关联的顶点
  8. //遍历 key 数组
  9. for (int v = 0; v < V; v++) {
  10. //如果当前顶点为被选择,且对应的权值小于 min 值
  11. if (visited[v] == false && key[v] < min) {
  12. //更新 min 的值并记录该顶点的位置
  13. min = key[v];
  14. min_index = v;
  15. }
  16. }
  17. //返回最小权值的顶点的位置
  18. return min_index;
  19. }
  20. //输出最小生成树
  21. void print_MST(int parent[], int cost[V][V])
  22. {
  23. int minCost = 0;
  24. printf("最小生成树为:\n");
  25. //遍历 parent 数组
  26. for (int i = 1; i < V; i++) {
  27. //parent 数组下标值表示各个顶点,各个下标对应的值为该顶点的父节点
  28. printf("%d - %d wight:%d\n", parent[i] + 1, i + 1, cost[i][parent[i]]);//由于数组下标从 0 开始,因此输出时各自 +1
  29. //统计最小生成树的总权值
  30. minCost += cost[i][parent[i]];
  31. }
  32. printf("总权值为:%d", minCost);
  33. }
  34. //根据用户提供了图的信息(存储在 cost 数组中),寻找最小生成树
  35. void find_MST(int cost[V][V])
  36. { //key 数组用于记录 B 类顶点到 A 类顶点的权值
  37. //parent 数组用于记录最小生成树中各个顶点父节点的位置,便于最终生成最小生成树
  38. //visited 数组用于记录各个顶点属于 A 类还是 B 类
  39. int parent[V], key[V];
  40. bool visited[V];
  41. // 初始化 3 个数组
  42. for (int i = 0; i < V; i++) {
  43. key[i] = 2147483647; // 将 key 数组各个位置设置为无限大的数
  44. visited[i] = false; // 所有的顶点全部属于 B 类
  45. parent[i] = -1; // 所有顶点都没有父节点
  46. }
  47. // 选择 key 数组中第一个顶点,开始寻找最小生成树
  48. key[0] = 0; // 该顶点对应的权值设为 0
  49. parent[0] = -1; // 该顶点没有父节点
  50. // 对于 V 个顶点的图,最需选择 V-1 条路径,即可构成最小生成树
  51. for (int x = 0; x < V - 1; x++)
  52. {
  53. // 从 key 数组中找到权值最小的顶点所在的位置
  54. int u = min_Key(key, visited);
  55. // 该顶点划分到 A 类
  56. visited[u] = true;
  57. // 由于新顶点加入 A 类,因此需要更新 key 数组中的数据
  58. for (int v = 0; v < V; v++)
  59. {
  60. // 如果类 B 中存在到下标为 u 的顶点的权值比 key 数组中记录的权值还小,表明新顶点的加入,使得类 B 到类 A 顶点的权值有了更好的选择
  61. if (cost[u][v] != 0 && visited[v] == false && cost[u][v] < key[v])
  62. {
  63. // 更新 parent 数组记录的各个顶点父节点的信息
  64. parent[v] = u;
  65. // 更新 key 数组
  66. key[v] = cost[u][v];
  67. }
  68. }
  69. }
  70. //根据 parent 记录的各个顶点父节点的信息,输出寻找到的最小生成树
  71. print_MST(parent, cost);
  72. }
  73. // main function
  74. int main()
  75. {
  76. int p1, p2;
  77. int wight;
  78. int cost[V][V] = { 0 };
  79. printf("输入图(顶点到顶点的路径和权值):\n");
  80. while (1) {
  81. scanf("%d %d", &p1, &p2);
  82. //如果用户输入 -1 -1,表示输入结束
  83. if (p1 == -1 && p2 == -1) {
  84. break;
  85. }
  86. scanf("%d", &wight);
  87. cost[p1 - 1][p2 - 1] = wight;
  88. cost[p2 - 1][p1 - 1] = wight;
  89. }
  90. // 根据用户输入的图的信息,寻找最小生成树
  91. find_MST(cost);
  92. return 0;
  93. }


如下是使用普里姆算法在图 1 所示的连通网中查找最小生成树的 Java 程序:

  1. import java.util.Scanner;
  2. public class prim {
  3. static int V = 6;
  4. public static int min_Key(int []key,boolean []visited) {
  5. //遍历 key 数组使用,min 记录最小的权值,min_index 记录最小权值关联的顶点
  6. int min = 2147483647,min_index = 0;
  7. //遍历 key 数组
  8. for (int v = 0; v < V; v++) {
  9. //如果当前顶点为被选择,且对应的权值小于 min 值
  10. if (visited[v] == false && key[v] < min) {
  11. //更新 min 的值并记录该顶点的位置
  12. min = key[v];
  13. min_index = v;
  14. }
  15. }
  16. //返回最小权值的顶点的位置
  17. return min_index;
  18. }
  19. public static void print_MST(int []parent, int [][]cost) {
  20. int minCost = 0;
  21. System.out.println("最小生成树为:");
  22. //遍历 parent 数组
  23. for (int i = 1; i < V; i++) {
  24. //parent 数组下标值表示各个顶点,各个下标对应的值为该顶点的父节点
  25. System.out.println((parent[i]+1)+" - "+(i+1)+" wight:"+cost[i][parent[i]]);//由于数组下标从 0 开始,因此输出时各自 +1
  26. //统计最小生成树的总权值
  27. minCost += cost[i][parent[i]];
  28. }
  29. System.out.print("总权值为:"+minCost);
  30. }
  31. public static void find_MST(int [][]cost) {
  32. //key 数组用于记录 B 类顶点到 A 类顶点的权值
  33. //parent 数组用于记录最小生成树中各个顶点父节点的位置,便于最终生成最小生成树
  34. //visited 数组用于记录各个顶点属于 A 类还是 B 类
  35. int []parent = new int[V];
  36. int []key = new int[V];
  37. boolean []visited=new boolean[V];
  38. // 初始化 3 个数组
  39. for (int i = 0; i < V; i++) {
  40. key[i] = 2147483647; // 将 key 数组各个位置设置为无限大的数
  41. visited[i] = false; // 所有的顶点全部属于 B 类
  42. parent[i] = -1; // 所有顶点都没有父节点
  43. }
  44. // 选择 key 数组中第一个顶点,开始寻找最小生成树
  45. key[0] = 0; // 该顶点对应的权值设为 0
  46. parent[0] = -1; // 该顶点没有父节点
  47. // 对于 V 个顶点的图,最需选择 V-1 条路径,即可构成最小生成树
  48. for (int x = 0; x < V - 1; x++)
  49. {
  50. // 从 key 数组中找到权值最小的顶点所在的位置
  51. int u = min_Key(key, visited);
  52. // 该顶点划分到 A 类
  53. visited[u] = true;
  54. // 由于新顶点加入 A 类,因此需要更新 key 数组中的数据
  55. for (int v = 0; v < V; v++)
  56. {
  57. // 如果类 B 中存在到下标为 u 的顶点的权值比 key 数组中记录的权值还小,表明新顶点的加入,使得类 B 到类 A 顶点的权值有了更好的选择
  58. if (cost[u][v] != 0 && visited[v] == false && cost[u][v] < key[v])
  59. {
  60. // 更新 parent 数组记录的各个顶点父节点的信息
  61. parent[v] = u;
  62. // 更新 key 数组
  63. key[v] = cost[u][v];
  64. }
  65. }
  66. }
  67. //根据 parent 记录的各个顶点父节点的信息,输出寻找到的最小生成树
  68. print_MST(parent, cost);
  69. }
  70. public static void main(String[] args) {
  71. int [][]cost = new int[V][V];
  72. System.out.println("输入图(顶点到顶点的路径和权值):");
  73. Scanner sc = new Scanner(System.in);
  74. while (true) {
  75. int p1 = sc.nextInt();
  76. int p2 = sc.nextInt();
  77. // System.out.println(p1+p2);
  78. if (p1 == -1 && p2 == -1) {
  79. break;
  80. }
  81. int wight = sc.nextInt();
  82. cost[p1-1][p2-1] = wight;
  83. cost[p2-1][p1-1] = wight;
  84. }
  85. // 根据用户输入的图的信息,寻找最小生成树
  86. find_MST(cost);
  87. }
  88. }


如下是使用普里姆算法在图 1 所示的连通网中查找最小生成树的 Python 程序:

  1. V = 6 #图中顶点的个数
  2. cost = [[0]*V for i in range(V)]
  3. print("输入图(顶点到顶点的路径和权值):")
  4. while True:
  5. li = input().split()
  6. p1 = int(li[0])
  7. p2 = int(li[1])
  8. if p1 == -1 and p2 == -1:
  9. break
  10. wight = int(li[2])
  11. cost[p1-1][p2-1] = wight
  12. cost[p2-1][p1-1] = wight
  13. #查找权值最小的、尚未被选择的顶点,key 列表记录了各顶点之间的权值数据,visited列表记录着各个顶点是否已经被选择的信息
  14. def min_Key(key,visited):
  15. #遍历 key 列表使用,min 记录最小的权值,min_index 记录最小权值关联的顶点
  16. min = float('inf')
  17. min_index = 0
  18. #遍历 key 列表
  19. for v in range(V):
  20. #如果当前顶点为被选择,且对应的权值小于 min 值
  21. if visited[v] == False and key[v]<min:
  22. #更新 min 的值并记录该顶点的位置
  23. min = key[v]
  24. min_index=v
  25. #返回最小权值的顶点的位置
  26. return min_index
  27. #输出最小生成树
  28. def print_MST(parent,cost):
  29. minCost=0
  30. print("最小生成树为:")
  31. #遍历 parent 列表
  32. for i in range(1,V):
  33. #parent 列表下标值表示各个顶点,各个下标对应的值为该顶点的父节点
  34. print("%d - %d wight:%d"%(parent[i]+1, i+1, cost[i][parent[i]]))
  35. #统计最小生成树的总权值
  36. minCost = minCost + cost[i][parent[i]];
  37. print("总权值为:%d"%(minCost))
  38. #根据用户提供了图的信息(存储在 cost 列表中),寻找最小生成树
  39. def find_MST(cost):
  40. #key 列表用于记录 B 类顶点到 A 类顶点的权值
  41. #parent 列表用于记录最小生成树中各个顶点父节点的位置,便于最终生成最小生成树
  42. #visited 列表用于记录各个顶点属于 A 类还是 B 类
  43. parent = [-1]*V
  44. key = [float('inf')]*V
  45. visited = [False]*V
  46. # 选择 key 列表中第一个顶点,开始寻找最小生成树
  47. key[0] = 0
  48. parent[0]= -1
  49. # 对于 V 个顶点的图,最需选择 V-1 条路径,即可构成最小生成树
  50. for x in range(V-1):
  51. # 从 key 列表中找到权值最小的顶点所在的位置
  52. u = min_Key(key,visited)
  53. visited[u] = True
  54. # 由于新顶点加入 A 类,因此需要更新 key 列表中的数据
  55. for v in range(V):
  56. # 如果类 B 中存在到下标为 u 的顶点的权值比 key 列表中记录的权值还小,表明新顶点的加入,使得类 B 到类 A 顶点的权值有了更好的选择
  57. if cost[u][v] !=0 and visited[v] == False and cost[u][v] < key[v]:
  58. # 更新 parent 列表记录的各个顶点父节点的信息
  59. parent[v] = u
  60. # 更新 key 列表
  61. key[v] = cost[u][v]
  62. # 根据 parent 记录的各个顶点父节点的信息,输出寻找到的最小生成树
  63. print_MST(parent,cost);
  64. find_MST(cost)


图 1 连通网中的顶点 A、B、C、D、S、T 分别用 1~6 的数字表示,上面程序的运行结果均为:

输入图(顶点到顶点的路径和权值):
1 5 7
1 3 3
5 3 8
1 2 6
2 3 4
2 4 2
3 4 3
2 6 5
4 6 2
-1 -1
最小生成树为:
4 - 2 wight:2
1 - 3 wight:3
3 - 4 wight:3
1 - 5 wight:7
4 - 6 wight:2
总权值为:17

最短路径算法

最短路径算法

实际生产和生活中,经常需要查找最短路径,例如借助高德、腾讯地图等导航工具查找到目的地的最短可行路线,建立道路交通网、物流运输网络、计算机网络等,这时就可以使用最短路径算法。

常用的查找最短路径的算法有 4 种,下表给出了它们各自的名称和特点。

表 1 最短路径算法
最短路径算法描 述
迪杰斯特拉算法(Dijkstra) 寻找某个特定顶点到其它所有顶点的最短路径,该算法要求所有路径的权值为非负数。
弗洛伊德算法(Floyd-Warshall) 寻找各个顶点之间的最短路径,允许非环路的路径权值为负数,该算法不仅适用于稀疏图,在稠密图(路径数量多的图)中寻找最短路径的效率也很高。
贝尔曼福特算法(Bellman-Ford) 寻找某个特定顶点到其它所有顶点的最短路径,该算法允许路径的权值为负数。

推荐阅读:
约翰逊算法(Johnson) 寻找各个顶点之间的最短路径,允许非环路的路径权值为负数,该算法更适用于稀疏图(路径数量少的图)。

推荐阅读:
 

迪杰斯特拉算法(求最短路径)

 

迪杰斯特拉算法的具体实现

了解了迪杰斯特拉算法的实现过程之后,接下来分别编写 C、Java 和 Python 程序真正地实现迪杰斯特拉算法。

仍以图 1 为例,迪杰斯特拉算法查找顶点 0 到其它顶点所有最短路径的 C 语言程序为:

  1. #include <stdio.h>
  2. #define V 20 //顶点的最大个数
  3. #define INFINITY 65535
  4. typedef struct {
  5. int vexs[V]; //存储图中顶点数据
  6. int arcs[V][V]; //二维数组,记录顶点之间的关系
  7. int vexnum, arcnum; //记录图的顶点数和弧(边)数
  8. }MGraph;
  9. //根据顶点本身数据,判断出顶点在二维数组中的位置
  10. int LocateVex(MGraph * G, int v) {
  11. int i = 0;
  12. //遍历一维数组,找到变量v
  13. for (; i < G->vexnum; i++) {
  14. if (G->vexs[i] == v) {
  15. break;
  16. }
  17. }
  18. //如果找不到,输出提示语句,返回-1
  19. if (i > G->vexnum) {
  20. printf("no such vertex.\n");
  21. return -1;
  22. }
  23. return i;
  24. }
  25. //构造无向有权图
  26. void CreateDG(MGraph *G) {
  27. printf("输入图的顶点数和边数:");
  28. scanf("%d %d", &(G->vexnum), &(G->arcnum));
  29. printf("输入各个顶点:");
  30. for (int i = 0; i < G->vexnum; i++) {
  31. scanf("%d", &(G->vexs[i]));
  32. }
  33. for (int i = 0; i < G->vexnum; i++) {
  34. for (int j = 0; j < G->vexnum; j++) {
  35. G->arcs[i][j] = INFINITY;
  36. }
  37. }
  38. printf("输入各个边的数据:\n");
  39. for (int i = 0; i < G->arcnum; i++) {
  40. int v1, v2, w;
  41. scanf("%d %d %d", &v1, &v2, &w);
  42. int n = LocateVex(G, v1);
  43. int m = LocateVex(G, v2);
  44. if (m == -1 || n == -1) {
  45. return;
  46. }
  47. G->arcs[n][m] = w;
  48. G->arcs[m][n] = w;
  49. }
  50. }
  51. //迪杰斯特拉算法,v0表示有向网中起始点所在数组中的下标
  52. void Dijkstra_minTree(MGraph G, int v0, int p[V], int D[V]) {
  53. int final[V];//为各个顶点配置一个标记值,用于确认该顶点是否已经找到最短路径
  54. //对各数组进行初始化
  55. for (int v = 0; v < G.vexnum; v++) {
  56. final[v] = 0;
  57. D[v] = G.arcs[v0][v];
  58. p[v] = 0;
  59. }
  60. //由于以v0位下标的顶点为起始点,所以不用再判断
  61. D[v0] = 0;
  62. final[v0] = 1;
  63. int k = 0;
  64. for (int i = 0; i < G.vexnum; i++) {
  65. int min = INFINITY;
  66. //选择到各顶点权值最小的顶点,即为本次能确定最短路径的顶点
  67. for (int w = 0; w < G.vexnum; w++) {
  68. if (!final[w]) {
  69. if (D[w] < min) {
  70. k = w;
  71. min = D[w];
  72. }
  73. }
  74. }
  75. //设置该顶点的标志位为1,避免下次重复判断
  76. final[k] = 1;
  77. //对v0到各顶点的权值进行更新
  78. for (int w = 0; w < G.vexnum; w++) {
  79. if (!final[w] && (min + G.arcs[k][w] < D[w])) {
  80. D[w] = min + G.arcs[k][w];
  81. p[w] = k;//记录各个最短路径上存在的顶点
  82. }
  83. }
  84. }
  85. }
  86. int main() {
  87. MGraph G;
  88. CreateDG(&G);
  89. int P[V] = { 0 }; // 记录顶点 0 到各个顶点的最短的路径
  90. int D[V] = { 0 }; // 记录顶点 0 到各个顶点的总权值
  91. Dijkstra_minTree(G, 0, P, D);
  92. printf("最短路径为:\n");
  93. for (int i = 1; i < G.vexnum; i++) {
  94. printf("%d - %d的最短路径中的顶点有:", i, 0);
  95. printf(" %d-", i);
  96. int j = i;
  97. //由于每一段最短路径上都记录着经过的顶点,所以采用嵌套的方式输出即可得到各个最短路径上的所有顶点
  98. while (P[j] != 0) {
  99. printf("%d-", P[j]);
  100. j = P[j];
  101. }
  102. printf("0\n");
  103. }
  104. printf("源点到各顶点的最短路径长度为:\n");
  105. for (int i = 1; i < G.vexnum; i++) {
  106. printf("%d - %d : %d \n", G.vexs[0], G.vexs[i], D[i]);
  107. }
  108. return 0;
  109. }


迪杰斯特拉算法查找顶点 0 到其它顶点所有最短路径的 Java 程序为:

  1. import java.util.Scanner;
  2. public class Dijkstra {
  3. static int V = 9; // 图中边的数量
  4. public static class MGraph {
  5. int[] vexs = new int[V]; // 存储图中顶点数据
  6. int[][] arcs = new int[V][V]; // 二维数组,记录顶点之间的关系
  7. int vexnum, arcnum; // 记录图的顶点数和弧(边)数
  8. }
  9. public static int LocateVex(MGraph G, int V) {
  10. int i = 0;
  11. // 遍历一维数组,找到变量v
  12. for (; i < G.vexnum; i++) {
  13. if (G.vexs[i] == V) {
  14. break;
  15. }
  16. }
  17. // 如果找不到,输出提示语句,返回-1
  18. if (i > G.vexnum) {
  19. System.out.println("顶点输入有误");
  20. return -1;
  21. }
  22. return i;
  23. }
  24. // 构造无向有权图
  25. public static void CreatDG(MGraph G) {
  26. Scanner scn = new Scanner(System.in);
  27. System.out.print("输入图的顶点数和边数:");
  28. G.vexnum = scn.nextInt();
  29. G.arcnum = scn.nextInt();
  30. System.out.print("输入各个顶点:");
  31. for (int i = 0; i < G.vexnum; i++) {
  32. G.vexs[i] = scn.nextInt();
  33. }
  34. for (int i = 0; i < G.vexnum; i++) {
  35. for (int j = 0; j < G.vexnum; j++) {
  36. G.arcs[i][j] = 65535;
  37. }
  38. }
  39. System.out.println("输入各个边的数据:");
  40. for (int i = 0; i < G.arcnum; i++) {
  41. int v1 = scn.nextInt();
  42. int v2 = scn.nextInt();
  43. int w = scn.nextInt();
  44. int n = LocateVex(G, v1);
  45. int m = LocateVex(G, v2);
  46. if (m == -1 || n == -1) {
  47. return;
  48. }
  49. G.arcs[n][m] = w;
  50. G.arcs[m][n] = w;
  51. }
  52. }
  53. // 迪杰斯特拉算法,v0表示有向网中起始点所在数组中的下标
  54. public static void Dijkstra_minTree(MGraph G, int v0, int[] p, int[] D) {
  55. int[] tab = new int[V]; // 为各个顶点配置一个标记值,用于确认该顶点是否已经找到最短路径
  56. // 对各数组进行初始化
  57. for (int v = 0; v < G.vexnum; v++) {
  58. tab[v] = 0;
  59. D[v] = G.arcs[v0][v];
  60. p[v] = 0;
  61. }
  62. // 由于以v0位下标的顶点为起始点,所以不用再判断
  63. D[v0] = 0;
  64. tab[v0] = 1;
  65. int k = 0;
  66. for (int i = 0; i < G.vexnum; i++) {
  67. int min = 65535;
  68. // 选择到各顶点权值最小的顶点,即为本次能确定最短路径的顶点
  69. for (int w = 0; w < G.vexnum; w++) {
  70. if (tab[w] != 1) {
  71. if (D[w] < min) {
  72. k = w;
  73. min = D[w];
  74. }
  75. }
  76. }
  77. // 设置该顶点的标志位为1,避免下次重复判断
  78. tab[k] = 1;
  79. // 对v0到各顶点的权值进行更新
  80. for (int w = 0; w < G.vexnum; w++) {
  81. if (tab[w] != 1 && (min + G.arcs[k][w] < D[w])) {
  82. D[w] = min + G.arcs[k][w];
  83. p[w] = k;// 记录各个最短路径上存在的顶点
  84. }
  85. }
  86. }
  87. }
  88. public static void main(String[] args) {
  89. MGraph G = new MGraph();
  90. CreatDG(G);
  91. int[] P = new int[V]; // 记录顶点 0 到各个顶点的最短的路径
  92. int[] D = new int[V]; // 记录顶点 0 到各个顶点的总权值
  93. Dijkstra_minTree(G, 0, P, D);
  94. System.out.println("最短路径为:");
  95. for (int i = 1; i < G.vexnum; i++) {
  96. System.out.print(i + " - " + 0 + " 的最短路径中的顶点有:");
  97. System.out.print(i + "-");
  98. int j = i;
  99. // 由于每一段最短路径上都记录着经过的顶点,所以采用嵌套的方式输出即可得到各个最短路径上的所有顶点
  100. while (P[j] != 0) {
  101. System.out.print(P[j] + "-");
  102. j = P[j];
  103. }
  104. System.out.println("0");
  105. }
  106. System.out.println("源点到各顶点的最短路径长度为:");
  107. for (int i = 1; i < G.vexnum; i++) {
  108. System.out.println(G.vexs[0] + " - " + G.vexs[i] + " : " + D[i]);
  109. }
  110. }
  111. }


迪杰斯特拉算法查找顶点 0 到其它顶点所有最短路径的 Python 程序为:

  1. V = 20 #顶点的最大个数
  2. INFINITY = 65535 #设定一个最大值
  3. P = [0]*V # 记录顶点 0 到各个顶点的最短的路径
  4. D = [0]*V # 记录顶点 0 到各个顶点的总权值
  5. class MGraph:
  6. vexs = []*V #存储图中顶点数据
  7. arcs = [[0]*V for i in range(V)] #二维列表,记录顶点之间的关系
  8. vexnum = 0 #记录图的顶点数和弧(边)数
  9. arcnum = 0
  10. G = MGraph()
  11. #根据顶点本身数据,判断出顶点在二维数组中的位置
  12. def LocateVex(G,v):
  13. #遍历一维数组,找到变量v
  14. for i in range(G.vexnum):
  15. if G.vexs[i] == v:
  16. break
  17. #如果找不到,输出提示语句,返回-1
  18. if i>G.vexnum:
  19. print("顶点输入有误")
  20. return -1
  21. return i
  22. #构造无向有权图
  23. def CreateDG(G):
  24. print("输入图的顶点数和边数:",end='')
  25. li = input().split()
  26. G.vexnum = int(li[0])
  27. G.arcnum = int(li[1])
  28. print("输入各个顶点:",end='')
  29. G.vexs = [int(i) for i in input().split()]
  30. for i in range(G.vexnum):
  31. for j in range(G.vexnum):
  32. G.arcs[i][j] = INFINITY
  33. print("输入各个边的数据:")
  34. for i in range(G.arcnum):
  35. li = input().split()
  36. v1 = int(li[0])
  37. v2 = int(li[1])
  38. w = int(li[2])
  39. n = LocateVex(G,v1)
  40. m = LocateVex(G,v2)
  41. if m == -1 or n == -1:
  42. return
  43. G.arcs[n][m] = w
  44. G.arcs[m][n] = w
  45. CreateDG(G)
  46. #迪杰斯特拉算法,v0表示有向网中起始点所在数组中的下标
  47. def Dijkstra_minTree(G,v0,P,D):
  48. #为各个顶点配置一个标记值,用于确认该顶点是否已经找到最短路径
  49. final = [0]*V
  50. #对各数组进行初始化
  51. for i in range(G.vexnum):
  52. D[i] = G.arcs[v0][i]
  53. #由于以v0位下标的顶点为起始点,所以不用再判断
  54. D[v0] = 0
  55. final[v0] = 1
  56. k =0
  57. for i in range(G.vexnum):
  58. low = INFINITY
  59. #选择到各顶点权值最小的顶点,即为本次能确定最短路径的顶点
  60. for w in range(G.vexnum):
  61. if not final[w]:
  62. if D[w] < low:
  63. k = w
  64. low = D[w]
  65. #设置该顶点的标志位为1,避免下次重复判断
  66. final[k] = 1
  67. #对v0到各顶点的权值进行更新
  68. for w in range(G.vexnum):
  69. if not final[w] and (low + G.arcs[k][w]<D[w]):
  70. D[w] = low + G.arcs[k][w]
  71. P[w] = k #记录各个最短路径上存在的顶点
  72. Dijkstra_minTree(G,0,P,D)
  73. print("最短路径为:")
  74. for i in range(1,G.vexnum):
  75. print("%d - %d的最短路径中的顶点有:"%(i,0),end='')
  76. print("%d-"%(i),end='')
  77. j = i
  78. #由于每一段最短路径上都记录着经过的顶点,所以采用嵌套的方式输出即可得到各个最短路径上的所有顶点
  79. while P[j] != 0:
  80. print("%d-"%(P[j]),end='')
  81. j = P[j]
  82. print("0")
  83. print("源点到各顶点的最短路径长度为:")
  84. for i in range(1,G.vexnum):
  85. print("%d - %d : %d"%(G.vexs[0], G.vexs[i], D[i]))


以上程序的执行过程为:

输入图的顶点数和边数:7 9
输入各个顶点:0 1 2 3 4 5 6
输入各个边的数据:
0 1 2
0 2 6
1 3 5
2 3 8
3 5 15
3 4 10
4 5 6
4 6 2
5 6 6
最短路径为:
1 - 0的最短路径中的顶点有: 1-0
2 - 0的最短路径中的顶点有: 2-0
3 - 0的最短路径中的顶点有: 3-1-0
4 - 0的最短路径中的顶点有: 4-3-1-0
5 - 0的最短路径中的顶点有: 5-3-1-0
6 - 0的最短路径中的顶点有: 6-4-3-1-0
源点到各顶点的最短路径长度为:
0 - 1 : 2
0 - 2 : 6
0 - 3 : 7
0 - 4 : 17
0 - 5 : 22
0 - 6 : 19

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2022-03-08 14:07  hanease  阅读(100)  评论(0编辑  收藏  举报