经典的内排序算法包括冒泡排序、选择排序、插入排序、希尔排序、堆排序、归并排序、快速排序等。冒泡排序和直接选择排序的的时间复杂度决定了基本没有排序算法会应用到它们,因此,这里就忽略掉对他们的介绍。
直接插入排序
类似于我们整理手中的扑克牌,直接插入排序的要点在于将数组分为两部分,左边的部分已经有序,右边是还无序的待排序部分。
void insertSort(vector<int> input)
{
if (input.size() < 2)
return;
int temp = 0;
int i, j;
for (i = 1; i < input.size(); ++i)
{
//出现了逆序对,说明需要进行插入排序,否则,说明inpu[i]不小于左边所有数,不需要进行插入。
if (input[i] < input[i - 1])
{
temp = input[i];
for (j = i-1; j>=0 && input[j] > temp; --j)
input[j + 1] = input[j]; //向后移动
input[j+1] = temp; //找到插入点,进行插入
}
}
for (auto v : input)
cout << v << ",";
}
分析代码可知,是否进行插入操作的条件在于是否发现了逆序对。虽然插入排序的平均和最差复杂度为O(N^2),但如果数组基本有序或者数组大小很小,这时候逆序对数目很少,很适合使用插入排序。而且,直接插入排序是一种稳定排序。
虽然直接插入排序性能要优于冒泡排序和直接选择排序,但仍然为O(N^2),希尔排序作为直接插入排序的优化算法,作为第一个突破平方复杂度的排序算法,复杂度为O(nlogn~n^2)。希尔排序主要思想就是将数组划分为子数组,然后各自排序,使得整体数组接近基本有序,这又强调了插入排序的应用场景:数组基本有序或者数组大小很小。希尔排序的难点在于如何划分子数组,不同于分段划分,希尔排序设置了一个增量值,将相隔某个增量的数组元素划分为子数组,使得排序效率提高。而增量值的确定,则是一个研究中的问题。
堆排序
堆排序对原始记录的初始排序状态不敏感,无论最好,最坏还是平均时间复杂度均为O(nlogn)。由于记录的交换是跳跃进行的,因此是不稳定排序。
//调整堆(堆顶不满足堆性质时)
void AdjustHeap(vector<int>& input, int p, int q)
{
int temp = input[p];
for (int j = 2 * p + 1; j <= q; j = 2 * j + 1)
{
//j<q表明左右子节点都存在,这时,定位到孩子中较大的那个
if (j < q && input[j] < input[j + 1])
++j;
//比较孩子中较大的那个与父节点大小,按照大小情况调整位置
if (temp < input[j])
{
input[p] = input[j]; //父节点小于孩子节点,将孩子节点调整到父节点上,
p = j;
}
else
break; //堆顶元素大于孩子节点元素,此时说明堆得性质并没有被打破,直接退出循环
}
input[p] = temp;
}
void heapSort(vector<int> input)
{
int len = input.size();
//对所有非叶子节点进行堆调整,最终使得整棵树成为大顶堆
for (int i = len / 2 - 1; i >= 0; --i)
{
//堆作为一棵完全二叉树,满足对于节点i,其子节点为2*i+1和2*i+2
AdjustHeap(input, i, len - 1);
}
for (int i = len - 1; i > 0; --i)
{
//交换堆顶与最后一个元素
int temp = input[0];
input[0] = input[i];
input[i] = temp;
AdjustHeap(input, 0, i - 1);
}
for (auto v : input)
cout << v << ",";
}
堆排序构建初始堆时,需要比较的次数较多,因此,
不适合数组个数较少的情况,而且,由于是迭代计算,
不会产生递归调用的成本。
快速排序
快速排序是应用最广,在各种场景下平均性能最好的的排序算法。它的主要思想就是每次选择一个数作为轴(pivot),经过一趟比较和交换之后,轴左边的数均小于轴,右边的数均大于轴。然后对于轴两边的子数组序列递归进行以上步骤。
int partition(vector<int>& input, int p, int q)
{
int key = input[p];
while (p < q)
{
while (p < q && key <= input[q])
--q;
//交换
int temp = input[q];
input[q] = input[p];
input[p] = temp;
while (p < q && key >= input[p])
++p;
//交换
temp = input[p];
input[p] = input[q];
input[q] = temp;
}
return p;
}
//对[p,q]范围内的元素进行快排
void qSort(vector<int>& input, int p, int q)
{
int pivot;
if (p < q)
{
pivot = partition(input, p, q); //算出轴的位置
//对轴两边的元素递归调用
qSort(input, p, pivot - 1);
qSort(input, pivot + 1, q);
}
}
void quickSort(vector<int> input)
{
qSort(input, 0, input.size() - 1);
for (auto v : input)
cout << v << ",";
}
快速排序最好与平均复杂度为O(nlogn),最差情况下为O(n^2)。虽然性能优越,应用最广,但是需要注意这么几点:
1.快速排序对于轴的选取很敏感,因此一般会进行优化,比如取第一个,最后一个和中间元素三个元素中排在中间的那个
2.快速排序递归调用时,如果递归层数太深,会产生大量递归调用,这时经常会使用堆排序替代快排
3.快速排序是不稳定排序,如果某些排序要求稳定性,通常使用归并排序替代。
比如,SGI STL的排序算法实现的原理就是,先设定一个元素个数阈值,如果待排元素少于阈值,使用插入排序。在元素大于阈值的情况下,先计算递归层数,如果递归层数大于另一个设定的阈值,则使用堆排序,否则使用快速排序。同时,SGI STL提供了归并排序算法供需要稳定排序而又性能要求高的用户使用。
归并排序
归并排序是一种高性能同时能提供稳定排序的算法。
void merge(vector<int>& input, int L, int M, int R)
{
int LEFT_SIZE = M - L + 1;
int RIGHT_SIZE = R - M;
vector<int> left(LEFT_SIZE);
//left.clear();
vector<int> right(RIGHT_SIZE);
//right.clear();
//填充左右数组
for (int i = 0; i < LEFT_SIZE; ++i)
left[i] = input[L+i];
for (int j = 0; j < RIGHT_SIZE; ++j)
right[j] = input[M + 1 + j];
//合并左右数组
int i = 0, j = 0, k = L;
while (i < LEFT_SIZE && j < RIGHT_SIZE)
{
if (left[i] <= right[j])
{
input[k] = left[i];
k++;
i++;
}
else
{
input[k] = right[j];
k++;
j++;
}
}
while (i < LEFT_SIZE)
{
input[k] = left[i];
k++;
i++;
}
while (j < RIGHT_SIZE)
{
input[k] = right[j];
k++;
j++;
}
}
void mSort(vector<int>& input, int L, int R)
{
if (L == R)
return;
int M = (L + R) / 2;
//递归划分数组
mSort(input, L, M);
mSort(input, M + 1, R);
//合并
merge(input, L, M, R);
}
void mergeSort(vector<int> input)
{
mSort(input, 0, input.size() - 1);
for (auto v : input)
cout << v << ",";
}
由代码可知,归并排序分为两个过程:
1.问题分治,直到子序列只剩一个数为止
2.结果合并,将两个已排序的子序列合并为完整的序列
总结
对于经典的内排序算法,如果不考虑稳定性,在数量比较大的情况下,如果递归层次不是很多,选择快速排序,如果递归层次太多,选用堆排序。在小数据量时,选择插入排序。如果稳定性是很重要的考量,则选择归并排序。