序
- 在之前的笔记(二)中记录了初等排序,分别是插入、冒泡和选择排序。而在这次的笔记中将记录高等排序。
- 对于初等排序,时间复杂度为O(n2)。
- 而对于高等排序,可以达到O(nlogn),甚至在特定条件下还可以达到线性时间复杂度 O(n)。
高级排序
- 归并排序
- 快速排序
- 计数排序
归并排序
- 所谓归并,是指将两个有序的数组合并为一个有序的大数组。
- 不断重复对某个数组的子数组归并的过程,就可以将这个数组完全排序。
(图片来自VisuAlgo)
- 对于归并排序来说,不断重复归并的过程非常符合分治法的思想,所以我们可以采用递归来解决这个问题。
对于一个数组,首先将它一分为二
然后对左右两边的子数组再进行分割操作,直到每个小子数组的元素个数为1
将元素个数为1的两个相邻小数组通过合并,合为一个元素数为2的有序数组
重复此过程,合成元素数为4,8,16……的数组,直到完全排序完毕
- 代码如下
//首先来构造归并函数
void merge(int *array, int left, int right, int mid){
int n1 = mid - left; //左边数组的长度
int n2 = right - mid; //右边数组的长度
int Left[n1+1],Right[n2+1]; //声明两个数组用来存放排好序的左右数组
//将左边数组读入
for (int i = 0; i < n1; i++)
Left[i] = array[left + i];
//将右边数组读入
for (int i = 0; i < n2; i++)
Right[i] = array[mid + i];
//将最后一位设为最大,以防在读入最后一位时无法在左右数组之间作比较
Left[n1] = INT_MAX;
Right[n2] = INT_MAX;
//定义两个下标,分别为左数组头和右数组的头
int pointer1 = 0, pointer2 = 0;
//将左右数组合并
for (int k = left; k < right; k++){
//如果左数组的头小,则将左数组的头写入原数组,并将左数组的头向后移动一位
if (Left[pointer1] <= Right[pointer2]){
array[k] = Left[pointer1++];
}
//右边同理
else{
array[k] = Right[pointer2++];
}
}
}
//接下来我们构造递归函数
void mergeSort(int *array, int left, int right){
//当数组的个数大于1的时候进行,等于一直接返回
if (right - left > 1){
int mid = (right + left)/2; //找出中间值
mergeSort(array, left, mid); //先分割,对左边数组进行操作
mergeSort(array, mid, right); //对右边数组进行操作
merge(array, left, right, mid); //执行归并操作
}
}
- 归并排序是稳定排序,因为我们在进行归并操作的时候如果满足条件Left[pointer1] <= Right[pointer2],也就是说如果遇到相等的元素,优先选择左边数组的,这样就不会造成换位。对于归并排序来说,我们从元素数为1的小数组开始归并,然后是元素数为2、4、8……的数组进行排序,而归并操作要遍历每个子数组,所以时间复杂度为O(nlogn)。归并排序非常适合整体呈现无序性,但局部有序的数据进行排列。
快速排序
- 虽然归并排序很快,但是在执行归并操作的时候需要额外的空间来存储当前的有序子数组,而快速排序则更快,并且不需要额外的空间。
- 快速排序的核心思想就是每次归位一个数,而归位的方法就是让它左边的数都小于它,而它右边的数都大于它。我们可以利用交换的思想来实现,首先从左边开始,找出一个大于这个数的数,再在右边找一个小于它的数,然后交换它们。最后把这个数移到中间,然后再分别对其左边和右边进行上述操作直至排序完成。
- 具体方法如图*(图片来自VisuAlgo)*
- 代码如下
void quickSort(int *array, int left, int right){
//递归的终止条件是左索引等于右索引,也就是只有一个数的情况
if (right<=left) return;
//首先记录下数组最左边的值,以它为基准,将大于它的放在右边,小于它的放在左边
int temp = array[left];
int i = left, j = right;
//创建了两个用来循环的下标,i从左开始,j从右开始
while (i<j){
//先从右向左找,找到小于temp的数
while(array[j]>=temp&&j>i){
j--;
}
//把当前的比array小的数放到array[i]里
array[i] = array[j];
//然后从i开始向右找比temp大的数
while(array[i]<temp&&i<j){
i++;
}
//把这个数放到array[j]里
array[j] = array[i];
}
//最后循环退出,i与j相等,再把记录下的temp值填入
array[i] = temp;
//递归排序temp的左右两边
quickSort(array, i+1, right);
quickSort(array, left, i-1);
}
- 快速排序会交换不相邻的元素,所以快速排序是不稳定排序。而假如在选择每次都能恰好选择到中间值,则算法的时间复杂度为O(nlogn)。如果使用上述代码,即每次选择最左边的作为中间值,那么对于已经排好序的数组,时间复杂度可能会高达O(n2)。所以,我们可以考虑随机选择中间值。
计数排序
- 计数排序的核心思想就是记录比这个数小的元素的个数,然后直接将这个数放到数组中它该在的位置。
如图 (图片来自VisuAlgo)
//参数分别为原始数组,排序后数组,最大元素,数组中元素个数
void countingSort(int *array, int *output, int maxNum, int n){
int count[maxNum] = {0}; //初始化计数数组为0
//遍历原始数组,统计array[i]出现的次数
for (int i = 0; i < n; i++){
count[array[i]]++;
}
//对所有的计数累加,即计算出有多少小于它的元素
for (int i = 1; i <= maxNum; i++){
count[i] += count[i-1];
}
//逆向遍历数组,保证稳定性
for (int i = n - 1; i >= 0; i--){
//遍历原始数组,根据计数数组中的对应值将array[i]填到结果数组的对应位置
output[count[array[i]]-1] = array[i];
//更新计数数组
count[array[i]]--;
}
}
- 在上述代码中,只要逆序遍历数组,就能保证排序的稳定性,因为在添加计数的时候靠后的相同元素后进入计数,所以在输出时要先取出后进入的元素。对于以空间换时间的计数排序,分别遍历原始数组和计数数组,其时间复杂度为O(n+k)(其中n为数组长度,k为计数数组长度),达到了线性时间复杂度,快于归并排序和快速排序。但是当k很大时,其效率远不如快速排序,并且占用了额外的空间,所以计数排序适合对数据较小的数组进行排序。