排序算法总结 比较排序和顺序时间排序总结 线性时间排序

  笔者前面在之前的博客当中已经说明了几种经常遇到的排序算法,分别是:插入排序、归并排序、堆排序、快速排序,现在分别对它们的基本思路进行一次复习。

1、插入排序

      基本思路:对于一个数组(下标从0开始,下同)当中的下标为i的数,如果前面的i个数已经排好序的话,那么只需要在前i个数当中为下标为i的数找到合适的位置插入就可以完成一次排序,接下来重复上述过程直到排序完毕。实现程序如下:

 1 #include "stdafx.h"
 2 int _tmain(int argc, _TCHAR* argv[])
 3 {
 4     int a[5] = { 6, 10, 5, 3, 7 };
 5     int array_length = sizeof(a) / sizeof(a[0]);//数组大小
 6     int key;
 7     for (int i = 1; i < array_length; i++)
 8     {
 9         key = a[i];
10         int j = i - 1;
11         while (j >= 0 && a[j]>key)
12         {
13             a[j + 1] = a[j];
14             j--;
15         }
16         a[j+1] = key;
17         for (int k = 0; k < array_length; k++)
18         {
19             printf("%d ", a[k]);
20         }
21         printf("
");
22 
23     }
24     for (int k = 0; k < array_length; k++)
25     {
26         printf("%d ", a[k]);
27     }
28     getchar();
29     return 0;
30 }

2、归并排序

      基本思路:运用了分治的思想,将一个待排序的数组A从中间分为两部分子数组A1和A2,对它们分别排序,然后将排完序之后的A1和A2组合成为一个有序的数组即可。对于A1和A2的排序重复上述过程即可,因此也是一个递归的过程。程序实现包括两部分:1、子有序数组的合并;2、程序递归。实现程序如下:

 1 #include "stdafx.h";
 2 #include <string>;
 3 using namespace std;
 4 //归并排序算法
 5 void Merge(int a[], int low, int middle, int high)//三个输入分别为待排序数组的下标,中间位置的下标和上标
 6 {
 7     int left_length = middle - low + 1;//左子数组的长度(包括下标为middle的数)
 8     int right_length = high - middle;//右子数组的长度
 9     int* L = (int*)malloc((left_length + 1)*sizeof(int));//多出来的一个数是放置一个哨兵牌,在后面的程序当中简化
10     int* R = (int*)malloc((right_length + 1)*sizeof(int));//同上
11     //将数组a中子数组当中的数复制到L当中
12     for (int i = low; i <= middle; i++)
13     {
14         L[i-low] = a[i];
15     }
16     //同上
17     for (int i = middle+1; i <=high; i++)
18     {
19         R[i-middle-1] = a[i];
20     }
21     L[left_length] = 10000;//设置哨兵牌
22     R[right_length] = 10000;//同上
23     int l = 0;//指示左子数组现在的比较数指针
24     int r = 0;//指示右子数组现在的比较数指针
25     //对两个子数组当中的数进行比较一一放入数组a当中
26     //从这段程序当中可以看出哨兵牌的作用:当一个子数组除了哨兵牌之外的数都已经放入a之后,另外一个子数组的数必定比哨兵牌小,所以另一个数组当中剩余的数会被放入到a当中,而且由于循环次数是两个数组总的非哨兵牌的数量和,哨兵牌必然不会被放入数组a当中
27     for (int i = low; i <= high; i++)
28     {
29         if (L[l] < R[r])
30         {
31             a[i] = L[l];
32             l++;
33         }else{
34             a[i] = R[r];
35             r++;
36         }
37     }
38 }
39 
40 void MergeSort(int a[], int low, int high)
41 {
42     if (low < high)
43     {
44         int middle = (low + high) / 2;
45         MergeSort(a, low, middle);
46         MergeSort(a, middle+1, high);
47         Merge(a, low, middle, high);
48     }
49 }
50 
51 int _tmain(int argc, _TCHAR* argv[])
52 {
53     int a[6] = { 1, 23, 2, 6, 7, 2 };
54     int array_length = sizeof(a) / sizeof(a[0]);//数组大小
55     MergeSort(a, 0, array_length - 1);
56     for (int k = 0; k < array_length; k++)
57     {
58         printf("%d ", a[k]);
59     }
60     getchar();
61     return 0;
62 }

3、堆排序

      比较重要的概念有:堆、最大堆

      基本思路:最大堆的根节点就是目前堆当中的最大数,将根元素和堆中最后的一个叶节点元素交换,然后对新堆进行一次“维护”,重复上述过程即可完成排序。在编程实现的过程当中有三步需要完成:1、维护最大堆性质的函数;2、调整数组当中数的位置来建立最大堆;3、排序。实现程序可以查看笔者写的另外一篇博客

4、快速排序

      基本思路:采用分治的策略。对于待排序数组A,取数组当中最后的一个元素作为“主元”,将小于“主元”的数都放置在“主元”的左边,大于“主元”的数都放置在“主元”右边,然后对“主元”左右两边的子数组分别进行快速排序。实现程序可以查看笔者写的这篇博客

      通过对上述四种常见算法的回顾可以发现它们都是原址排序,并且实现过程当中存在着将两个数之间进行比较获取两个数子之间顺序信息的基本过程,这种算法被称之为比较排序(《算法导论》当中有一小节专门讨论一下比较排序)。存在另外的一种排序方式,不需要进行比较的过程来获取数据之间顺序的信息,下面介绍三种排序方式:计数排序、基数排序、桶排序,它们的时间复杂度和输入的数据规模呈现线性关系。

线性时间排序

1、计数排序

      原理:对于一个数i如果能够找到数组当中有k个小于等于它的数那个i的位置应该放在第k+1个位置。

      基本思路:对于一个待排序数组a其中的整数都属于{0,k}这个集合并且最大数为k,准备另外两个数组b和c。其中b数组的长度为k+1这意味着它的下标能够包含数组a当中所有的值,b数组当中某一个位置存储的值就是a数组当中小于或者等于当前位置下标的数的个数。最后根据b数组当中的信息和上述原理,将a数组中的数一一放入c数组当中的合适位置,完成排序。实现程序如下:

 1 #include "stdafx.h"
 2 #include <string>
 3 using namespace std;
 4 //计数排序算法
 5 void CountSort(int a[], int b[], int k,int length)//a是要被排序数组,b是被排好序的数组,k是a中的最大值,length是数组a的数组长度
 6 {
 7     int* c=(int*)malloc((k+1)*sizeof(int));//创建一个包含k+1个数的整数数组
 8     //对c数组当中的元素赋值为0
 9     for (int i = 0; i <= k; i++)
10     {
11         c[i] = 0;
12     }
13     //找到数组a当中的值和c数组下标相同的数,每找到一个就增加数组c对应的计数
14     for (int i = 0; i < length; i++)
15     {
16         c[a[i]]++;
17     }
18     //在c[a[i]]当中的记录是小于或等于a[i]数的个数
19     for (int i = 1; i < k + 1; i++)
20     {
21         c[i] = c[i] + c[i - 1];
22     }
23 
24     for (int i = length-1; i >= 0; i--)
25     {
26         b[c[a[i]]-1] = a[i];
27         c[a[i]]--;
28     }
29 }
30 
31 int _tmain(int argc, _TCHAR* argv[])
32 {
33     int a[6] = { 1, 23, 2, 6, 7,2 };
34     int array_length = sizeof(a) / sizeof(a[0]);//数组大小
35     int max_a=a[0];//存储数组a当中的最大值
36     for (int i = 1; i < array_length; i++)
37     {
38         if (max_a <= a[i])
39         {
40             max_a = a[i];
41         }
42     }
43     int* b = (int*)malloc(array_length*sizeof(int));
44     CountSort(a, b, max_a, array_length);
45     for (int k = 0; k < array_length; k++)
46     {
47         printf("%d ", b[k]);
48     }
49     getchar();
50     return 0;
51 }

2、基数排序

  应用场景:当一个数组当中存储的数字均为有d位的数字式,如A[123,213,321,231],其中的元素都为3位(个、十、百)的数字,将这一数组进行排序。

  基本原理:如果数组中的数都是d位的,那么从最小位开始遍历,根据数字的每一位来进行一次“稳定”排序,思路可以参考下图(以数组A[123,321,213,231]非递增排序为例):

排序算法总结
比较排序和顺序时间排序总结
线性时间排序

  这里一定要注意必须是稳定排序(可以使用上述算法当中的:插入排序、快速排序、基数排序),不然最后排序结果不对。关于各种算法的稳定性可以看这一个博客,上面总结得比较全。

3、桶排序

      桶排序是基于这样一个假设:所输入的数据服从正态分布。即所有的数字(共n个)按照正态分布取自区间[0,1),将这一区间平均划分为n份,或者称n个“桶”,每一个桶当中放置在这个桶所设定的范围当中的数,将待排序数组当中的数放入自己相应的桶当中,最后对每一个“桶”进行一次排序,然后将每一个桶按照顺序合并起来即可完成排序。

      对于以上排序笔者目前将它们的基本思路记录了下来,并且对常见算法提供了C++实现。对算法的运行效率的分析留到以后重新开一个博客来专门进行总结。