下面介绍的所有排序默认都为非递减排序!所有算法和测试用例见github
1.简单排序
简单排序算法主要包括冒泡排序、选择排序、插入排序以及由插入排序改进而来的希尔排序,其时间复杂度都为 <nobr aria-hidden="true"> nlogn </nobr> <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi> n </mi> <mi> l </mi> <mi> o </mi> <mi> g </mi> <mi> n </mi> </math>
1.1 冒泡排序
算法思想:
属于暴力解法
从第一个元素开始,两两比较大小,如果后者 < 前者,则交换两者顺序,以此类推,直到最大的元素“冒泡”到它的最终位置
代码如下:
#include <iostream>
#include <algorithm>
template <typename T>
void bubbleSort (T arr[], int n){
for (int i = 0 ; i < n - 1; i++){
for (int j = 0; j < n - 1 - i; j++){
if (arr[j + 1] < arr[j])
std::swap(arr[j + 1],arr[j]);
}
}
}
1.2 选择排序
算法思想:
属于暴力解法
每次循环遍历是,选择排序默认第一个元素即为本次循环最小的元素,然后依次与后面的元素进行比较,不断更新该次循环中的最小值,最终将得到的最小元素和默认的初始元素交换位置即可,以此类推不断进行比较。
思想和冒泡有点类似,不同点在于冒泡排序时前后元素两两比较,而选择排序是初始元素与后续所有元素进行比较。
代码如下:
#include <iostream>
#include <algorithm>
template <typename T>
void selectionSort(T arr[],int n){
int minIndex = 0;
for (int i = 0; i < n - 1;i++){
//这里只需要记录最小值的索引即可,最后交换arr[minIndex]和arr[i]即可,无需其他冗余的交换操作
minIndex = i;
for (int j = i + 1; j < n ;j++){
if (arr[j] < arr[minIndex])
minIndex = j;
}
std::swap(arr[minIndex],arr[i]);
}
}
1.3 插入排序
算法思想:
自下而上的减治算法
假设数组前面几个元素已经有序,插入排序的思想为,把未排序的第一个元素从后向前依次和已排序的元素进行比较,如果遇到比本身小元素,将未排序元素插入该元素后一个位置,以此类推……
代码如下:
#include <iostream>
template <typename T>
void insertionSort(T arr[],int n){
T target = 0;
int j = 0;
for (int i = 1;i < n;i++){
target = arr[i];
j = i - 1;
while (j >= 0 && arr[j] > target){
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = target;
}
}
需要说明的是,虽然插入排序的时间复杂度平均情况下为 <nobr aria-hidden="true"> O(n2) </nobr> <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi> O </mi> <mo stretchy="false"> ( </mo> <msup> <mi> n </mi> <mn> 2 </mn> </msup> <mo stretchy="false"> ) </mo> </math>,但当待排序数组近乎有序的时候,其时间复杂度退化为 <nobr aria-hidden="true"> O(n) </nobr> <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi> O </mi> <mo stretchy="false"> ( </mo> <mi> n </mi> <mo stretchy="false"> ) </mo> </math>,效率完全不亚于任何一种 <nobr aria-hidden="true"> O(nlogn) </nobr> <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi> O </mi> <mo stretchy="false"> ( </mo> <mi> n </mi> <mi> l </mi> <mi> o </mi> <mi> g </mi> <mi> n </mi> <mo stretchy="false"> ) </mo> </math>的高级排序算法,因此插入排序也常常作为优化子过程用在高级排序算法中,当高级排序算法将数组整理的近乎有序的时候,此时调用插入排序的效果是非常好的,下文高级排序算法中将会涉及到该优化思想。
1.4 希尔排序
算法思想:
希尔排序(由D.L. 希尔发明)是在总结并放大插入排序优点的基础上被创造出来的,可以理解为是插入排序的改进。
由上文知道,插入排序对近乎有序的元素进行排序相当高效,希尔排序就是根据给定的步长对元素进行插入排序,进而构造近乎有序的数组,并不断进行插入排序,显然这个步长必须以1作为结束。因此,该算法的时间复杂度更准确的说是依赖于步长队列的选取的。对于希尔排序,步长1,4,13,40……的效率是最高的,当然,使用的时候需要反过来。
代码如下:
#include <iostream>
template <typename T>
void shellSort(T arr[],int n){
int h = 1;
T target = 0;
int j = 0;
while (h <= n)
h = 3 * h + 1;
while (h > 0){
//下面即为步长为h的插入排序
for (int i = h;i < n ; i += h){
target = arr[i];
j = i - h;
while (j >= 0 && arr[j] > target){
arr[j + h] = arr[j];
j -= h;
}
arr[j + h] = target;
}
h /= 3;
}
}
2 高级排序
高级排序主要包括归并排序、快速排序和堆排序,算法复杂度平均情况下为 <nobr aria-hidden="true"> O(nlogn) </nobr> <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi> O </mi> <mo stretchy="false"> ( </mo> <mi> n </mi> <mi> l </mi> <mi> o </mi> <mi> g </mi> <mi> n </mi> <mo stretchy="false"> ) </mo> </math>,但是针对不同类型的数据表现出来的性能可能截然不同,下面将会讨论不同的算法适合的应用场景,以及该算法针对大多情况下的改进。
2.1 归并排序
算法思想:
归并排序是应用分而治之技术的一个完美例子
对于一个需要排序的数组A[0…n-1],归并排序将他们一分为二:A[0…n / 2]和A[n / 2 + 1],并对每个子数组递归排序,然后将两个排好序的子数组合并为一个有序数组。
首先我们考虑将数组分成A和B,并保证A和B都是有序的。这里用到了 递归 的思想,每个数组一分为二,知道分成的数组仅仅包含一个元素,因为我们认为一旦只包含一个元素,该数组即为有序数组。
归并的代码如下:
template <typename T>
void __mergeSort(T arr[],int l,int r){
//递归终止条件
if (l >= r){
return ;
}
int mid = (r + l) / 2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid + 1, r);
//分别排序完成后进行归并
__merge(arr, l, mid, r);
}
以上逻辑很简单,关键在于如何将两个已排序的数组进行归并。与其他排序算法不同的是,为此我们需要引入一个辅助数组来帮助我们完成归并。
我们将新开辟的经过赋值的辅助数组命名为aux(上图的第二个数组),原来的数组为arr(上图的第一个数组),用指针k(索引下标,下同)指向arr的第一个元素,并分别用指针i,j分别指向两个待合并的数组的第一个元素,然后比较这两个元素的大小,将较小的元素添加到arr[k],然后指针k后移,被复制的数组的指针后移。上述操作一直持续到aux两部分中的一个被处理完为止,然后,在未处理完的数组中,剩下的元素直接被复制到arr的尾部。
归并排序代码如下:
template <typename T>
//l为数组第一个元素的下标,mid为待归并数组中的第一个数组的最后一个元素的下标,r为数组最后一个元素的下标
void __merge(T arr[],int l,int mid,int r){
//声明辅助数组aux,注意元素个数(数组下标为闭区间)
T aux[r - l + 1];
//辅助数组赋值,注意arr下标从l开始,aux下标从0开始
for (int i = l;i <= r;i++){
aux[i - l] = arr[i];
}
//定义待归并数组的首元素指针
int i = l, j = mid + 1;
for (int k = l;k <= r;k++){
//判断指针i是否越过mid
//如果是,说明aux[l...mid]已经归并完成
if (i > mid){
arr[k] = aux[j - l];
j++;
}else if (j > r){//同理,检查j是否越过r
arr[k] =aux[i - l];
i++;
}else if (aux[i - l] < aux[j - l]){
arr[k] = aux[i - l];
i++;
}else{
arr[k] = aux[j - l];
j++;
}
}
}
最后,我们按照上面对简单排序的函数格式,对归并最后进行一下封装
template <typename T>
void mergeSort(T arr[], int n){
__mergeSort(arr,0,n-1);
}
2.1.1 归并排序性能测试
我们用函数自动顺序生成了100000个元素,并随即对其中10组进行交换,这样我们得到了一组近乎有序的数组,对同一样的样本数据,叫我们分别用插入排序和归并排序进行排序测试,测试结果如下:
由此得出,对近乎有序的数组(更极端点,对完全有序的数组),在大数量级的前提下,插入排序的表现要优于归并排序!
这是由于在近乎有序的情况下,插入排序会退化为 <nobr aria-hidden="true"> O(n) </nobr> <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi> O </mi> <mo stretchy="false"> ( </mo> <mi> n </mi> <mo stretchy="false"> ) </mo> </math>,对已排好序的元素不再进行排序处理。而归并排序依然沿用之前的规则,即使是有序的元素,还是会按照原定的策略进行排序。
2.1.2 归并排序优化
针对这一点,我们引出了归并排序的一点优化,代码如下:
template <typename T>
void __mergeSort(T arr[],int l,int r) {
//递归终止条件
// if (l >= r) {
// return ;
// }
//优化2:如果r-l<=15(随便选择),则利用插入排序
if ( r - l <= 15) {
insertionSort(arr,l,r);
return;
}
int mid = (r + l) / 2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid + 1, r);
//分别排序完成后进行归并
//优化1:如果近乎有序,则加入这个判断会有效提升性能
if (arr[mid] > arr[mid + 1])
__merge(arr, l, mid, r);
}
其中,插入排序的代码如下:
template <typename T>
void insertionSort(T arr[],int l, int r){
T target = 0;
int j = 0;
for (int i = l + 1;i <= r;i++){
target = arr[i];
j = i - 1;
while (j >= l && arr[j] > target){
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = target;
}
}
效果如下:
虽然还是比插入排序稍微慢一点,但是相比之前的归并排序,速度已经提升了不少
2.1.3 自下而上的归并排序算法
上文中介绍的归并排序属于自顶向下的方法,就是说从整体出发,将整体分成部分,再将部分合成整体。下面我们介绍自下而上的归并排序算法,先从小数组开始归并,然后将归并完成的数组(长度是小数组的2倍)再继续归并,图例如下:
自底而上的思想不需要递归,只需要迭代,代码如下:
#include <iostream>
#include <algorithm>
template <typename T>
void mergeSortBU(T arr[],int n){
for (int h = 1;h <= n;h += h){
//保证一定有两个数组进行归并,而不是一个
for(int i = 0;i + h < n;i += h + h){
__merge(arr,i,i + h - 1,std::min(r,i + h + h - 1));
}
}
}
由于较少利用了数组的索引特性,所以自底而上的归并排序可以扩展用来对链表进行排序,时间复杂度 <nobr aria-hidden="true"> O(nlogn) </nobr> <math xmlns="http://www.w3.org/1998/Math/MathML"> <mi> O </mi> <mo stretchy="false"> ( </mo> <mi> n </mi> <mi> l </mi> <mi> o </mi> <mi> g </mi> <mi> n </mi> <mo stretchy="false"> ) </mo> </math>
csdn居然有长度限制,无奈快速排序和堆排序只能分开来写了,详情见**