1. 非比较排序
上一篇文章我们罗列了数据结构中排序的八种方法。这八种方法都是需要比较才能实现的,那怎么样才可以通过非比较的方法来实现数组的排序呢?这里就提供一种非比较排序的方法。
具体的操作思路如下:
1. 先统计待比较数组arr中重复数组的个数,并相对应的记录下来。
2. 在另一个第二个新开辟的数组count里,将arr数组中的元素当作下标,将对应元素出现的个数作为元素对应放。
3. 然后遍历count数组里面的元素,如果元素不为零,就将这个数据的下标作为元素返回给原数组arr,将这个数据的元素作为返回次数继续返回。
4. 原数组arr此时就有序了。
我们可以根据上面的思路看一下下面这个例子:
上面的数组为arr,下面的数组为count,先看arr数组中,6出现1次,1出现2次,2出现2次,9出现1次,4出现3次。所以在count数组中,下标为6的位置为1,下标为1的位置为2,下标为2的位置为2,下标为9的位置为1,下标为4的位置3。
然后遍历count数组,下标为0没有元素,跳过,下标为1有2,那么就在arr数组中从下标为0开始,连续放两个1,然后继续遍历count数组,连续放两个2,三个4,1个6,1个9。就得到了新的arr数组1 1 2 2 4 4 4 6 9。
这种方法需要新开辟一个数组用于暂时存放数组,而这个新开辟的count数组大小与原arr数组中的最大元素是息息相关的。现在我们试着排下面这个数组:
由第一个例子我们不难知道,开辟count数组的空间就需要109+1=110(看上面的例子,要以元素中的最大值作为下标,就需要比最大值还要多一个空间大小,因为还有下标为0的位置)个整形大小的空间。而这个数组中最小值为100,意思是在tmp数组中,只有在下标为100的时候才会有元素出现,这样子的话前面99个整形大小的空间就被浪费了。并且如果数组里面的元素有负数的话,又不可能将这个负数的个数放在下标为负数的空间里。而我们利用计算机排序的数组中,这两种情况更多。所以我们可以采用下面这种优化的方法。
1. 同上,先将arr数组中的元素以及元素重复的个数对应的存储下来。数组的最大值为max,最小值为min。
2. 开辟max-min+1个空间作为count数组的空间大小。现在开辟的空间里面的值都是随机的,我们通过memset函数将空间里面的整型都初始化为0。
3. 假设arr数组中下标为i的arr[i]元素重复了n次,那么就在count数组中下标为arr[i]-min的位置定为n。
4. 当arr数组的情况全部放入count数组的后,遍历count数组,将count数组中元素不为零的下标为j的数据作为放回去的次数,值为i+min,从arr[0]开始依次放回去。
5. 此时arr数组的元素全部有序。
现在我们继续以上面的示例数组{100,101,109,105,101,105}为例详细讲解一下这个过程:
首先原数组依然命名为arr,新开辟的数组为count。arr数组中最大值为109,最小值为100。所以开辟的count数组的大小为109-100+1=10个整型空间大小。100出现的次数为1次,101出现的次数为2次,109出现的个数为1次,105出现的个数为2次。所以在count数组中,下标为100-100=0的位置的数据为1,下标为101-100=1的位置的数据为2,下标为109-100=9的位置的数据为1,下标为105-100=5的位置的数据为2。
然后遍历count数组,若数组元素为0的话直接跳过,若不为0,则将下标为j的元素count[j]作为上传次数,将j+min作为上传的数据,从arr[0]开始依次按顺序上传上去。连续放count[j]个大小为j+min的数据。最后就得到有序的数组arr{100,101,101,105,105,109}.
通过这个过程,我们可以写一下代码:
//非比较排序
void CountSort(int* arr, int n)
{//先找数组arr中最大值max和最小值min,这里还是需要用到比较int min = arr[0];int max = arr[0];for (int i = 0; i < n; i++){if (arr[i] > max){max = arr[i];}if (arr[i] < min){min = arr[i];}}//开辟range个整型空间大小的数组countint range = max - min + 1;int* count = (int*)malloc(sizeof(int) * range);if (count == NULL){perror("malloc fail!");exit(1);}//将count数组里面的元素都初始化为0memset(count, 0, sizeof(int) * range);//将arr中下标为i的元素arr[j]-min作为下标传给count,将arr中元素出现的次数作为对应数值传给countfor (int i = 0; i < n; i++){count[arr[i] - min]++;}//将count中的下标j加上min作为数值传给arr,将count[j]作为连续的上传次数int index = 0;for (int j = 0; j < range; j++){while (count[j]--){arr[index++] = j + min;}}
}
我们可以根据下面这个代码测试一下排序算法所需要的时间:
void test()
{srand((unsigned int)time(0));const int N = 100000;int* a1 = (int*)malloc(sizeof(int) * N);int* a2 = (int*)malloc(sizeof(int) * N);int* a3 = (int*)malloc(sizeof(int) * N);int* a4 = (int*)malloc(sizeof(int) * N);int* a5 = (int*)malloc(sizeof(int) * N);int* a6 = (int*)malloc(sizeof(int) * N);int* a7 = (int*)malloc(sizeof(int) * N);int* a8 = (int*)malloc(sizeof(int) * N);for (int i = 0; i < N; i++){a1[i] = rand();a2[i] =a1[i];a3[i] = a1[i];a4[i] = a1[i];a5[i] = a1[i];a6[i] = a1[i];a7[i] = a1[i];a8[i] = a1[i];}//测试冒泡排序int begin1 = clock();BubbleSort(a1, N);int end1 = clock();//测试直接插入排序int begin2 = clock();InsertSort(a2, N);int end2 = clock();//测试希尔排序int begin3 = clock();ShellSort(a3, N);int end3 = clock();//测试直接选择排序2int begin4 = clock();SelectSort2(a4, N);int end4 = clock();//测试快排hoare版本int begin5 = clock();QuickSort(a5, 0, N - 1);int end5 = clock();//测试非递归版本int begin6 = clock();QuickSortNonR(a6, 0, N - 1);int end6 = clock();//测试归并排序int begin7 = clock();MergeSort(a7, N);int end7 = clock();//测试非比较排序int begin8 = clock();CountSort(a8, N);int end8 = clock();printf("BubbleSort:%d\n", end1 - begin1);printf("InsertSort:%d\n", end2 - begin2);printf("ShellSort:%d\n", end3 - begin3);printf("SelectSort2:%d\n", end4 - begin4);printf("QuickSort:%d\n", end5 - begin5);printf("QuickSortNonR:%d\n", end6 - begin6);printf("MergeSort:%d\n", end7 - begin7);printf("CountSort:%d\n", end8 - begin8);free(a1);free(a2);free(a3);free(a4);free(a5);free(a6);free(a7);free(a8);}
代码的结果如下:
我们可以看到,冒泡排序的效率最低,快排第二,而非比较排序效率第一。所以说非比较排序在某些时候使用起来效率是最高的。但是有些时候不能使用非比较排序:
1. 很多数据不集中的时候不能使用非比较排序。
2.非比较排序不能排小数。
2. 排序算法复杂度及稳定性分析
稳定性就是两个相同的数据在排序完成前和排序完成后的相对位置不改变的话就稳定,如果改变的话就不稳定。
不稳定的四种情况可以通过下面四种情况进行实验即可得出结论:
直接选择排序:5 8 5 2 9
希尔排序:5 8 2 5 9
堆排序:2 2 2 2
快速排序:5 3 3 4 3 8 9 10 11