在本篇,我们介绍四种排序算法,分别是:归并排序、快速排序、希尔排序和堆排序。(以下代码均用C++实现)
预知:以下代码中用到的swap函数的定义头文件是#include<algorithm>
一、归并排序:
归并排序(英语:Merge sort,或mergesort),是创建在归并操作上的一种有效的排序算法,效率为 O ( n log n ) O(n\log n) O(nlogn)(大 O O O符号)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行:
- 分割:递归地把当前序列平均分割成两半。
- 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。
图解如下:
代码如下:
//1.归并排序
void mergeSort(int source[], int n){ //n代表数组大小
int *a = source;
int *b = new int[n];
int seg, start;
for(seg = 1; seg < n; seg += seg){
for(start = 0; start < n; start += seg * 2){
int low = start, mid = min(start + seg, n), high = min(start + seg * 2, n);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2=high;
while(start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while(start1 < end1)
b[k++] = a[start1++];
while(start2 < end2)
b[k++] = a[start2++];
}
int *temp = a;
a = b;
b = temp;
}
if(a != source){
for(int i = 0; i < n; i++)
b[i] = a[i];
b = a;
}
delete[] b; //最后不要忘了释放掉b的空间,数组不要忘了加中括号("[]")
}
复杂度分析:
时间复杂度 最坏时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)、最优时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)、平均时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)
空间复杂度 需要辅助空间
O
(
n
)
O(n)
O(n)
二、快速排序:
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序n个项要
O
(
n
log
n
)
O(n\log n)
O(nlogn)(大O符号)次比较。在最坏状况下则需要
O
(
n
2
)
O(n^2)
O(n2)次比较,但这种状况并不常见。事实上,快速排序
O
(
n
log
n
)
O(n\log n)
O(nlogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。
步骤为:
- 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot);
- 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
- 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
若想更清楚地了解快排,可查看该链接,链接中有清晰的图文描述。
图解如下:
代码如下:
//2.快速排序
/*
你可以直接使用algorithm头文件中的sort(a, a + n)来进行快排
sort(a, a + n);//排序a[0]~a[n-1]的所有数
*/
void quickSort(int source[], int start, int end){
if(start >= end) return;
int mid = source[end]; //枢纽元素值(基准数)
int left = start, right = end - 1;
while(left<right){ //在整个范围内搜寻比枢纽元素值小或大的元素,然后将左侧元素与右侧元素交换
while(source[left] < mid && left < right){ //试图在左侧找到一个比枢纽元更大的元素
left++;
}
while(source[right] >= mid && left < right){ //试图在右侧找到一个比枢纽元更小的元素
right--;
}
swap(source[left],source[right]);
}
if(source[left] >= source[end])
swap(source[left],source[end]);
else
left++;
quickSort(source, start, left - 1);
quickSort(source, left + 1, end);
}
时间复杂度 最坏时间复杂度
O
(
n
2
)
O(n^2)
O(n2)、最优时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)、平均时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)
空间复杂度 需要辅助空间
O
(
log
n
)
O(\log n)
O(logn)
若想更加详细清楚地了解快排的复杂度分析,可以自行去维基百科搜索。
三、希尔排序:
希尔排序(Shellsort),也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位(即步长为1)。
我们可以把希尔排序理解为步长更大的插入排序,即刚开始选择一个大的步长,慢慢缩小步长直至为1,这也就利用了上面的插入排序的第一条性质。
图解如下:
以23, 10, 4, 1的步长序列进行希尔排序。
代码如下:
//3.希尔排序
void shellSort(int source[], int n){ //n代表数组大小
int h = 1; //步长
while(h < n / 3){
h = 3 * h + 1;
}
while(h >= 1){
for(int i = h; i < n; i++){
for(int j = i; j >= h && source[j] < source[j - h]; j -= h)
swap(source[j], source[j - h]);
}
h /= 3;
}
}
时间复杂度 最优时间复杂度 O ( n ) O(n) O(n)、最坏时间复杂度:根据步长序列的不同而不同,好的是 O ( n log 2 n ) O(n\log ^2 n) O(nlog2n)
空间复杂度 需要辅助空间 O ( 1 ) O(1) O(1)
希尔排序的步长选择与时间复杂度的关系:
四、堆排序:
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
- 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点;
- 创建最大堆(Build Max Heap):将堆中的所有数据重新排序;
- 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算。
参考更详细的堆排序介绍,请点击该链接
图解如下:
如若感觉上图过于抽象,请点击上方的堆排序介绍,相信你看完了之后会对堆排序有一个更加清晰的认识。
代码如下:
//4.堆排序
void maxHeapify(int source[], int start, int end){
int father = start;
int son = father * 2 + 1;
while(son <= end){ //子节点在指标范围内才进行比较
if(son + 1 <= end && source[son] < source[son + 1]) //先比较两个子节点大小,选择最大的子节点
son++;
if(source[father] > source[son]) //若父节点已经大于子节点,则代表已经调整完毕,跳出该函数
return;
else{ //否则交换父子内容,再继续子节点和父节点的比较
swap(source[father], source[son]);
father = son;
son = father * 2 + 1;
}
}
}
void heapSort(int source[], int n){ //n代表数组大小
for(int i = n / 2 - 1; i >= 0; i--) //从最后一个非叶节点(n/2-1)开始调整
maxHeapify(source,i,n-1);
//上步循环运行完毕之后,构成了一个大顶堆
for(int i = n - 1; i > 0; i--){ //调整元素,完毕后即完成了堆排序
swap(source[0],source[i]);
maxHeapify(source,0,i-1);
}
}
复杂度分析:
时间复杂度 最坏时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)、最优时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)、平均时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn)
空间复杂度 需要辅助空间
O
(
1
)
O(1)
O(1)
简要比较:
以上解释和图表大部分来自维基百科
如有错误欢迎来指正