1. 归并排序概述
归并排序,其排序的实现思想是先将所有的记录完全分开,然后两两合并,在合并的过程中将其排 好序,最终能够得到⼀个完整的有序表。
归并思想:
大问题,拆分成小问题、而且要求每个小问题都是独立的
1.1 归并排序的执⾏流程:
1. 不断地将当前序列平均分割成2个⼦序列;例如下⾯的序列,被分割成2个⼦序列,然后继续将这些⼦序列分割成⼦序列,直到不能再分割位置(序列中只剩⼀个元素)
2. 接下来,在不断的将两个⼦序列合并成⼀个有序序列;也就是说,刚刚是拆分,现在是合并
由于是不断的合并成⼀个有序序列,所以最终只剩下⼀个有序序列:
1.2 分割的过程
由于在分割时,都是将⼀个有序序列分割为2个两个⼦序列,并且该操作是重复执⾏的,所以肯定会使⽤到递归。由于是从中间进⾏分割,所以需要计算出中间的位置。所以实现流程是:
- 计算拆分的中间位置
- 分别继续对左边序列和右边序列进⾏拆分
1.3 合并的过程
在merge时,肯定会得到2个有序的数组。所以要做的事情就是将这两个有序的数组合并为⼀个有序的数组。现有两个有序的数组,然后根据这两个有序的数组合并为⼀个。
现在要将这两个有序序列合并为⼀个更⼤的有序序列,所以可以先⽐较两个序列的头元素,谁的值 ⽐较⼩,就先放⼊⼤序列中,利⽤两个索引,分别记录两个序列中待⽐较值的下标。然后将值⼩的序列下标右移⼀个单位,继续⽐较。最终将两个有序数组合并为⼀个的流程图如下:
合并后,会出现merge左边先结束,merge右边先结束的情况。
#ifndef MERGE_SORT_H
#define MERGE_SORT_H
#include "sortHelper.h"
void mergeSort(SortTable *table);
#endif
#include "mergeSort.h"
// merge合并
static void merge(SortTable *table, int left, int mid, int right)
{
// 1. 将左右任务的区域进行拷贝,方便从拷贝的空间里,按照归并的思想,向原空间填入有序的值
int n1=mid-left+1;
int n2=right-mid;
// 分配aux1和aux2
Element *aux1 = (Element*)malloc(sizeof(int) * n1);
Element *aux2 = (Element*)malloc(sizeof(int) * n2);
if(aux1==NULL || aux2==NULL)
{
printf("aux malloc error!\n");
exit(0);
}
// 将左右任务的子空间进行拷贝
for(int i=0;i<n1;i++)
{
aux1[i]=table->data[left+i];
}
for(int j=0;j<n2;j++)
{
aux2[j]=table->data[mid+1+j];
}
// 2. 将有序的临时aux1和aux2的空间,进行归并
int i=0;
int j=0;
int k=left;
// 归并任务的访问区域[left...right]
while(i<n1 && j<n2)
{
if(aux1[i].key<=aux2[j].key)
{
table->data[k++]=aux1[i++];
}
else {
table->data[k++]=aux2[j++];
}
}
// 把还有值的区域,填入到结果区域
while(i<n1)
{
table->data[k++]=aux1[i++];
}
while(j<n2)
{
table->data[k++]=aux2[j++];
}
// 3. 释放临时空间
free(aux1);
free(aux2);
}
// 设计一个通用的任务,在某一个区间内,完成归并算法
// 归并任务的访问区域[left...right]
static void mergeLoop(SortTable *table, int left, int right)
{
// 递归出口
if (left >= right) {
return;
}
// 中间位置
int mid = (left + right) / 2+left;
// 合并
mergeLoop(table, left, mid);
mergeLoop(table, mid + 1, right);
// 合并
merge(table, left, mid, right);
}
// 归并排序
void mergeSort(SortTable *table)
{
mergeLoop(table, 0, table->length - 1);
}
2. 计数排序
前⾯介绍的冒泡,选择,插⼊,归并,快速,希尔,堆排序,都是基于⽐较的排序,这些基于⽐较 的排序,有以下⼏个特点:平均时间复杂度最低的是O(nlogn)。
⽽计数排序,不是基于⽐较的排序。其中不基于⽐较的排序还有桶排序,基数排序等。
它们是典型的⽤空间换时间,在某些时候,平均时间复杂度可以⽐O(nlogn)更低,也就是说,在某 些时候,这种利⽤空间换时间的排序算法,性能⽐前⾯基于⽐较的排序算法更快。
2.1 计数排序
计数排序是在1954年由Harold H.Seward提出,适合对⼀定范围内的整数进⾏排序。
计数排序核⼼思想:
统计每个整数在序列中出现的次数,进⽽推导出每个整数在有序序列中的索引。
我们以数组[1,4,1,2,5,2,4,1,8]为例进⾏说明。
第⼀步:建⽴⼀个初始化为 0 ,⻓度为 9 (原始数组中的最⼤值 8 加 1) 的数组count[]。
第⼆步:遍历数组 [1,4,1,2,5,2,4,1,8] ,访问第⼀个元素 1 ,然后将数组 标为 1 的元素加 1,表示当前 1 出现了⼀次,即 count[1] = 1 ; 依次遍历,对count进⾏统计。
2.2 计数排序的改进
上述算法的问题如下:
- ⽆法对负整数进⾏排序
- 极其浪费内存空间
- 是⼀个不稳定排序
2.2.1 优化1
只要不再以输⼊数列的 [最⼤值+1] ,作为统计数组的⻓度,⽽是以数列 [最⼤值-最⼩值+1] 作为统 计数组的⻓度即可。数列的最⼩值作为⼀个偏移量,⽤于计算整数在统计数组中的下标。
⽐如,假设下⾯的数组的数列:
95,94,91,98,99,90,99,93,91,92
统计出数组的⻓度为99-90+1=10,偏移量等于数列的最⼩值90。对于第1个整数95,对应的统计数组下标是95-90=5。
2.2.2 优化2
假设数据如下:
上述数据按照计数排序得到的结果如下:
如何判断5中的2个⼈谁是C谁是D那。那么需要进⾏变形。
这是如何变形的呢?其实就是从统计数组的第2个元素开始,每⼀个元素都加上前⾯所有元素之和。
这样相加的⽬的,是让统计数组存储的元素值,等于相应整数的最终排序位置的序号。例如,下标 是9的元素值为5,代表原始数列的整数9,最终的排序在第5位。
⾸先,遍历成绩表最后⼀⾏E的成绩,E是95,那么5下标元素是4,表示E在最后的排名在第4位, 同时将原来的值减1,表示下次再遇到95的成绩时,最终排名是第3。
3. 桶排序
其实桶排序重要的是它的思想,⽽不是具体实现,桶排序从字⾯的意思上看:
- 若⼲个桶,说明此类排序将数据放⼊若⼲个桶中。
- 每个桶有容量,桶是有⼀定容积的容器,所以每个桶中可能有多个元素。
- 从整体来看,整个排序更希望桶能够更匀称,即既不溢出(太多)⼜不太少。
假设有⼀个⾮整数数列,如下:
4.5, 0.84, 3.25, 2.18, 0.5
桶排序的第1步,就是创建这些桶,并确定每⼀个桶的区间范围。
具体需要建⽴多少个桶,如何确定桶的区间范围,有很多种不同的⽅式。我们这⾥创建的桶数量等 于原始数列的元素数量,除最后⼀个桶只包含数列最⼤值外,前⾯各个桶的区间按照⽐例来确定。
区间跨度=(最⼤值-最⼩值)/(桶的数量-1)
第2步,遍历原始数列,把元素对号⼊座放⼊各个桶中。
第3步,对每个桶内部的元素分别进⾏排序。
第4步,遍历所有的桶,输出所有元素。
4. 基数排序
与基于⽐较的排序算法(归并排序、堆排序、快速排序、冒泡排序、插⼊排序等等)相⽐,基于⽐ 较的排序算法的时间复杂度最好也就是O(nlogn),⽽且不能⽐ O(nlogn)更⼩了。
计数排序(Counting Sort)的时间复杂度为O(n)量级,更准确的说,计数排序的时间复杂度 为 O(n + k),其中k表示待排序元素的取值范围(最⼤与最⼩元素之差加 1 )。那么问题来了,当这个元 素的的范围在 1 到 n^2 怎么办呢?
此时就不能⽤计数排序了奥,因为这种情况下,计数排序的时间复杂度达到了O(n^2)量级。
⽐如对数组 [170, 45, 75, 90, 802, 24, 2, 66] 这个⽽⾔,数组总共包含 8 个元素,⽽数组中的最⼤ 值和最⼩值之差为 802 - 2 = 800 ,这种情况下,计数排序就 ”失灵了“ 。
那么有没有那种排序算法可以在线性时间对这个数组进⾏排序呢?
基数排序(Radix Sorting) 。基数排序的总体思想就是从待排序数组当 中,元素的最低有效位到 最⾼有效位 逐位 进⾏⽐较排序;此外,基数排序使⽤计数排序作为⼀个排序的 ⼦过程。